Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[ty] Allow enum member accesses on self#25077

Merged
charliermarsh merged 2 commits into
mainfrom
charlie/self-enum-member-bound
May 21, 2026
Merged

[ty] Allow enum member accesses on self#25077
charliermarsh merged 2 commits into
mainfrom
charlie/self-enum-member-bound

Conversation

@charliermarsh

@charliermarsh charliermarsh commented May 10, 2026

Copy link
Copy Markdown
Member

Summary

This allows enum members to be resolved when accessed through a Self-typed enum instance. Previously, enum member lookup handled class-member patterns like case Answer.YES:, but not equivalent self-member patterns like:

class Answer(Enum):
    NO = 0
    YES = 1

    def is_yes(self) -> bool:
        match self:
            case self.YES:
                return True

The lookup reuses the receiver’s nominal class for nominal instances and bounded Self typevars before resolving enum metadata, so case self.YES: narrows the same way as case Answer.YES:.

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label May 10, 2026
@astral-sh-bot

astral-sh-bot Bot commented May 10, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 89.36%. The percentage of expected errors that received a diagnostic held steady at 85.49%. The number of fully passing files held steady at 88/134.

@charliermarsh charliermarsh force-pushed the charlie/self-enum-member-bound branch from da87f2a to d60347b Compare May 10, 2026 00:06
@astral-sh-bot

