Skip to content

Commit

Permalink
Fix incorrect truthyness for Enum types and literals (#17337)
Browse files Browse the repository at this point in the history
Fixes: #17333 

This ensures `can_be_true` and `can_be_false` on enum literals depends
on the specific `Enum` fallback type behind the `Literal`, since
`__bool__` can be overriden like on any other type.

Additionally typeops `true_only` and `false_only` now respect the
metaclass when looking up the return values of `__bool__` and `__len__`,
which ensures that a default `Enum` that doesn't override `__bool__` is
still considered always truthy.
  • Loading branch information
Daverball authored Nov 14, 2024
1 parent 3e52d0c commit fa01a07
Show file tree
Hide file tree
Showing 15 changed files with 136 additions and 4 deletions.
8 changes: 8 additions & 0 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,8 +650,16 @@ def _remove_redundant_union_items(items: list[Type], keep_erased: bool) -> list[
def _get_type_method_ret_type(t: Type, *, name: str) -> Type | None:
t = get_proper_type(t)

# For Enum literals the ret_type can change based on the Enum
# we need to check the type of the enum rather than the literal
if isinstance(t, LiteralType) and t.is_enum_literal():
t = t.fallback

if isinstance(t, Instance):
sym = t.type.get(name)
# Fallback to the metaclass for the lookup when necessary
if not sym and (m := t.type.metaclass_type):
sym = m.type.get(name)
if sym:
sym_type = get_proper_type(sym.type)
if isinstance(sym_type, CallableType):
Expand Down
18 changes: 18 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2818,10 +2818,28 @@ def __init__(
self.fallback = fallback
self._hash = -1 # Cached hash value

# NOTE: Enum types are always truthy by default, but this can be changed
# in subclasses, so we need to get the truthyness from the Enum
# type rather than base it on the value (which is a non-empty
# string for enums, so always truthy)
# TODO: We should consider moving this branch to the `can_be_true`
# `can_be_false` properties instead, so the truthyness only
# needs to be determined once per set of Enum literals.
# However, the same can be said for `TypeAliasType` in some
# cases and we only set the default based on the type it is
# aliasing. So if we decide to change this, we may want to
# change that as well. perf_compare output was inconclusive
# but slightly favored this version, probably because we have
# almost no test cases where we would redundantly compute
# `can_be_false`/`can_be_true`.
def can_be_false_default(self) -> bool:
if self.fallback.type.is_enum:
return self.fallback.can_be_false
return not self.value

def can_be_true_default(self) -> bool:
if self.fallback.type.is_enum:
return self.fallback.can_be_true
return bool(self.value)

def accept(self, visitor: TypeVisitor[T]) -> T:
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/check-custom-plugin.test
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,7 @@ class Cls(enum.Enum):
attr = 'test'

reveal_type(Cls.attr) # N: Revealed type is "builtins.int"
[builtins fixtures/enum.pyi]
[file mypy.ini]
\[mypy]
plugins=<ROOT>/test-data/unit/plugins/class_attr_hook.py
Expand Down
Loading

0 comments on commit fa01a07

Please sign in to comment.