Skip to content

Commit f0d6113

Browse files
authored
[stubtest] transparent @type_check_only types (#20352)
This closes #20223 by mapping a `@type_check_only` type (of an instance) to its first non-`@type_check_only` superclass. A use case is for annotating specific `numpy.ufunc` instances through `@type_check_only` specialized `ufunc` subtypes, as explained in #20223.
1 parent 275d0ba commit f0d6113

File tree

2 files changed

+55
-2
lines changed

2 files changed

+55
-2
lines changed

mypy/stubtest.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from mypy import nodes
4646
from mypy.config_parser import parse_config_file
4747
from mypy.evalexpr import UNKNOWN, evaluate_expression
48+
from mypy.maptype import map_instance_to_supertype
4849
from mypy.options import Options
4950
from mypy.util import FancyFormatter, bytes_to_human_readable_repr, is_dunder, plural_s
5051

@@ -1852,10 +1853,39 @@ def describe_runtime_callable(signature: inspect.Signature, *, is_async: bool) -
18521853
return f'{"async " if is_async else ""}def {signature}'
18531854

18541855

1856+
class _TypeCheckOnlyBaseMapper(mypy.types.TypeTranslator):
1857+
"""Rewrites @type_check_only instances to the nearest runtime-visible base class."""
1858+
1859+
def visit_instance(self, t: mypy.types.Instance, /) -> mypy.types.Type:
1860+
instance = mypy.types.get_proper_type(super().visit_instance(t))
1861+
assert isinstance(instance, mypy.types.Instance)
1862+
1863+
if instance.type.is_type_check_only:
1864+
# find the nearest non-@type_check_only base class
1865+
for base_info in instance.type.mro[1:]:
1866+
if not base_info.is_type_check_only:
1867+
return map_instance_to_supertype(instance, base_info)
1868+
1869+
msg = f"all base classes of {instance.type.fullname!r} are @type_check_only"
1870+
assert False, msg
1871+
1872+
return instance
1873+
1874+
def visit_type_alias_type(self, t: mypy.types.TypeAliasType, /) -> mypy.types.Type:
1875+
return t
1876+
1877+
1878+
_TYPE_CHECK_ONLY_BASE_MAPPER = _TypeCheckOnlyBaseMapper()
1879+
1880+
1881+
def _relax_type_check_only_type(typ: mypy.types.ProperType) -> mypy.types.ProperType:
1882+
return mypy.types.get_proper_type(typ.accept(_TYPE_CHECK_ONLY_BASE_MAPPER))
1883+
1884+
18551885
def is_subtype_helper(left: mypy.types.Type, right: mypy.types.Type) -> bool:
18561886
"""Checks whether ``left`` is a subtype of ``right``."""
1857-
left = mypy.types.get_proper_type(left)
1858-
right = mypy.types.get_proper_type(right)
1887+
left = _relax_type_check_only_type(mypy.types.get_proper_type(left))
1888+
right = _relax_type_check_only_type(mypy.types.get_proper_type(right))
18591889
if (
18601890
isinstance(left, mypy.types.LiteralType)
18611891
and isinstance(left.value, int)

mypy/test/teststubtest.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,29 @@ class X:
311311
error="X.mistyped_var",
312312
)
313313

314+
@collect_cases
315+
def test_transparent_type_check_only_subclasses(self) -> Iterator[Case]:
316+
# See https://github.com/python/mypy/issues/20223
317+
yield Case(
318+
stub="""
319+
from typing import type_check_only
320+
321+
class UFunc: ...
322+
323+
@type_check_only
324+
class _BinaryUFunc(UFunc): ...
325+
326+
equal: _BinaryUFunc
327+
""",
328+
runtime="""
329+
class UFunc:
330+
pass
331+
332+
equal = UFunc()
333+
""",
334+
error=None,
335+
)
336+
314337
@collect_cases
315338
def test_coroutines(self) -> Iterator[Case]:
316339
yield Case(stub="def bar() -> int: ...", runtime="async def bar(): return 5", error="bar")

0 commit comments

Comments
 (0)