Skip to content

Commit

Permalink
Fix stubtest false positives with TypedDicts at runtime (#14984)
Browse files Browse the repository at this point in the history
Fixes #14983
  • Loading branch information
AlexWaygood committed Apr 4, 2023
1 parent 7d2844c commit bf82b76
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 11 deletions.
17 changes: 11 additions & 6 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from functools import singledispatch
from pathlib import Path
from typing import Any, Generic, Iterator, TypeVar, Union
from typing_extensions import get_origin
from typing_extensions import get_origin, is_typeddict

import mypy.build
import mypy.modulefinder
Expand Down Expand Up @@ -436,12 +436,12 @@ class SubClass(runtime): # type: ignore[misc]


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

yield from _verify_final(stub, runtime, object_path)
yield from _verify_metaclass(stub, runtime, object_path)
is_runtime_typeddict = stub.typeddict_type is not None and is_typeddict(runtime)
yield from _verify_metaclass(
stub, runtime, object_path, is_runtime_typeddict=is_runtime_typeddict
)

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

for entry in sorted(to_check):
mangled_entry = entry
Expand Down
22 changes: 17 additions & 5 deletions mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1572,24 +1572,36 @@ class _Options(TypedDict):
)

@collect_cases
def test_protocol(self) -> Iterator[Case]:
def test_runtime_typing_objects(self) -> Iterator[Case]:
yield Case(
stub="from typing_extensions import Protocol, TypedDict",
runtime="from typing_extensions import Protocol, TypedDict",
error=None,
)
yield Case(
stub="""
from typing_extensions import Protocol
class X(Protocol):
bar: int
def foo(self, x: int, y: bytes = ...) -> str: ...
""",
runtime="""
from typing_extensions import Protocol
class X(Protocol):
bar: int
def foo(self, x: int, y: bytes = ...) -> str: ...
""",
error=None,
)
yield Case(
stub="""
class Y(TypedDict):
a: int
""",
runtime="""
class Y(TypedDict):
a: int
""",
error=None,
)

@collect_cases
def test_type_var(self) -> Iterator[Case]:
Expand Down
4 changes: 4 additions & 0 deletions test-data/unit/lib-stub/typing_extensions.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ class _TypedDict(Mapping[str, object]):
if sys.version_info < (3, 0):
def has_key(self, k: str) -> bool: ...
def __delitem__(self, k: NoReturn) -> None: ...
# Stubtest's tests need the following items:
__required_keys__: frozenset[str]
__optional_keys__: frozenset[str]
__total__: bool

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

Expand Down

0 comments on commit bf82b76

Please sign in to comment.