Skip to content

Commit a9a2901

Browse files
committed
Check runtime availability of private types not marked @type_check_only
1 parent e8147f2 commit a9a2901

File tree

2 files changed

+57
-8
lines changed

2 files changed

+57
-8
lines changed

mypy/stubtest.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import importlib
1515
import importlib.machinery
1616
import inspect
17+
import keyword
1718
import os
1819
import pkgutil
1920
import re
@@ -356,11 +357,7 @@ def verify_mypyfile(
356357
runtime_all_as_set = None
357358

358359
# Check things in the stub
359-
to_check = {
360-
m
361-
for m, o in stub.names.items()
362-
if not o.module_hidden and (not is_probably_private(m) or hasattr(runtime, m))
363-
}
360+
to_check = {m for m, o in stub.names.items() if not o.module_hidden}
364361

365362
def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
366363
"""Heuristics to determine whether a name originates from another module."""
@@ -418,6 +415,15 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
418415
# Don't recursively check exported modules, since that leads to infinite recursion
419416
continue
420417
assert stub_entry is not None
418+
if (
419+
is_probably_private(entry)
420+
and not hasattr(runtime, entry)
421+
and not isinstance(stub_entry, Missing)
422+
and not _is_decoratable(stub_entry)
423+
):
424+
# Skip private names that don't exist at runtime and which cannot
425+
# be marked with @type_check_only.
426+
continue
421427
try:
422428
runtime_entry = getattr(runtime, entry, MISSING)
423429
except Exception:
@@ -427,6 +433,23 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
427433
yield from verify(stub_entry, runtime_entry, object_path + [entry])
428434

429435

436+
def _is_decoratable(stub: nodes.SymbolNode) -> bool:
437+
if not isinstance(stub, nodes.TypeInfo):
438+
return False
439+
if stub.is_newtype:
440+
return False
441+
if stub.typeddict_type is not None:
442+
return all(
443+
name.isidentifier() and not keyword.iskeyword(name)
444+
for name in stub.typeddict_type.items.keys()
445+
)
446+
if stub.is_named_tuple:
447+
return all(
448+
name.isidentifier() and not keyword.iskeyword(name) for name in stub.names.keys()
449+
)
450+
return True
451+
452+
430453
def _verify_final(
431454
stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str]
432455
) -> Iterator[Error]:
@@ -526,7 +549,10 @@ def verify_typeinfo(
526549
return
527550

528551
if isinstance(runtime, Missing):
529-
yield Error(object_path, "is not present at runtime", stub, runtime, stub_desc=repr(stub))
552+
msg = "is not present at runtime"
553+
if is_probably_private(stub.name):
554+
msg += '. Maybe mark it as "@type_check_only"?'
555+
yield Error(object_path, msg, stub, runtime, stub_desc=repr(stub))
530556
return
531557
if not isinstance(runtime, type):
532558
# Yes, some runtime objects can be not types, no way to tell mypy about that.

mypy/test/teststubtest.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def __getitem__(self, typeargs: Any) -> object: ...
5252
5353
Final = 0
5454
Literal = 0
55+
NewType = 0
5556
TypedDict = 0
5657
5758
class TypeVar:
@@ -1122,7 +1123,7 @@ def test_type_alias(self) -> Iterator[Case]:
11221123
import collections.abc
11231124
import re
11241125
import typing
1125-
from typing import Callable, Dict, Generic, Iterable, List, Match, Tuple, TypeVar, Union
1126+
from typing import Callable, Dict, Generic, Iterable, List, Match, Tuple, TypeVar, Union, type_check_only
11261127
""",
11271128
runtime="""
11281129
import collections.abc
@@ -1193,6 +1194,7 @@ class Y: ...
11931194
yield Case(
11941195
stub="""
11951196
_T = TypeVar("_T")
1197+
@type_check_only
11961198
class _Spam(Generic[_T]):
11971199
def foo(self) -> None: ...
11981200
IntFood = _Spam[int]
@@ -1477,6 +1479,7 @@ def test_missing(self) -> Iterator[Case]:
14771479
yield Case(stub="x = 5", runtime="", error="x")
14781480
yield Case(stub="def f(): ...", runtime="", error="f")
14791481
yield Case(stub="class X: ...", runtime="", error="X")
1482+
yield Case(stub="class _X: ...", runtime="", error="_X")
14801483
yield Case(
14811484
stub="""
14821485
from typing import overload
@@ -1533,6 +1536,8 @@ def __delattr__(self, name: str, /) -> None: ...
15331536
runtime="class FakeDelattrClass: ...",
15341537
error="FakeDelattrClass.__delattr__",
15351538
)
1539+
yield Case(stub="from typing import NewType", runtime="", error=None)
1540+
yield Case(stub="_Int = NewType('_Int', int)", runtime="", error=None)
15361541

15371542
@collect_cases
15381543
def test_missing_no_runtime_all(self) -> Iterator[Case]:
@@ -2048,8 +2053,9 @@ def test_special_subtype(self) -> Iterator[Case]:
20482053
)
20492054
yield Case(
20502055
stub="""
2051-
from typing import TypedDict
2056+
from typing import TypedDict, type_check_only
20522057
2058+
@type_check_only
20532059
class _Options(TypedDict):
20542060
a: str
20552061
b: int
@@ -2472,6 +2478,23 @@ def func2() -> None: ...
24722478
runtime="def func2() -> None: ...",
24732479
error="func2",
24742480
)
2481+
# The same is true for private types
2482+
yield Case(
2483+
stub="""
2484+
@type_check_only
2485+
class _P1: ...
2486+
""",
2487+
runtime="",
2488+
error=None,
2489+
)
2490+
yield Case(
2491+
stub="""
2492+
@type_check_only
2493+
class _P2: ...
2494+
""",
2495+
runtime="class _P2: ...",
2496+
error="_P2",
2497+
)
24752498
# A type that exists at runtime is allowed to alias a type marked
24762499
# as '@type_check_only' in the stubs.
24772500
yield Case(

0 commit comments

Comments
 (0)