Skip to content

Commit 6f4dbb8

Browse files
committed
Merge remote-tracking branch 'upstream/master' into bugfix/gh-19576-enum-comparison-overlap
2 parents ab3ed33 + db67fac commit 6f4dbb8

File tree

9 files changed

+96
-36
lines changed

9 files changed

+96
-36
lines changed

.github/workflows/test.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ jobs:
3737
toxenv: py
3838
tox_extra_args: "-n 4"
3939
test_mypyc: true
40-
- name: Test suite with py39-windows-64
41-
python: '3.9'
42-
os: windows-latest
43-
toxenv: py39
44-
tox_extra_args: "-n 4"
4540
- name: Test suite with py310-ubuntu
4641
python: '3.10'
4742
os: ubuntu-24.04-arm
@@ -64,6 +59,11 @@ jobs:
6459
toxenv: py
6560
tox_extra_args: "-n 4"
6661
test_mypyc: true
62+
- name: Test suite with py313-windows-64
63+
python: '3.13'
64+
os: windows-latest
65+
toxenv: py
66+
tox_extra_args: "-n 4"
6767

6868
- name: Test suite with py314-dev-ubuntu
6969
python: '3.14-dev'

mypy/checker.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -832,8 +832,10 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None:
832832
# At this point we should have set the impl already, and all remaining
833833
# items are decorators
834834

