Skip to content

Commit 0a05e61

Browse files
authored
stubtest: various improvements (#8502)
* stubtest: refine verify_signature If runtime takes a keyword-only argument, we now unconditionally check that the stub also does. This leads to good outcomes on typeshed. Also further clarify the comments. * stubtest: ignore "this", another annoying module * stubtest: support using regexes in whitelist * stubtest: add --ignore-unused-whitelist * stubtest: handle name mangling
1 parent f2ead85 commit 0a05e61

File tree

2 files changed

+109
-21
lines changed

2 files changed

+109
-21
lines changed

mypy/stubtest.py

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import enum
1010
import importlib
1111
import inspect
12+
import re
1213
import sys
1314
import types
1415
import warnings
@@ -240,9 +241,12 @@ def verify_typeinfo(
240241
to_check.update(m for m in cast(Any, vars)(runtime) if not m.startswith("_"))
241242

242243
for entry in sorted(to_check):
244+
mangled_entry = entry
245+
if entry.startswith("__") and not entry.endswith("__"):
246+
mangled_entry = "_{}{}".format(stub.name, entry)
243247
yield from verify(
244248
next((t.names[entry].node for t in stub.mro if entry in t.names), MISSING),
245-
getattr(runtime, entry, MISSING),
249+
getattr(runtime, mangled_entry, MISSING),
246250
object_path + [entry],
247251
)
248252

@@ -266,7 +270,13 @@ def _verify_static_class_methods(
266270
# Look the object up statically, to avoid binding by the descriptor protocol
267271
static_runtime = importlib.import_module(object_path[0])
268272
for entry in object_path[1:]:
269-
static_runtime = inspect.getattr_static(static_runtime, entry)
273+
try:
274+
static_runtime = inspect.getattr_static(static_runtime, entry)
275+
except AttributeError:
276+
# This can happen with mangled names, ignore for now.
277+
# TODO: pass more information about ancestors of nodes/objects to verify, so we don't
278+
# have to do this hacky lookup. Would be useful in a couple other places too.
279+
return
270280

271281
if isinstance(static_runtime, classmethod) and not stub.is_class:
272282
yield "runtime is a classmethod but stub is not"
@@ -582,21 +592,24 @@ def _verify_signature(
582592

583593
# Check unmatched keyword-only args
584594
if runtime.varkw is None or not set(runtime.kwonly).issubset(set(stub.kwonly)):
595+
# There are cases where the stub exhaustively lists out the extra parameters the function
596+
# would take through *kwargs. Hence, a) we only check if the runtime actually takes those
597+
# parameters when the above condition holds and b) below, we don't enforce that the stub
598+
# takes *kwargs, since runtime logic may prevent additional arguments from actually being
599+
# accepted.
585600
for arg in sorted(set(stub.kwonly) - set(runtime.kwonly)):
586601
yield 'runtime does not have argument "{}"'.format(arg)
587-
if stub.varkw is None or not set(stub.kwonly).issubset(set(runtime.kwonly)):
588-
for arg in sorted(set(runtime.kwonly) - set(stub.kwonly)):
589-
if arg in set(stub_arg.variable.name for stub_arg in stub.pos):
590-
# Don't report this if we've reported it before
591-
if len(stub.pos) > len(runtime.pos) and runtime.varpos is not None:
592-
yield 'stub argument "{}" is not keyword-only'.format(arg)
593-
else:
594-
yield 'stub does not have argument "{}"'.format(arg)
602+
for arg in sorted(set(runtime.kwonly) - set(stub.kwonly)):
603+
if arg in set(stub_arg.variable.name for stub_arg in stub.pos):
604+
# Don't report this if we've reported it before
605+
if len(stub.pos) > len(runtime.pos) and runtime.varpos is not None:
606+
yield 'stub argument "{}" is not keyword-only'.format(arg)
607+
else:
608+
yield 'stub does not have argument "{}"'.format(arg)
595609

596610
# Checks involving **kwargs
597611
if stub.varkw is None and runtime.varkw is not None:
598-
# There are cases where the stub exhaustively lists out the extra parameters the function
599-
# would take through **kwargs, so we don't enforce that the stub takes **kwargs.
612+
# As mentioned above, don't enforce that the stub takes **kwargs.
600613
# Also check against positional parameters, to avoid a nitpicky message when an argument
601614
# isn't marked as keyword-only
602615
stub_pos_names = set(stub_arg.variable.name for stub_arg in stub.pos)
@@ -1016,6 +1029,7 @@ def test_stubs(args: argparse.Namespace) -> int:
10161029
for whitelist_file in args.whitelist
10171030
for entry in get_whitelist_entries(whitelist_file)
10181031
}
1032+
whitelist_regexes = {entry: re.compile(entry) for entry in whitelist}
10191033

10201034
# If we need to generate a whitelist, we store Error.object_desc for each error here.
10211035
generated_whitelist = set()
@@ -1024,7 +1038,8 @@ def test_stubs(args: argparse.Namespace) -> int:
10241038
if args.check_typeshed:
10251039
assert not args.modules, "Cannot pass both --check-typeshed and a list of modules"
10261040
modules = get_typeshed_stdlib_modules(args.custom_typeshed_dir)
1027-
modules.remove("antigravity") # it's super annoying
1041+
annoying_modules = {"antigravity", "this"}
1042+
modules = [m for m in modules if m not in annoying_modules]
10281043

10291044
assert modules, "No modules to check"
10301045

@@ -1048,6 +1063,14 @@ def test_stubs(args: argparse.Namespace) -> int:
10481063
if error.object_desc in whitelist:
10491064
whitelist[error.object_desc] = True
10501065
continue
1066+
is_whitelisted = False
1067+
for w in whitelist:
1068+
if whitelist_regexes[w].fullmatch(error.object_desc):
1069+
whitelist[w] = True
1070+
is_whitelisted = True
1071+
break
1072+
if is_whitelisted:
1073+
continue
10511074

10521075
# We have errors, so change exit code, and output whatever necessary
10531076
exit_code = 1
@@ -1057,10 +1080,13 @@ def test_stubs(args: argparse.Namespace) -> int:
10571080
print(error.get_description(concise=args.concise))
10581081

10591082
# Print unused whitelist entries
1060-
for w in whitelist:
1061-
if not whitelist[w]:
1062-
exit_code = 1
1063-
print("note: unused whitelist entry {}".format(w))
1083+
if not args.ignore_unused_whitelist:
1084+
for w in whitelist:
1085+
# Don't consider an entry unused if it regex-matches the empty string
1086+
# This allows us to whitelist errors that don't manifest at all on some systems
1087+
if not whitelist[w] and not whitelist_regexes[w].fullmatch(""):
1088+
exit_code = 1
1089+
print("note: unused whitelist entry {}".format(w))
10641090

10651091
# Print the generated whitelist
10661092
if args.generate_whitelist:
@@ -1100,14 +1126,20 @@ def parse_options(args: List[str]) -> argparse.Namespace:
11001126
default=[],
11011127
help=(
11021128
"Use file as a whitelist. Can be passed multiple times to combine multiple "
1103-
"whitelists. Whitelist can be created with --generate-whitelist"
1129+
"whitelists. Whitelists can be created with --generate-whitelist"
11041130
),
11051131
)
11061132
parser.add_argument(
11071133
"--generate-whitelist",
11081134
action="store_true",
11091135
help="Print a whitelist (to stdout) to be used with --whitelist",
11101136
)
1137+
parser.add_argument(
1138+
"--ignore-unused-whitelist",
1139+
action="store_true",
1140+
help="Ignore unused whitelist entries",
1141+
)
1142+
11111143
return parser.parse_args(args)
11121144

11131145

mypy/test/teststubtest.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,11 @@ def test_varargs_varkwargs(self) -> Iterator[Case]:
369369
runtime="def k5(a, *, b, c, **kwargs): pass",
370370
error="k5",
371371
)
372+
yield Case(
373+
stub="def k6(a, *, b, **kwargs) -> None: ...",
374+
runtime="def k6(a, *, b, c, **kwargs): pass",
375+
error="k6",
376+
)
372377

373378
@collect_cases
374379
def test_overload(self) -> Iterator[Case]:
@@ -567,6 +572,22 @@ def h(x: str): ...
567572
yield Case(stub="", runtime="__all__ += ['y']\ny = 5", error="y")
568573
yield Case(stub="", runtime="__all__ += ['g']\ndef g(): pass", error="g")
569574

575+
@collect_cases
576+
def test_name_mangling(self) -> Iterator[Case]:
577+
yield Case(
578+
stub="""
579+
class X:
580+
def __mangle_good(self, text: str) -> None: ...
581+
def __mangle_bad(self, number: int) -> None: ...
582+
""",
583+
runtime="""
584+
class X:
585+
def __mangle_good(self, text): pass
586+
def __mangle_bad(self, text): pass
587+
""",
588+
error="X.__mangle_bad"
589+
)
590+
570591

571592
def remove_color_code(s: str) -> str:
572593
return re.sub("\\x1b.*?m", "", s) # this works!
@@ -610,20 +631,55 @@ def test_ignore_flags(self) -> None:
610631

611632
def test_whitelist(self) -> None:
612633
# Can't use this as a context because Windows
613-
whitelist = tempfile.NamedTemporaryFile(mode="w", delete=False)
634+
whitelist = tempfile.NamedTemporaryFile(mode="w+", delete=False)
614635
try:
615636
with whitelist:
616-
whitelist.write("{}.bad\n# a comment".format(TEST_MODULE_NAME))
637+
whitelist.write("{}.bad # comment\n# comment".format(TEST_MODULE_NAME))
617638

618639
output = run_stubtest(
619640
stub="def bad(number: int, text: str) -> None: ...",
620-
runtime="def bad(num, text) -> None: pass",
641+
runtime="def bad(asdf, text): pass",
621642
options=["--whitelist", whitelist.name],
622643
)
623644
assert not output
624645

646+
# test unused entry detection
625647
output = run_stubtest(stub="", runtime="", options=["--whitelist", whitelist.name])
626648
assert output == "note: unused whitelist entry {}.bad\n".format(TEST_MODULE_NAME)
649+
650+
output = run_stubtest(
651+
stub="",
652+
runtime="",
653+
options=["--whitelist", whitelist.name, "--ignore-unused-whitelist"],
654+
)
655+
assert not output
656+
657+
# test regex matching
658+
with open(whitelist.name, mode="w+") as f:
659+
f.write("{}.b.*\n".format(TEST_MODULE_NAME))
660+
f.write("(unused_missing)?\n")
661+
f.write("unused.*\n")
662+
663+
output = run_stubtest(
664+
stub=textwrap.dedent(
665+
"""
666+
def good() -> None: ...
667+
def bad(number: int) -> None: ...
668+
def also_bad(number: int) -> None: ...
669+
""".lstrip("\n")
670+
),
671+
runtime=textwrap.dedent(
672+
"""
673+
def good(): pass
674+
def bad(asdf): pass
675+
def also_bad(asdf): pass
676+
""".lstrip("\n")
677+
),
678+
options=["--whitelist", whitelist.name, "--generate-whitelist"],
679+
)
680+
assert output == "note: unused whitelist entry unused.*\n{}.also_bad\n".format(
681+
TEST_MODULE_NAME
682+
)
627683
finally:
628684
os.unlink(whitelist.name)
629685

0 commit comments

Comments
 (0)