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

Skip to content

[ty] Model non-exhaustive enum member sets#26277

Merged
charliermarsh merged 2 commits into
mainfrom
charlie/enum-member-exhaustiveness
Jun 24, 2026
Merged

[ty] Model non-exhaustive enum member sets#26277
charliermarsh merged 2 commits into
mainfrom
charlie/enum-member-exhaustiveness

Conversation

@charliermarsh

@charliermarsh charliermarsh commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

We currently treat the members declared on every enum as its complete runtime domain. That assumption does not hold for Flag and IntFlag, which also have zero and unnamed combinations, or for enums whose custom _missing_ method or metaclass can create additional members. As a result, we can incorrectly treat a one-member enum as a singleton, widen all declared members to the nominal enum, erase a non-empty complement, or consider a match over the declared members exhaustive.

This adds a members_are_exhaustive property to EnumClassLiteral and uses it wherever we rely on an enum being a finite set: complement construction, union simplification, singleton detection, finite-alternative narrowing, and match exhaustiveness. Open enums retain the nominal remainder after known members are excluded.

Explicit membership tests remain precise when the enum uses identity comparison. For example, a metaclass may add INJECTED, but testing against ONLY still establishes that exact member on the positive branch and excludes only that member on the negative branch:

class OpenEnum(Enum, metaclass=InjectingEnumMeta):
    ONLY = 1

def check(value: OpenEnum):
    if value in (OpenEnum.ONLY,):
        reveal_type(value)  # Literal[OpenEnum.ONLY]
    else:
        reveal_type(value)  # OpenEnum & ~Literal[OpenEnum.ONLY]

This is the semantic foundation split out of #26270; that PR adds the compact same-enum comparison path on top.

@charliermarsh charliermarsh added the ty Multi-file analysis & type inference label Jun 23, 2026
@astral-sh-bot

astral-sh-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

Typing conformance results improved 🎉

The percentage of diagnostics emitted that were expected errors increased from 94.37% to 94.47%. The percentage of expected errors that received a diagnostic increased from 89.00% to 89.10%. The number of fully passing files improved from 94/134 to 95/134.

Summary

How are test cases classified?

Each test case represents one expected error annotation or a group of annotations sharing a tag. Counts are per test case, not per diagnostic — multiple diagnostics on the same line count as one. Required annotations (E) are true positives when ty flags the expected location and false negatives when it does not. Optional annotations (E?) are true positives when flagged but true negatives (not false negatives) when not. Tagged annotations (E[tag]) require ty to flag exactly one of the tagged lines; tagged multi-annotations (E[tag+]) allow any number up to the tag count. Flagging unexpected locations counts as a false positive.

Metric Old New Diff Outcome
True Positives 955 956 +1 ⏫ (✅)
False Positives 57 56 -1 ⏬ (✅)
False Negatives 118 117 -1 ⏬ (✅)
Total Diagnostics 1060 1060 +0
Precision 94.37% 94.47% +0.10% ⏫ (✅)
Recall 89.00% 89.10% +0.09% ⏫ (✅)
Passing Files 94/134 95/134 +1 ⏫ (✅)

Test file breakdown

1 file altered
File True Positives False Positives False Negatives Status
enums_expansion.py 1 (+1) ✅ 0 (-1) ✅ 0 (-1) ✅ ✅ Newly Passing 🎉
Total (all files) 956 (+1) ✅ 56 (-1) ✅ 117 (-1) ✅ 95/134

True positives added (1)

1 diagnostic
Test case Diff

enums_expansion.py:53

+error[type-assertion-failure] Type `CustomFlags & ~Literal[CustomFlags.FLAG1] & ~Literal[CustomFlags.FLAG2]` does not match asserted type `Literal[CustomFlags.FLAG3]`

False positives removed (1)

1 diagnostic
Test case Diff

enums_expansion.py:52

-error[type-assertion-failure] Type `Literal[CustomFlags.FLAG3]` does not match asserted type `CustomFlags`

@astral-sh-bot

astral-sh-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
prefect 450.57MB 450.63MB +0.01% (57.32kB)
sphinx 167.49MB 167.52MB +0.02% (29.84kB)
trio 70.58MB 70.59MB +0.02% (11.56kB)
flake8 29.12MB 29.13MB +0.02% (5.32kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
all_narrowing_constraints_for_expression 5.13MB 5.15MB +0.23% (11.90kB)
enum_class_literal 319.10kB 325.62kB +2.04% (6.52kB)
Type<'db>::apply_specialization_inner_::interned_arguments 2.94MB 2.95MB +0.18% (5.31kB)
Type<'db>::class_member_with_policy_ 7.88MB 7.89MB +0.06% (4.90kB)
member_lookup_with_policy_inner::interned_arguments 6.72MB 6.73MB +0.06% (3.98kB)
Type<'db>::apply_specialization_inner_ 2.02MB 2.03MB +0.17% (3.47kB)
Type<'db>::class_member_with_policy_::interned_arguments 5.72MB 5.72MB +0.06% (3.45kB)
infer_expression_types_impl 37.69MB 37.69MB +0.01% (3.41kB)
member_lookup_with_policy_inner 10.12MB 10.12MB +0.03% (3.27kB)
infer_definition_types 50.56MB 50.56MB +0.01% (2.98kB)
Specialization 2.66MB 2.66MB +0.06% (1.53kB)
is_possibly_constraint_set_assignable::interned_arguments 351.23kB 352.34kB +0.32% (1.12kB)
when_constraint_set_assignable_to_owned_impl::interned_arguments 637.14kB 638.26kB +0.18% (1.12kB)
BoundMethodType 1.69MB 1.70MB +0.06% (1.09kB)
when_constraint_set_assignable_to_owned_impl 2.09MB 2.09MB +0.03% (728.00B)
... 15 more

sphinx

Name Old New Diff Outcome
all_narrowing_constraints_for_expression 1.89MB 1.90MB +0.46% (8.85kB)
Type<'db>::apply_specialization_inner_::interned_arguments 1.54MB 1.54MB +0.17% (2.73kB)
infer_expression_types_impl 14.75MB 14.75MB +0.02% (2.63kB)
Type<'db>::class_member_with_policy_ 3.57MB 3.58MB +0.06% (2.11kB)
Type<'db>::apply_specialization_inner_ 1.03MB 1.03MB +0.17% (1.80kB)
member_lookup_with_policy_inner::interned_arguments 2.69MB 2.69MB +0.06% (1.76kB)
enum_class_literal 69.22kB 70.92kB +2.46% (1.70kB)
infer_definition_types 13.84MB 13.85MB +0.01% (1.70kB)
Type<'db>::class_member_with_policy_::interned_arguments 2.35MB 2.35MB +0.06% (1.52kB)
member_lookup_with_policy_inner 4.20MB 4.20MB +0.03% (1.31kB)
Specialization 1.40MB 1.40MB +0.05% (784.00B)
BoundMethodType 771.80kB 772.34kB +0.07% (560.00B)
is_possibly_constraint_set_assignable::interned_arguments 165.69kB 166.12kB +0.26% (440.00B)
when_constraint_set_assignable_to_owned_impl::interned_arguments 326.30kB 326.73kB +0.13% (440.00B)
infer_statement_types_impl 484.00kB 484.34kB +0.07% (352.00B)
... 13 more

trio

Name Old New Diff Outcome
enum_class_literal 22.39kB 24.27kB +8.41% (1.88kB)
Type<'db>::class_member_with_policy_ 887.61kB 889.12kB +0.17% (1.52kB)
member_lookup_with_policy_inner::interned_arguments 764.41kB 765.82kB +0.18% (1.41kB)
Type<'db>::class_member_with_policy_::interned_arguments 626.13kB 627.35kB +0.19% (1.22kB)
all_narrowing_constraints_for_expression 450.03kB 451.20kB +0.26% (1.17kB)
member_lookup_with_policy_inner 1.01MB 1.02MB +0.11% (1.12kB)
Type<'db>::apply_specialization_inner_::interned_arguments 504.92kB 505.70kB +0.15% (800.00B)
Type<'db>::apply_specialization_inner_ 347.32kB 347.84kB +0.15% (528.00B)
infer_expression_types_impl 4.35MB 4.35MB +0.01% (312.00B)
Specialization 456.73kB 456.95kB +0.05% (224.00B)
enum_metadata 150.09kB 150.30kB +0.14% (216.00B)
BoundMethodType 182.89kB 183.05kB +0.09% (160.00B)
analyze_non_terminal_call 374.19kB 374.33kB +0.04% (144.00B)
InferableTypeVarsInner 70.59kB 70.72kB +0.19% (136.00B)
known_class_to_class_literal 4.02kB 4.10kB +2.14% (88.00B)
... 14 more

flake8

Name Old New Diff Outcome
Type<'db>::class_member_with_policy_ 233.23kB 234.07kB +0.36% (864.00B)
enum_class_literal 6.05kB 6.69kB +10.59% (656.00B)
member_lookup_with_policy_inner::interned_arguments 204.26kB 204.84kB +0.29% (600.00B)
Type<'db>::class_member_with_policy_::interned_arguments 167.98kB 168.49kB +0.30% (520.00B)
all_narrowing_constraints_for_expression 70.22kB 70.73kB +0.72% (520.00B)
member_lookup_with_policy_inner 288.54kB 288.98kB +0.15% (456.00B)
Type<'db>::apply_specialization_inner_::interned_arguments 174.14kB 174.53kB +0.22% (400.00B)
Type<'db>::apply_specialization_inner_ 117.90kB 118.16kB +0.22% (264.00B)
enum_metadata 42.31kB 42.52kB +0.50% (216.00B)
Specialization 174.45kB 174.56kB +0.06% (112.00B)
infer_expression_types_impl 640.31kB 640.41kB +0.02% (104.00B)
when_constraint_set_assignable_to_owned_impl 102.70kB 102.79kB +0.09% (96.00B)
known_class_to_class_literal 3.07kB 3.16kB +2.80% (88.00B)
place_by_id 136.95kB 137.03kB +0.06% (88.00B)
is_possibly_constraint_set_assignable::interned_arguments 18.99kB 19.08kB +0.45% (88.00B)
... 8 more

@astral-sh-bot

astral-sh-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-return-type 3 0 0
Total 3 0 0

Raw diff:

rotki (https://github.com/rotki/rotki)
+ rotkehlchen/tasks/historical_balances.py:186:20 error[invalid-return-type] Return type does not match returned value: expected `list[tuple[Bucket, Literal[EventDirection.IN, EventDirection.OUT]]]`, found `list[tuple[Bucket, Literal[EventDirection.IN, EventDirection.OUT]] | tuple[Self@from_event, EventDirection]]`
+ rotkehlchen/tasks/historical_balances.py:195:16 error[invalid-return-type] Return type does not match returned value: expected `list[tuple[Bucket, Literal[EventDirection.IN, EventDirection.OUT]]]`, found `list[tuple[Bucket, Literal[EventDirection.IN, EventDirection.OUT]] | tuple[Self@from_event, EventDirection]]`

steam.py (https://github.com/Gobot1234/steam.py)
+ steam/ext/commands/cooldown.py:52:43 error[invalid-return-type] Function can implicitly return `None`, which is not assignable to return type `BucketTypeType`

Full report with detailed diff (timing results)

@charliermarsh charliermarsh marked this pull request as ready for review June 23, 2026 18:10
@charliermarsh charliermarsh requested a review from a team as a code owner June 23, 2026 18:10
@astral-sh-bot astral-sh-bot Bot requested a review from sharkdp June 23, 2026 18:10
@charliermarsh charliermarsh marked this pull request as draft June 23, 2026 18:11
@codspeed-hq

codspeed-hq Bot commented Jun 23, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 71 untouched benchmarks
⏩ 64 skipped benchmarks1


Comparing charlie/enum-member-exhaustiveness (b7c3250) with main (2f3791b)

Open in CodSpeed

Footnotes

  1. 64 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@charliermarsh charliermarsh marked this pull request as ready for review June 23, 2026 18:13
.collect();
aliases.sort_unstable();
let members_are_exhaustive = !metadata.value_construction.metaclass_may_transform_values
&& !Type::ClassLiteral(class).is_subtype_of(db, KnownClass::Flag.to_subclass_of(db))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This doesn't "finish" support for Flag, but it fell out of optimization work in a separate PR (and turns out to get us passing a new conformance file).

@charliermarsh charliermarsh force-pushed the charlie/enum-member-exhaustiveness branch from 7e97231 to c031ea2 Compare June 23, 2026 19:22
@charliermarsh charliermarsh force-pushed the charlie/enum-member-exhaustiveness branch 2 times, most recently from 9ca4e30 to b619ccc Compare June 24, 2026 01:31

@sharkdp sharkdp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice - thank you!

```

Compact enum complements that are equivalent to a literal union are still spellable.
When narrowing leaves only one enum member, the inferred type is the corresponding literal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is an entirely different test now. Why was this changed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Because for Flag, the test is actually incorrect!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I can change the copy back at least though if that's what you mean.

@charliermarsh charliermarsh force-pushed the charlie/enum-member-exhaustiveness branch from b619ccc to b7c3250 Compare June 24, 2026 13:52
@charliermarsh charliermarsh enabled auto-merge (squash) June 24, 2026 13:52
@charliermarsh charliermarsh merged commit c0ee43d into main Jun 24, 2026
62 checks passed
@charliermarsh charliermarsh deleted the charlie/enum-member-exhaustiveness branch June 24, 2026 13:57
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.

2 participants