835-
if self.msg.errors.file in self.msg.errors.ignored_files or (
836-
self.is_typeshed_stub and self.options.test_env
835+
if (
836+
self.options.ignore_errors
837+
or self.msg.errors.file in self.msg.errors.ignored_files
838+
or (self.is_typeshed_stub and self.options.test_env)
837839
):
838840
# This is a little hacky, however, the quadratic check here is really expensive, this
839841
# method has no side effects, so we should skip it if we aren't going to report
@@ -1444,7 +1446,19 @@ def check_func_def(
14441446
# TODO: Find a way of working around this limitation
14451447
if _is_empty_generator_function(item) or len(expanded) >= 2:
14461448
self.binder.suppress_unreachable_warnings()
1447-
self.accept(item.body)
1449+
# When checking a third-party library, we can skip function body,
1450+
# if during semantic analysis we found that there are no attributes
1451+
# defined via self here.
1452+
if (
1453+
not (
1454+
self.options.ignore_errors
1455+
or self.msg.errors.file in self.msg.errors.ignored_files
1456+
)
1457+
or self.options.preserve_asts
1458+
or not isinstance(defn, FuncDef)
1459+
or defn.has_self_attr_def
1460+
):
1461+
self.accept(item.body)
14481462
unreachable = self.binder.is_unreachable()
14491463
if new_frame is not None:
14501464
self.binder.pop_frame(True, 0)
@@ -2127,6 +2141,9 @@ def check_method_override(
21272141
21282142
Return a list of base classes which contain an attribute with the method name.
21292143
"""
2144+
if self.options.ignore_errors or self.msg.errors.file in self.msg.errors.ignored_files:
2145+
# Method override checks may be expensive, so skip them in third-party libraries.
2146+
return None
21302147
# Check against definitions in base classes.
21312148
check_override_compatibility = (
21322149
defn.name not in ("__init__", "__new__", "__init_subclass__", "__post_init__")

mypy/checkexpr.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5350,9 +5350,9 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
53505350
# an error, but returns the TypedDict type that matches the literal it found
53515351
# that would cause a second error when that TypedDict type is returned upstream
53525352
# to avoid the second error, we always return TypedDict type that was requested
5353-
typeddict_contexts = self.find_typeddict_context(self.type_context[-1], e)
5353+
typeddict_contexts, exhaustive = self.find_typeddict_context(self.type_context[-1], e)
53545354
if typeddict_contexts:
5355-
if len(typeddict_contexts) == 1:
5355+
if len(typeddict_contexts) == 1 and exhaustive:
53565356
return self.check_typeddict_literal_in_context(e, typeddict_contexts[0])
53575357
# Multiple items union, check if at least one of them matches cleanly.
53585358
for typeddict_context in typeddict_contexts:
@@ -5363,7 +5363,8 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
53635363
self.chk.store_types(tmap)
53645364
return ret_type
53655365
# No item matched without an error, so we can't unambiguously choose the item.
5366-
self.msg.typeddict_context_ambiguous(typeddict_contexts, e)
5366+
if exhaustive:
5367+
self.msg.typeddict_context_ambiguous(typeddict_contexts, e)
53675368

53685369
# fast path attempt
53695370
dt = self.fast_dict_type(e)
@@ -5425,22 +5426,29 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
54255426

54265427
def find_typeddict_context(
54275428
self, context: Type | None, dict_expr: DictExpr
5428-
) -> list[TypedDictType]:
5429+
) -> tuple[list[TypedDictType], bool]:
5430+
"""Extract `TypedDict` members of the enclosing context.
5431+
5432+
Returns:
5433+
a 2-tuple, (found_candidates, is_exhaustive)
5434+
"""
54295435
context = get_proper_type(context)
54305436
if isinstance(context, TypedDictType):
5431-
return [context]
5437+
return [context], True
54325438
elif isinstance(context, UnionType):
54335439
items = []
5440+
exhaustive = True
54345441
for item in context.items:
5435-
item_contexts = self.find_typeddict_context(item, dict_expr)
5442+
item_contexts, item_exhaustive = self.find_typeddict_context(item, dict_expr)
54365443
for item_context in item_contexts:
54375444
if self.match_typeddict_call_with_dict(
54385445
item_context, dict_expr.items, dict_expr
54395446
):
54405447
items.append(item_context)
5441-
return items
5448+
exhaustive = exhaustive and item_exhaustive
5449+
return items, exhaustive
54425450
# No TypedDict type in context.
5443-
return []
5451+
return [], False
54445452

54455453
def visit_lambda_expr(self, e: LambdaExpr) -> Type:
54465454
"""Type check lambda expression."""

mypy/fastparse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2234,7 +2234,7 @@ def visit_index_expr(self, e: IndexExpr) -> None:
22342234
pass
22352235

22362236
def visit_member_expr(self, e: MemberExpr) -> None:
2237-
if self.lvalue:
2237+
if self.lvalue and isinstance(e.expr, NameExpr):
22382238
self.found = True
22392239

22402240

mypy/nodes.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,7 @@ class FuncDef(FuncItem, SymbolNode, Statement):
818818
"original_def",
819819
"is_trivial_body",
820820
"is_trivial_self",
821+
"has_self_attr_def",
821822
"is_mypy_only",
822823
# Present only when a function is decorated with @typing.dataclass_transform or similar
823824
"dataclass_transform_spec",
@@ -856,6 +857,8 @@ def __init__(
856857
# the majority). In cases where self is not annotated and there are no Self
857858
# in the signature we can simply drop the first argument.
858859
self.is_trivial_self = False
860+
# Keep track of functions where self attributes are defined.
861+
self.has_self_attr_def = False
859862
# This is needed because for positional-only arguments the name is set to None,
860863
# but we sometimes still want to show it in error messages.
861864
if arguments:
@@ -915,7 +918,7 @@ def deserialize(cls, data: JsonDict) -> FuncDef:
915918
# NOTE: ret.info is set in the fixup phase.
916919
ret.arg_names = data["arg_names"]
917920
ret.original_first_arg = data.get("original_first_arg")
918-
ret.arg_kinds = [ArgKind(x) for x in data["arg_kinds"]]
921+
ret.arg_kinds = [ARG_KINDS[x] for x in data["arg_kinds"]]
919922
ret.abstract_status = data["abstract_status"]
920923
ret.dataclass_transform_spec = (
921924
DataclassTransformSpec.deserialize(data["dataclass_transform_spec"])
@@ -2013,6 +2016,8 @@ def is_star(self) -> bool:
20132016
ARG_STAR2: Final = ArgKind.ARG_STAR2
20142017
ARG_NAMED_OPT: Final = ArgKind.ARG_NAMED_OPT
20152018

2019+
ARG_KINDS: Final = (ARG_POS, ARG_OPT, ARG_STAR, ARG_NAMED, ARG_STAR2, ARG_NAMED_OPT)
2020+
20162021

20172022
class CallExpr(Expression):
20182023
"""Call expression.
@@ -3488,6 +3493,8 @@ def update_tuple_type(self, typ: mypy.types.TupleType) -> None:
34883493
self.special_alias = alias
34893494
else:
34903495
self.special_alias.target = alias.target
3496+
# Invalidate recursive status cache in case it was previously set.
3497+
self.special_alias._is_recursive = None
34913498

34923499
def update_typeddict_type(self, typ: mypy.types.TypedDictType) -> None:
34933500
"""Update typeddict_type and special_alias as needed."""
@@ -3497,6 +3504,8 @@ def update_typeddict_type(self, typ: mypy.types.TypedDictType) -> None:
34973504
self.special_alias = alias
34983505
else:
34993506
self.special_alias.target = alias.target
3507+
# Invalidate recursive status cache in case it was previously set.
3508+
self.special_alias._is_recursive = None
35003509

35013510
def __str__(self) -> str:
35023511
"""Return a string representation of the type.

mypy/semanal.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4570,6 +4570,9 @@ def analyze_member_lvalue(
45704570
lval.node = v
45714571
# TODO: should we also set lval.kind = MDEF?
45724572
self.type.names[lval.name] = SymbolTableNode(MDEF, v, implicit=True)
4573+
for func in self.scope.functions:
4574+
if isinstance(func, FuncDef):
4575+
func.has_self_attr_def = True
45734576
self.check_lvalue_validity(lval.node, lval)
45744577

45754578
def is_self_member_ref(self, memberexpr: MemberExpr) -> bool:
@@ -5633,6 +5636,8 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None:
56335636
existing.node.target = res
56345637
existing.node.alias_tvars = alias_tvars
56355638
updated = True
5639+
# Invalidate recursive status cache in case it was previously set.
5640+
existing.node._is_recursive = None
56365641
else:
56375642
# Otherwise just replace existing placeholder with type alias.
56385643
existing.node = alias_node

mypy/typeops.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
flatten_nested_unions,
6464
get_proper_type,
6565
get_proper_types,
66+
remove_dups,
6667
)
6768
from mypy.typetraverser import TypeTraverserVisitor
6869
from mypy.typevars import fill_typevars
@@ -995,7 +996,7 @@ def is_singleton_type(typ: Type) -> bool:
995996
return typ.is_singleton_type()
996997

997998

998-
def try_expanding_sum_type_to_union(typ: Type, target_fullname: str) -> ProperType:
999+
def try_expanding_sum_type_to_union(typ: Type, target_fullname: str) -> Type:
9991000
"""Attempts to recursively expand any enum Instances with the given target_fullname
10001001
into a Union of all of its component LiteralTypes.
10011002
@@ -1017,21 +1018,22 @@ class Status(Enum):
10171018
typ = get_proper_type(typ)
10181019

10191020
if isinstance(typ, UnionType):
1021+
# Non-empty enums cannot subclass each other so simply removing duplicates is enough.
10201022
items = [
1021-
try_expanding_sum_type_to_union(item, target_fullname) for item in typ.relevant_items()
1023+
try_expanding_sum_type_to_union(item, target_fullname)
1024+
for item in remove_dups(flatten_nested_unions(typ.relevant_items()))
10221025
]
1023-
return make_simplified_union(items, contract_literals=False)
1026+
return UnionType.make_union(items)
10241027

10251028
if isinstance(typ, Instance) and typ.type.fullname == target_fullname:
10261029
if typ.type.fullname == "builtins.bool":
1027-
items = [LiteralType(True, typ), LiteralType(False, typ)]
1028-
return make_simplified_union(items, contract_literals=False)
1030+
return UnionType([LiteralType(True, typ), LiteralType(False, typ)])
10291031

10301032
if typ.type.is_enum:
10311033
items = [LiteralType(name, typ) for name in typ.type.enum_members]
10321034
if not items:
10331035
return typ
1034-
return make_simplified_union(items, contract_literals=False)
1036+
return UnionType.make_union(items)
10351037

10361038
return typ
10371039

mypy/types.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import mypy.nodes
2323
from mypy.bogus_type import Bogus
24-
from mypy.nodes import ARG_POS, ARG_STAR, ARG_STAR2, INVARIANT, ArgKind, FakeInfo, SymbolNode
24+
from mypy.nodes import ARG_KINDS, ARG_POS, ARG_STAR, ARG_STAR2, INVARIANT, ArgKind, SymbolNode
2525
from mypy.options import Options
2626
from mypy.state import state
2727
from mypy.util import IdMapper
@@ -538,6 +538,10 @@ def __repr__(self) -> str:
538538
return self.raw_id.__repr__()
539539

540540
def __eq__(self, other: object) -> bool:
541+
# Although this call is not expensive (like UnionType or TypedDictType),
542+
# most of the time we get the same object here, so add a fast path.
543+
if self is other:
544+
return True
541545
return (
542546
isinstance(other, TypeVarId)
543547
and self.raw_id == other.raw_id
@@ -1780,7 +1784,9 @@ def deserialize(cls, data: JsonDict) -> Parameters:
17801784
assert data[".class"] == "Parameters"
17811785
return Parameters(
17821786
[deserialize_type(t) for t in data["arg_types"]],
1783-
[ArgKind(x) for x in data["arg_kinds"]],
1787+
# This is a micro-optimization until mypyc gets dedicated enum support. Otherwise,
1788+
# we would spend ~20% of types deserialization time in Enum.__call__().
1789+
[ARG_KINDS[x] for x in data["arg_kinds"]],
17841790
data["arg_names"],
17851791
variables=[cast(TypeVarLikeType, deserialize_type(v)) for v in data["variables"]],
17861792
imprecise_arg_kinds=data["imprecise_arg_kinds"],
@@ -1797,7 +1803,7 @@ def __hash__(self) -> int:
17971803
)
17981804

17991805
def __eq__(self, other: object) -> bool:
1800-
if isinstance(other, (Parameters, CallableType)):
1806+
if isinstance(other, Parameters):
18011807
return (
18021808
self.arg_types == other.arg_types
18031809
and self.arg_names == other.arg_names
@@ -2210,15 +2216,9 @@ def with_normalized_var_args(self) -> Self:
22102216
)
22112217

22122218
def __hash__(self) -> int:
2213-
# self.is_type_obj() will fail if self.fallback.type is a FakeInfo
2214-
if isinstance(self.fallback.type, FakeInfo):
2215-
is_type_obj = 2
2216-
else:
2217-
is_type_obj = self.is_type_obj()
22182219
return hash(
22192220
(
22202221
self.ret_type,
2221-
is_type_obj,
22222222
self.is_ellipsis_args,
22232223
self.name,
22242224
tuple(self.arg_types),
@@ -2236,7 +2236,6 @@ def __eq__(self, other: object) -> bool:
22362236
and self.arg_names == other.arg_names
22372237
and self.arg_kinds == other.arg_kinds
22382238
and self.name == other.name
2239-
and self.is_type_obj() == other.is_type_obj()
22402239
and self.is_ellipsis_args == other.is_ellipsis_args
22412240
and self.type_guard == other.type_guard
22422241
and self.type_is == other.type_is
@@ -2271,10 +2270,10 @@ def serialize(self) -> JsonDict:
22712270
@classmethod
22722271
def deserialize(cls, data: JsonDict) -> CallableType:
22732272
assert data[".class"] == "CallableType"
2274-
# TODO: Set definition to the containing SymbolNode?
2273+
# The .definition link is set in fixup.py.
22752274
return CallableType(
22762275
[deserialize_type(t) for t in data["arg_types"]],
2277-
[ArgKind(x) for x in data["arg_kinds"]],
2276+
[ARG_KINDS[x] for x in data["arg_kinds"]],
22782277
data["arg_names"],
22792278
deserialize_type(data["ret_type"]),
22802279
Instance.deserialize(data["fallback"]),
@@ -2931,6 +2930,8 @@ def __hash__(self) -> int:
29312930
def __eq__(self, other: object) -> bool:
29322931
if not isinstance(other, UnionType):
29332932
return NotImplemented
2933+
if self is other:
2934+
return True
29342935
return frozenset(self.items) == frozenset(other.items)
29352936

29362937
@overload

test-data/unit/check-typeddict.test

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4289,3 +4289,21 @@ inputs: Sequence[Component] = [{
42894289
}]
42904290
[builtins fixtures/dict.pyi]
42914291
[typing fixtures/typing-typeddict.pyi]
4292+
4293+
[case testTypedDictAssignableToWiderContext]
4294+
from typing import TypedDict, Union
4295+
4296+
class TD(TypedDict):
4297+
x: int
4298+
4299+
x: Union[TD, dict[str, str]] = {"x": "foo"}
4300+
y: Union[TD, dict[str, int]] = {"x": "foo"} # E: Dict entry 0 has incompatible type "str": "str"; expected "str": "int"
4301+
4302+
def ok(d: Union[TD, dict[str, str]]) -> None: ...
4303+
ok({"x": "foo"})
4304+
4305+
def bad(d: Union[TD, dict[str, int]]) -> None: ...
4306+
bad({"x": "foo"}) # E: Dict entry 0 has incompatible type "str": "str"; expected "str": "int"
4307+
4308+
[builtins fixtures/dict.pyi]
4309+
[typing fixtures/typing-typeddict.pyi]

0 commit comments

Comments
 (0)