Skip to content
333 changes: 175 additions & 158 deletions mypy/checker.py

Large diffs are not rendered by default.

12 changes: 0 additions & 12 deletions mypy/meet.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,18 +657,6 @@ def _type_object_overlap(left: Type, right: Type) -> bool:
return False


def is_overlapping_erased_types(
left: Type, right: Type, *, ignore_promotions: bool = False
) -> bool:
"""The same as 'is_overlapping_erased_types', except the types are erased first."""
return is_overlapping_types(
erase_type(left),
erase_type(right),
ignore_promotions=ignore_promotions,
prohibit_none_typevar_overlap=True,
)


def are_typed_dicts_overlapping(
left: TypedDictType, right: TypedDictType, is_overlapping: Callable[[Type, Type], bool]
) -> bool:
Expand Down
4 changes: 3 additions & 1 deletion mypy/server/aststrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ def visit_class_def(self, node: ClassDef) -> None:
node.base_type_exprs.extend(node.removed_base_type_exprs)
node.removed_base_type_exprs = []
node.defs.body = [
s for s in node.defs.body if s not in to_delete # type: ignore[comparison-overlap]
s
for s in node.defs.body
if s not in to_delete # type: ignore[comparison-overlap, redundant-expr]
]
with self.enter_class(node.info):
super().visit_class_def(node)
Expand Down
2 changes: 1 addition & 1 deletion mypy/stubutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
)

# Modules that may fail when imported, or that may have side effects (fully qualified).
NOT_IMPORTABLE_MODULES = ()
NOT_IMPORTABLE_MODULES: tuple[str, ...] = ()

# Typing constructs to be replaced by their builtin equivalents.
TYPING_BUILTIN_REPLACEMENTS: Final = {
Expand Down
4 changes: 4 additions & 0 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,10 @@ def is_singleton_identity_type(typ: ProperType) -> bool:
)
if isinstance(typ, LiteralType):
return typ.is_enum_literal() or isinstance(typ.value, bool)
if isinstance(typ, TypeType) and isinstance(typ.item, Instance) and typ.item.type.is_final:
return True
if isinstance(typ, CallableType) and typ.is_type_obj() and typ.type_object().is_final:
return True
return False


Expand Down
188 changes: 188 additions & 0 deletions test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -2743,3 +2743,191 @@ reveal_type(Wrapper.Nested.FOO) # N: Revealed type is "Literal[__main__.Wrapper
reveal_type(Wrapper.Nested.FOO.value) # N: Revealed type is "builtins.ellipsis"
reveal_type(Wrapper.Nested.FOO._value_) # N: Revealed type is "builtins.ellipsis"
[builtins fixtures/enum.pyi]

[case testStrEnumEqualityReachability]
# flags: --strict-equality --warn-unreachable

from __future__ import annotations
import enum

# https://github.com/python/mypy/issues/17162

class MyEnum(str, enum.Enum):
A = 'a'

class MyEnum2(enum.StrEnum):
A = 'a'

def f1():
if MyEnum.A == 'a':
1 + ''

if MyEnum2.A == 'a':
1 + ''
[builtins fixtures/primitives.pyi]

[case testStrEnumEqualityNarrowing]
# flags: --strict-equality --warn-unreachable

from __future__ import annotations
import enum
from typing import Literal

# https://github.com/python/mypy/issues/18029

class Foo(enum.StrEnum):
FOO = 'a'

# Behaviour here is not yet ideal

def f1(a: Foo | Literal['foo']) -> Foo:
if a == 'foo':
reveal_type(a) # N: Revealed type is "__main__.Foo | Literal['foo']"
return Foo.FOO

reveal_type(a) # N: Revealed type is "__main__.Foo | Literal['foo']"
return a # E: Incompatible return value type (got "Foo | Literal['foo']", expected "Foo")
[builtins fixtures/primitives.pyi]

[case testStrEnumEqualityAlias]
# flags: --strict-equality --warn-unreachable
# https://github.com/python/mypy/issues/16830
from __future__ import annotations
from enum import Enum, auto

# https://github.com/python/mypy/issues/16830

class TrafficLight(Enum):
RED = auto()
AMBER = auto()
GREEN = auto()

