Skip to content

Conversation

@sharkdp
Copy link
Contributor

@sharkdp sharkdp commented Jul 23, 2025

Summary

We previously didn't recognize Literal[Color.RED] as single-valued, if the enum also derived from str or int:

from enum import Enum

class Color(str, Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

def _(color: Color):
    if color == Color.RED:
        reveal_type(color)  # previously: Color, now: Literal[Color.RED]

The reason for that was that int and str have "custom" __eq__ and __ne__ implementations that return bool. We do not treat enum literals from classes with custom __eq__ and __ne__ implementations as single-valued, but of course we know that int.__eq__ and str.__eq__ are well-behaved.

Test Plan

New Markdown tests.

@sharkdp sharkdp added the ty Multi-file analysis & type inference label Jul 23, 2025
@sharkdp sharkdp changed the title [ty] Infer single-valuedness for enums based on int/str [ty] Infer single-valuedness for enums based on int/str Jul 23, 2025
@github-actions
Copy link
Contributor

mypy_primer results

No ecosystem changes detected ✅
No memory usage changes detected ✅

@sharkdp sharkdp marked this pull request as ready for review July 23, 2025 13:34
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

I was also wondering if we could generalize this logic so that it applies to all instances of @final classes that do not override __eq__/__ne__ from object/int/str (not just instances of enum classes).

It's really the finality of enum classes that allows us to safely determine whether instances of enums are single-valued or not: because an enum class is final, we know that there are no possible subclasses of the enum class, therefore there are no possible subtypes of the enum type that could have badly behaved __eq__ or __ne__ methods. And that reasoning applies to any final class, not just enums

@sharkdp sharkdp merged commit 5a55bab into main Jul 23, 2025
38 checks passed
@sharkdp sharkdp deleted the david/int-and-str-enums branch July 23, 2025 13:55
UnboundVariable pushed a commit to UnboundVariable/ruff that referenced this pull request Jul 23, 2025
* main: (28 commits)
  [ty] highlight the argument in `static_assert` error messages (astral-sh#19426)
  [ty] Infer single-valuedness for enums based on `int`/`str` (astral-sh#19510)
  [ty] Restructure submodule query around `File` dependency
  [ty] Make `Module` a Salsa ingredient
  [ty] Reachability analysis for `isinstance(…)` branches (astral-sh#19503)
  [ty] Normalize single-member enums to their instance type (astral-sh#19502)
  [ty] Invert `ty_ide` and `ty_project` dependency (astral-sh#19501)
  [ty] Implement mock language server for testing (astral-sh#19391)
  [ty] Detect enums if metaclass is a subtype of EnumType/EnumMeta (astral-sh#19481)
  [ty] perform type narrowing for places marked `global` too (astral-sh#19381)
  [ty] Use `ThinVec` for sub segments in `PlaceExpr` (astral-sh#19470)
  [ty] Splat variadic arguments into parameter list (astral-sh#18996)
  [`flake8-pyi`] Skip fix if all `Union` members are `None` (`PYI016`)  (astral-sh#19416)
  Skip notebook with errors in ecosystem check (astral-sh#19491)
  [ty] Consistent use of American english (in rules) (astral-sh#19488)
  [ty] Support iterating over enums (astral-sh#19486)
  Fix panic for illegal `Literal[…]` annotations with inner subscript expressions (astral-sh#19489)
  Move fix suggestion to subdiagnostic (astral-sh#19464)
  [ty] Implement non-stdlib stub mapping for classes and functions (astral-sh#19471)
  [ty] Disallow illegal uses of `ClassVar` (astral-sh#19483)
  ...

# Conflicts:
#	crates/ty_ide/src/goto.rs
dcreager added a commit that referenced this pull request Jul 23, 2025
* main:
  [ty] Fix narrowing and reachability of class patterns with arguments (#19512)
  [ty] Implemented partial support for "find references" language server feature. (#19475)
  [`flake8-use-pathlib`] Add autofix for `PTH101`, `PTH104`, `PTH105`, `PTH121` (#19404)
  [`perflint`] Parenthesize generator expressions (`PERF401`) (#19325)
  [`pep8-naming`] Fix `N802` false positives for `CGIHTTPRequestHandler` and `SimpleHTTPRequestHandler` (#19432)
  [`pylint`] Handle empty comments after line continuation (`PLR2044`) (#19405)
  Move concise diagnostic rendering to `ruff_db` (#19398)
  [ty] highlight the argument in `static_assert` error messages (#19426)
  [ty] Infer single-valuedness for enums based on `int`/`str` (#19510)
  [ty] Restructure submodule query around `File` dependency
  [ty] Make `Module` a Salsa ingredient
  [ty] Reachability analysis for `isinstance(…)` branches (#19503)
  [ty] Normalize single-member enums to their instance type (#19502)
  [ty] Invert `ty_ide` and `ty_project` dependency (#19501)
  [ty] Implement mock language server for testing (#19391)
  [ty] Detect enums if metaclass is a subtype of EnumType/EnumMeta (#19481)
  [ty] perform type narrowing for places marked `global` too (#19381)
AlexWaygood pushed a commit that referenced this pull request Jul 25, 2025
## Summary

We previously didn't recognize `Literal[Color.RED]` as single-valued, if
the enum also derived from `str` or `int`:
```py
from enum import Enum

class Color(str, Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

def _(color: Color):
    if color == Color.RED:
        reveal_type(color)  # previously: Color, now: Literal[Color.RED]
```

The reason for that was that `int` and `str` have "custom" `__eq__` and
`__ne__` implementations that return `bool`. We do not treat enum
literals from classes with custom `__eq__` and `__ne__` implementations
as single-valued, but of course we know that `int.__eq__` and
`str.__eq__` are well-behaved.

## Test Plan

New Markdown tests.
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