astral-sh-bot Bot commented May 10, 2026

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
prefect 688.86MB 688.99MB +0.02% (130.59kB)
sphinx 256.66MB 256.76MB +0.04% (101.30kB)
trio 113.60MB 113.62MB +0.02% (20.61kB)
flake8 47.52MB 47.53MB +0.01% (5.52kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
Type<'db>::member_lookup_with_policy_ 16.50MB 16.60MB +0.59% (99.91kB)
BoundMethodType<'db>::into_callable_type_ 264.75kB 276.98kB +4.62% (12.23kB)
infer_expression_types_impl 52.81MB 52.82MB +0.01% (6.28kB)
infer_expression_type_impl 7.83MB 7.83MB +0.05% (3.70kB)
infer_definition_types 82.00MB 82.01MB +0.00% (3.08kB)
infer_scope_types_impl 48.11MB 48.11MB +0.00% (1.69kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 6.88MB 6.88MB +0.02% (1.25kB)
all_negative_narrowing_constraints_for_expression 6.27MB 6.27MB +0.02% (1.02kB)
all_narrowing_constraints_for_expression 6.48MB 6.48MB +0.01% (972.00B)
try_call_bin_op_return_type_impl 288.23kB 288.61kB +0.13% (396.00B)
infer_unpack_types 909.43kB 909.50kB +0.01% (72.00B)
loop_header_reachability 440.98kB 441.00kB +0.00% (12.00B)

sphinx

Name Old New Diff Outcome
Type<'db>::member_lookup_with_policy_ 6.89MB 6.98MB +1.24% (87.39kB)
BoundMethodType<'db>::into_callable_type_ 236.89kB 244.21kB +3.09% (7.31kB)
infer_expression_types_impl 18.50MB 18.50MB +0.01% (1.52kB)
infer_definition_types 22.03MB 22.03MB +0.01% (1.18kB)
infer_expression_type_impl 2.89MB 2.89MB +0.04% (1.16kB)
infer_unpack_types 396.66kB 397.43kB +0.19% (792.00B)
infer_scope_types_impl 12.95MB 12.95MB +0.00% (636.00B)
try_call_bin_op_return_type_impl 198.95kB 199.51kB +0.28% (576.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_ 2.35MB 2.36MB +0.02% (384.00B)
all_negative_narrowing_constraints_for_expression 2.34MB 2.34MB +0.01% (216.00B)
all_narrowing_constraints_for_expression 2.45MB 2.45MB +0.01% (192.00B)

trio

Name Old New Diff Outcome
Type<'db>::member_lookup_with_policy_ 1.87MB 1.88MB +0.72% (13.83kB)
BoundMethodType<'db>::into_callable_type_ 58.75kB 63.32kB +7.78% (4.57kB)
infer_expression_types_impl 6.09MB 6.09MB +0.01% (852.00B)
infer_expression_type_impl 1.26MB 1.26MB +0.06% (768.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_ 711.59kB 711.79kB +0.03% (204.00B)
infer_definition_types 7.24MB 7.24MB +0.00% (156.00B)
all_narrowing_constraints_for_expression 577.09kB 577.21kB +0.02% (120.00B)
all_negative_narrowing_constraints_for_expression 548.81kB 548.93kB +0.02% (120.00B)
infer_scope_types_impl 4.00MB 4.00MB +0.00% (24.00B)
loop_header_reachability 125.14kB 125.15kB +0.01% (12.00B)
infer_unpack_types 130.95kB 130.96kB +0.01% (12.00B)

flake8

Name Old New Diff Outcome
Type<'db>::member_lookup_with_policy_ 550.06kB 553.77kB +0.68% (3.71kB)
BoundMethodType<'db>::into_callable_type_ 19.62kB 20.74kB +5.73% (1.12kB)
try_call_bin_op_return_type_impl 6.24kB 6.52kB +4.51% (288.00B)
infer_definition_types 1.71MB 1.71MB +0.01% (156.00B)
infer_expression_types_impl 931.39kB 931.48kB +0.01% (96.00B)
infer_expression_type_impl 117.49kB 117.57kB +0.07% (84.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_ 306.46kB 306.50kB +0.02% (48.00B)
all_narrowing_constraints_for_expression 94.25kB 94.26kB +0.01% (12.00B)
all_negative_narrowing_constraints_for_expression 91.04kB 91.05kB +0.01% (12.00B)

@astral-sh-bot

astral-sh-bot Bot commented May 10, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

Lint rule Added Removed Changed
unresolved-attribute 0 7 0
Total 0 7 0

Raw diff:

cloud-init (https://github.com/canonical/cloud-init)
- tests/integration_tests/instances.py:58:34 error[unresolved-attribute] Object of type `int` has no attribute `name`
- tests/integration_tests/instances.py:58:50 error[unresolved-attribute] Object of type `int` has no attribute `name`

mkdocs (https://github.com/mkdocs/mkdocs)
- mkdocs/structure/files.py:47:29 error[unresolved-attribute] Object of type `int` has no attribute `value`
- mkdocs/structure/files.py:50:30 error[unresolved-attribute] Object of type `int` has no attribute `value`
- mkdocs/structure/files.py:53:30 error[unresolved-attribute] Object of type `int` has no attribute `value`
- mkdocs/structure/files.py:56:29 error[unresolved-attribute] Object of type `int` has no attribute `value`
- mkdocs/structure/files.py:59:30 error[unresolved-attribute] Object of type `int` has no attribute `value`

Full report with detailed diff (timing results)

@charliermarsh charliermarsh marked this pull request as ready for review May 10, 2026 00:13
@charliermarsh charliermarsh force-pushed the charlie/self-enum-member-bound branch 2 times, most recently from 3e0357a to 781d3e9 Compare May 14, 2026 09:45
@charliermarsh charliermarsh force-pushed the charlie/self-enum-member-bound branch from 781d3e9 to 34342b6 Compare May 21, 2026 08:47

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like they already worked fine before this PR, but I think we should also add a test for attribute access on cls in classmethods, if we don't have one already:

from enum import Enum

class Foo(Enum):
    A = 1
    B = 2

    @classmethod
    def foo(cls, x: Foo):
        reveal_type(cls.A)  # revealed: Literal[Foo.A]

        match x:
            case cls.A:
                reveal_type(x)  # revealed: Literal[Foo.A]
            case cls.B:
                reveal_type(x)  # revealed: Literal[Foo.A]
            case _:
                reveal_type(x)  # revealed: Never

Comment thread crates/ty_python_semantic/src/types.rs Outdated
Comment on lines 3532 to 3544
self.nominal_class(db).map(|class| class.class_literal(db))
}
_ => None,
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify the logic here: this is only complicated because there's a missing branch in Type::nominal_class()

diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index febf3e89fd..447b5c2478 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -1229,6 +1229,7 @@ impl<'db> Type<'db> {
                 };
                 bound.nominal_class(db)
             }
+            Type::LiteralValue(literal) => literal.fallback_instance(db).nominal_class(db),
             _ => None,
         }
     }
@@ -3531,19 +3532,7 @@ impl<'db> Type<'db> {
             | Type::TypedDict(_) => {
                 // Enum members can be accessed through enum instances and other enum members,
                 // e.g. `answer.YES` or `Answer.YES.NO`.
-                let enum_class = match self {
-                    Type::LiteralValue(literal) => literal
-                        .as_enum()
-                        .map(|enum_literal| enum_literal.enum_class(db)),
-                    // This includes `Self` typevars with an enum-class upper bound, which allows
-                    // enum methods to access members through `self`, e.g. `case self.YES:`.
-                    Type::NominalInstance(_) | Type::TypeVar(_) => {
-                        self.nominal_class(db).map(|class| class.class_literal(db))
-                    }
-                    _ => None,
-                };
-
-                if let Some(enum_class) = enum_class
+                if let Some(enum_class) = self.nominal_class(db).map(|cls| cls.class_literal(db))
                     && let Some(metadata) = enum_metadata(db, enum_class)
                     && let Some(resolved_name) = metadata.resolve_member(&name)
                 {

@AlexWaygood AlexWaygood assigned AlexWaygood and unassigned carljm May 21, 2026
@charliermarsh charliermarsh force-pushed the charlie/self-enum-member-bound branch from 34342b6 to c236b92 Compare May 21, 2026 11:30
@charliermarsh

Copy link
Copy Markdown
Member Author

Thank you!

@charliermarsh charliermarsh merged commit e9d72bb into main May 21, 2026
58 of 59 checks passed
@charliermarsh charliermarsh deleted the charlie/self-enum-member-bound branch May 21, 2026 11:39
thejchap pushed a commit to thejchap/ruff that referenced this pull request May 23, 2026
## Summary

This allows enum members to be resolved when accessed through a
`Self`-typed enum instance. Previously, enum member lookup handled
class-member patterns like `case Answer.YES:`, but not equivalent
self-member patterns like:

```py
class Answer(Enum):
    NO = 0
    YES = 1

    def is_yes(self) -> bool:
        match self:
            case self.YES:
                return True
```

The lookup reuses the receiver’s nominal class for nominal instances and
bounded `Self` typevars before resolving enum metadata, so `case
self.YES:` narrows the same way as `case Answer.YES:`.
anishgirianish pushed a commit to anishgirianish/ruff that referenced this pull request May 28, 2026
## Summary

This allows enum members to be resolved when accessed through a
`Self`-typed enum instance. Previously, enum member lookup handled
class-member patterns like `case Answer.YES:`, but not equivalent
self-member patterns like:

```py
class Answer(Enum):
    NO = 0
    YES = 1

    def is_yes(self) -> bool:
        match self:
            case self.YES:
                return True
```

The lookup reuses the receiver’s nominal class for nominal instances and
bounded `Self` typevars before resolving enum metadata, so `case
self.YES:` narrows the same way as `case Answer.YES:`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants