Skip to content

Commit bf82b76

Browse files
authored
Fix stubtest false positives with TypedDicts at runtime (#14984)
Fixes #14983
1 parent 7d2844c commit bf82b76

File tree

3 files changed

+32
-11
lines changed

3 files changed

+32
-11
lines changed

mypy/stubtest.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from functools import singledispatch
2727
from pathlib import Path
2828
from typing import Any, Generic, Iterator, TypeVar, Union
29-
from typing_extensions import get_origin
29+
from typing_extensions import get_origin, is_typeddict
3030

3131
import mypy.build
3232
import mypy.modulefinder
@@ -436,12 +436,12 @@ class SubClass(runtime): # type: ignore[misc]
436436

437437

438438
def _verify_metaclass(
439-
stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str]
439+
stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str], *, is_runtime_typeddict: bool
440440
) -> Iterator[Error]:
441441
# We exclude protocols, because of how complex their implementation is in different versions of
442-
# python. Enums are also hard, ignoring.
442+
# python. Enums are also hard, as are runtime TypedDicts; ignoring.
443443
# TODO: check that metaclasses are identical?
444-
if not stub.is_protocol and not stub.is_enum:
444+
if not stub.is_protocol and not stub.is_enum and not is_runtime_typeddict:
445445
runtime_metaclass = type(runtime)
446446
if runtime_metaclass is not type and stub.metaclass_type is None:
447447
# This means that runtime has a custom metaclass, but a stub does not.
@@ -485,22 +485,27 @@ def verify_typeinfo(
485485
return
486486

487487
yield from _verify_final(stub, runtime, object_path)
488-
yield from _verify_metaclass(stub, runtime, object_path)
488+
is_runtime_typeddict = stub.typeddict_type is not None and is_typeddict(runtime)
489+
yield from _verify_metaclass(
490+
stub, runtime, object_path, is_runtime_typeddict=is_runtime_typeddict
491+
)
489492

490493
# Check everything already defined on the stub class itself (i.e. not inherited)
491494
to_check = set(stub.names)
492495
# Check all public things on the runtime class
493496
to_check.update(
494497
m for m in vars(runtime) if not is_probably_private(m) and m not in IGNORABLE_CLASS_DUNDERS
495498
)
496-
# Special-case the __init__ method for Protocols
499+
# Special-case the __init__ method for Protocols and the __new__ method for TypedDicts
497500
#
498501
# TODO: On Python <3.11, __init__ methods on Protocol classes
499502
# are silently discarded and replaced.
500503
# However, this is not the case on Python 3.11+.
501504
# Ideally, we'd figure out a good way of validating Protocol __init__ methods on 3.11+.
502505
if stub.is_protocol:
503506
to_check.discard("__init__")
507+
if is_runtime_typeddict:
508+
to_check.discard("__new__")
504509

505510
for entry in sorted(to_check):
506511
mangled_entry = entry

mypy/test/teststubtest.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,24 +1572,36 @@ class _Options(TypedDict):
15721572
)
15731573

15741574
@collect_cases
1575-
def test_protocol(self) -> Iterator[Case]:
1575+
def test_runtime_typing_objects(self) -> Iterator[Case]:
1576+
yield Case(
1577+
stub="from typing_extensions import Protocol, TypedDict",
1578+
runtime="from typing_extensions import Protocol, TypedDict",
1579+
error=None,
1580+
)
15761581
yield Case(
15771582
stub="""
1578-
from typing_extensions import Protocol
1579-
15801583
class X(Protocol):
15811584
bar: int
15821585
def foo(self, x: int, y: bytes = ...) -> str: ...
15831586
""",
15841587
runtime="""
1585-
from typing_extensions import Protocol
1586-
15871588
class X(Protocol):
15881589
bar: int
15891590
def foo(self, x: int, y: bytes = ...) -> str: ...
15901591
""",
15911592
error=None,
15921593
)
1594+
yield Case(
1595+
stub="""
1596+
class Y(TypedDict):
1597+
a: int
1598+
""",
1599+
runtime="""
1600+
class Y(TypedDict):
1601+
a: int
1602+
""",
1603+
error=None,
1604+
)
15931605

15941606
@collect_cases
15951607
def test_type_var(self) -> Iterator[Case]:

test-data/unit/lib-stub/typing_extensions.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ class _TypedDict(Mapping[str, object]):
5656
if sys.version_info < (3, 0):
5757
def has_key(self, k: str) -> bool: ...
5858
def __delitem__(self, k: NoReturn) -> None: ...
59+
# Stubtest's tests need the following items:
60+
__required_keys__: frozenset[str]
61+
__optional_keys__: frozenset[str]
62+
__total__: bool
5963

6064
def TypedDict(typename: str, fields: Dict[str, Type[_T]], *, total: Any = ...) -> Type[dict]: ...
6165

0 commit comments

Comments
 (0)