Skip to content

Commit fa01a07

Browse files
authored
Fix incorrect truthyness for Enum types and literals (#17337)
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.
1 parent 3e52d0c commit fa01a07

15 files changed

+136
-4
lines changed

mypy/typeops.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,8 +650,16 @@ def _remove_redundant_union_items(items: list[Type], keep_erased: bool) -> list[
650650
def _get_type_method_ret_type(t: Type, *, name: str) -> Type | None:
651651
t = get_proper_type(t)
652652

653+
# For Enum literals the ret_type can change based on the Enum
654+
# we need to check the type of the enum rather than the literal
655+
if isinstance(t, LiteralType) and t.is_enum_literal():
656+
t = t.fallback
657+
653658
if isinstance(t, Instance):
654659
sym = t.type.get(name)
660+
# Fallback to the metaclass for the lookup when necessary
661+
if not sym and (m := t.type.metaclass_type):
662+
sym = m.type.get(name)
655663
if sym:
656664
sym_type = get_proper_type(sym.type)
657665
if isinstance(sym_type, CallableType):

mypy/types.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2818,10 +2818,28 @@ def __init__(
28182818
self.fallback = fallback
28192819
self._hash = -1 # Cached hash value
28202820

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

28242840
def can_be_true_default(self) -> bool:
2841+
if self.fallback.type.is_enum:
2842+
return self.fallback.can_be_true
28252843
return bool(self.value)
28262844

28272845
def accept(self, visitor: TypeVisitor[T]) -> T:

test-data/unit/check-custom-plugin.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,7 @@ class Cls(enum.Enum):
967967
attr = 'test'
968968

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

0 commit comments

Comments
 (0)