YELLOW = AMBER # alias

# Behaviour here is not yet ideal

def demo1(inst: TrafficLight) -> None:
if inst is TrafficLight.AMBER:
reveal_type(inst) # N: Revealed type is "Literal[__main__.TrafficLight.AMBER]"
else:
reveal_type(inst) # N: Revealed type is "Literal[__main__.TrafficLight.RED] | Literal[__main__.TrafficLight.GREEN] | Literal[__main__.TrafficLight.YELLOW]"

if inst == TrafficLight.AMBER:
reveal_type(inst) # N: Revealed type is "Literal[__main__.TrafficLight.AMBER]"
else:
reveal_type(inst) # N: Revealed type is "Literal[__main__.TrafficLight.RED] | Literal[__main__.TrafficLight.GREEN] | Literal[__main__.TrafficLight.YELLOW]"

def demo2() -> None:
if TrafficLight.AMBER is TrafficLight.YELLOW: # E: Non-overlapping identity check (left operand type: "Literal[TrafficLight.AMBER]", right operand type: "Literal[TrafficLight.YELLOW]")
1 + '' # E: Unsupported operand types for + ("int" and "str")
else:
1 + '' # E: Unsupported operand types for + ("int" and "str")
[builtins fixtures/primitives.pyi]


[case testEnumEqualityNarrowing]
# flags: --strict-equality --warn-unreachable

from __future__ import annotations
from typing import cast
from enum import Enum, StrEnum

# https://github.com/python/mypy/issues/16830

class E(Enum):
A = "a"
B = "b"
C = "c"

class Custom:
def __eq__(self, other: object) -> bool: return True

class SE(StrEnum):
A = "a"
B = "b"
C = "c"

# Behaviour here is not yet ideal

def f1(x: int | str | E):
if x == E.A:
reveal_type(x) # N: Revealed type is "Literal[__main__.E.A]"
else:
reveal_type(x) # N: Revealed type is "builtins.int | builtins.str | Literal[__main__.E.B] | Literal[__main__.E.C]"

if x in cast(list[E], []):
reveal_type(x) # N: Revealed type is "__main__.E"
else:
reveal_type(x) # N: Revealed type is "builtins.int | builtins.str | __main__.E"

if x == str():
reveal_type(x) # N: Revealed type is "builtins.str"
else:
reveal_type(x) # N: Revealed type is "builtins.int | builtins.str | __main__.E"

def f2(x: int | Custom | E):
if x == E.A:
reveal_type(x) # N: Revealed type is "__main__.Custom | Literal[__main__.E.A]"
else:
reveal_type(x) # N: Revealed type is "builtins.int | __main__.Custom | Literal[__main__.E.B] | Literal[__main__.E.C]"

if x in cast(list[E], []):
reveal_type(x) # N: Revealed type is "__main__.Custom | __main__.E"
else:
reveal_type(x) # N: Revealed type is "builtins.int | __main__.Custom | __main__.E"

if x == str():
reveal_type(x) # N: Revealed type is "__main__.Custom"
else:
reveal_type(x) # N: Revealed type is "builtins.int | __main__.Custom | __main__.E"

def f3(x: int | str | SE):
if x == SE.A:
reveal_type(x) # N: Revealed type is "builtins.int | builtins.str | __main__.SE"
else:
reveal_type(x) # N: Revealed type is "builtins.int | builtins.str | __main__.SE"

if x in cast(list[SE], []):
reveal_type(x) # N: Revealed type is "builtins.int | builtins.str | __main__.SE"
else:
reveal_type(x) # N: Revealed type is "builtins.int | builtins.str | __main__.SE"

if x == str():
reveal_type(x) # N: Revealed type is "builtins.int | builtins.str | __main__.SE"
else:
reveal_type(x) # N: Revealed type is "builtins.int | builtins.str | __main__.SE"

def f4(x: int | Custom | SE):
if x == SE.A:
reveal_type(x) # N: Revealed type is "__main__.Custom | Literal[__main__.SE.A]"
else:
reveal_type(x) # N: Revealed type is "builtins.int | __main__.Custom | Literal[__main__.SE.B] | Literal[__main__.SE.C]"

if x in cast(list[SE], []):
reveal_type(x) # N: Revealed type is "__main__.Custom | __main__.SE"
else:
reveal_type(x) # N: Revealed type is "builtins.int | __main__.Custom | __main__.SE"

if x == str():
reveal_type(x) # N: Revealed type is "__main__.Custom | __main__.SE"
else:
reveal_type(x) # N: Revealed type is "builtins.int | __main__.Custom | __main__.SE"


def f5(x: str | Custom | SE):
if x == SE.A:
reveal_type(x) # N: Revealed type is "Literal[__main__.SE.A] | __main__.Custom"
else:
reveal_type(x) # N: Revealed type is "builtins.str | __main__.Custom"

if x in cast(list[SE], []):
reveal_type(x) # N: Revealed type is "__main__.SE | __main__.Custom"
else:
reveal_type(x) # N: Revealed type is "builtins.str | __main__.Custom | __main__.SE"

if x == str():
reveal_type(x) # N: Revealed type is "builtins.str | __main__.Custom"
else:
reveal_type(x) # N: Revealed type is "builtins.str | __main__.Custom | __main__.SE"

[builtins fixtures/primitives.pyi]
51 changes: 47 additions & 4 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -1995,19 +1995,20 @@ else:
[out]

[case testNarrowTypeAfterInListNonOverlapping]
# flags: --warn-unreachable
from typing import List, Optional

x: List[str]
y: Optional[int]

if y in x:
reveal_type(y) # N: Revealed type is "builtins.int | None"
reveal_type(y) # E: Statement is unreachable
else:
reveal_type(y) # N: Revealed type is "builtins.int | None"
[builtins fixtures/list.pyi]
[out]

[case testNarrowTypeAfterInListNested]
# flags: --warn-unreachable
from typing import List, Optional, Any

x: Optional[int]
Expand All @@ -2017,9 +2018,8 @@ nested_any: List[List[Any]]
if lst in nested_any:
reveal_type(lst) # N: Revealed type is "builtins.list[builtins.int]"
if x in nested_any:
reveal_type(x) # N: Revealed type is "builtins.int | None"
reveal_type(x) # E: Statement is unreachable
[builtins fixtures/list.pyi]
[out]

[case testNarrowTypeAfterInTuple]
from typing import Optional
Expand Down Expand Up @@ -2885,6 +2885,23 @@ if hasattr(mod, "y"):
def __getattr__(attr: str) -> str: ...
[builtins fixtures/module.pyi]

[case testMultipleHasAttr-xfail]
# flags: --warn-unreachable
from __future__ import annotations
from typing import Any

def len(obj: object) -> Any: ...

def f(x: type | str):
if (
hasattr(x, "__origin__")
and x.__origin__ is list
and hasattr(x, "__args__")
and len(x.__args__) == 1
):
reveal_type(x.__args__[0]) # N: Revealed type is "Any"
[builtins fixtures/module.pyi]

[case testTypeIsntLostAfterNarrowing]
from typing import Any

Expand Down Expand Up @@ -2912,3 +2929,29 @@ if isinstance(a, B):
c = a

[builtins fixtures/isinstance.pyi]

[case testIsInstanceTypeAny]
from typing import Any

def foo(x: object, t: type[Any]):
if isinstance(x, t):
reveal_type(x) # N: Revealed type is "Any"
[builtins fixtures/isinstance.pyi]

[case testIsInstanceUnionTuple]
# flags: --strict-equality --warn-unreachable
from typing import Any

def f1(x: object):
if isinstance(x, str | (int, dict)): # E: Argument 2 to "isinstance" has incompatible type "object"; expected "type | tuple[Any, ...]"
reveal_type(x) # N: Revealed type is "builtins.object"
if type(x) == str | (int, dict):
reveal_type(x) # N: Revealed type is "builtins.object"

def f2(x: Any):
if isinstance(x, str | (int, dict)): # E: Argument 2 to "isinstance" has incompatible type "object"; expected "type | tuple[Any, ...]"
reveal_type(x) # N: Revealed type is "Any"
if type(x) == str | (int, dict):
reveal_type(x) # N: Revealed type is "Any"

[builtins fixtures/primitives.pyi]
Loading