| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882 |
- """Tests for stubs.
- Verify that various things in stubs are consistent with how things behave at runtime.
- """
- from __future__ import annotations
- import argparse
- import collections.abc
- import copy
- import enum
- import importlib
- import inspect
- import os
- import pkgutil
- import re
- import symtable
- import sys
- import traceback
- import types
- import typing
- import typing_extensions
- import warnings
- from contextlib import redirect_stderr, redirect_stdout
- from functools import singledispatch
- from pathlib import Path
- from typing import Any, Generic, Iterator, TypeVar, Union
- from typing_extensions import get_origin, is_typeddict
- import mypy.build
- import mypy.modulefinder
- import mypy.nodes
- import mypy.state
- import mypy.types
- import mypy.version
- from mypy import nodes
- from mypy.config_parser import parse_config_file
- from mypy.evalexpr import UNKNOWN, evaluate_expression
- from mypy.options import Options
- from mypy.util import FancyFormatter, bytes_to_human_readable_repr, is_dunder, plural_s
- class Missing:
- """Marker object for things that are missing (from a stub or the runtime)."""
- def __repr__(self) -> str:
- return "MISSING"
- MISSING: typing_extensions.Final = Missing()
- T = TypeVar("T")
- MaybeMissing: typing_extensions.TypeAlias = Union[T, Missing]
- _formatter: typing_extensions.Final = FancyFormatter(sys.stdout, sys.stderr, False)
- def _style(message: str, **kwargs: Any) -> str:
- """Wrapper around mypy.util for fancy formatting."""
- kwargs.setdefault("color", "none")
- return _formatter.style(message, **kwargs)
- def _truncate(message: str, length: int) -> str:
- if len(message) > length:
- return message[: length - 3] + "..."
- return message
- class StubtestFailure(Exception):
- pass
- class Error:
- def __init__(
- self,
- object_path: list[str],
- message: str,
- stub_object: MaybeMissing[nodes.Node],
- runtime_object: MaybeMissing[Any],
- *,
- stub_desc: str | None = None,
- runtime_desc: str | None = None,
- ) -> None:
- """Represents an error found by stubtest.
- :param object_path: Location of the object with the error,
- e.g. ``["module", "Class", "method"]``
- :param message: Error message
- :param stub_object: The mypy node representing the stub
- :param runtime_object: Actual object obtained from the runtime
- :param stub_desc: Specialised description for the stub object, should you wish
- :param runtime_desc: Specialised description for the runtime object, should you wish
- """
- self.object_path = object_path
- self.object_desc = ".".join(object_path)
- self.message = message
- self.stub_object = stub_object
- self.runtime_object = runtime_object
- self.stub_desc = stub_desc or str(getattr(stub_object, "type", stub_object))
- self.runtime_desc = runtime_desc or _truncate(repr(runtime_object), 100)
- def is_missing_stub(self) -> bool:
- """Whether or not the error is for something missing from the stub."""
- return isinstance(self.stub_object, Missing)
- def is_positional_only_related(self) -> bool:
- """Whether or not the error is for something being (or not being) positional-only."""
- # TODO: This is hacky, use error codes or something more resilient
- return "leading double underscore" in self.message
- def get_description(self, concise: bool = False) -> str:
- """Returns a description of the error.
- :param concise: Whether to return a concise, one-line description
- """
- if concise:
- return _style(self.object_desc, bold=True) + " " + self.message
- stub_line = None
- stub_file = None
- if not isinstance(self.stub_object, Missing):
- stub_line = self.stub_object.line
- stub_node = get_stub(self.object_path[0])
- if stub_node is not None:
- stub_file = stub_node.path or None
- stub_loc_str = ""
- if stub_file:
- stub_loc_str += f" in file {Path(stub_file)}"
- if stub_line:
- stub_loc_str += f"{':' if stub_file else ' at line '}{stub_line}"
- runtime_line = None
- runtime_file = None
- if not isinstance(self.runtime_object, Missing):
- try:
- runtime_line = inspect.getsourcelines(self.runtime_object)[1]
- except (OSError, TypeError, SyntaxError):
- pass
- try:
- runtime_file = inspect.getsourcefile(self.runtime_object)
- except TypeError:
- pass
- runtime_loc_str = ""
- if runtime_file:
- runtime_loc_str += f" in file {Path(runtime_file)}"
- if runtime_line:
- runtime_loc_str += f"{':' if runtime_file else ' at line '}{runtime_line}"
- output = [
- _style("error: ", color="red", bold=True),
- _style(self.object_desc, bold=True),
- " ",
- self.message,
- "\n",
- "Stub:",
- _style(stub_loc_str, dim=True),
- "\n",
- _style(self.stub_desc + "\n", color="blue", dim=True),
- "Runtime:",
- _style(runtime_loc_str, dim=True),
- "\n",
- _style(self.runtime_desc + "\n", color="blue", dim=True),
- ]
- return "".join(output)
- # ====================
- # Core logic
- # ====================
- def silent_import_module(module_name: str) -> types.ModuleType:
- with open(os.devnull, "w") as devnull:
- with warnings.catch_warnings(), redirect_stdout(devnull), redirect_stderr(devnull):
- warnings.simplefilter("ignore")
- runtime = importlib.import_module(module_name)
- # Also run the equivalent of `from module import *`
- # This could have the additional effect of loading not-yet-loaded submodules
- # mentioned in __all__
- __import__(module_name, fromlist=["*"])
- return runtime
- def test_module(module_name: str) -> Iterator[Error]:
- """Tests a given module's stub against introspecting it at runtime.
- Requires the stub to have been built already, accomplished by a call to ``build_stubs``.
- :param module_name: The module to test
- """
- stub = get_stub(module_name)
- if stub is None:
- if not is_probably_private(module_name.split(".")[-1]):
- runtime_desc = repr(sys.modules[module_name]) if module_name in sys.modules else "N/A"
- yield Error(
- [module_name], "failed to find stubs", MISSING, None, runtime_desc=runtime_desc
- )
- return
- try:
- runtime = silent_import_module(module_name)
- except KeyboardInterrupt:
- raise
- except BaseException as e:
- yield Error([module_name], f"failed to import, {type(e).__name__}: {e}", stub, MISSING)
- return
- with warnings.catch_warnings():
- warnings.simplefilter("ignore")
- try:
- yield from verify(stub, runtime, [module_name])
- except Exception as e:
- bottom_frame = list(traceback.walk_tb(e.__traceback__))[-1][0]
- bottom_module = bottom_frame.f_globals.get("__name__", "")
- # Pass on any errors originating from stubtest or mypy
- # These can occur expectedly, e.g. StubtestFailure
- if bottom_module == "__main__" or bottom_module.split(".")[0] == "mypy":
- raise
- yield Error(
- [module_name],
- f"encountered unexpected error, {type(e).__name__}: {e}",
- stub,
- runtime,
- stub_desc="N/A",
- runtime_desc=(
- "This is most likely the fault of something very dynamic in your library. "
- "It's also possible this is a bug in stubtest.\nIf in doubt, please "
- "open an issue at https://github.com/python/mypy\n\n"
- + traceback.format_exc().strip()
- ),
- )
- @singledispatch
- def verify(
- stub: MaybeMissing[nodes.Node], runtime: MaybeMissing[Any], object_path: list[str]
- ) -> Iterator[Error]:
- """Entry point for comparing a stub to a runtime object.
- We use single dispatch based on the type of ``stub``.
- :param stub: The mypy node representing a part of the stub
- :param runtime: The runtime object corresponding to ``stub``
- """
- yield Error(object_path, "is an unknown mypy node", stub, runtime)
- def _verify_exported_names(
- object_path: list[str], stub: nodes.MypyFile, runtime_all_as_set: set[str]
- ) -> Iterator[Error]:
- # note that this includes the case the stub simply defines `__all__: list[str]`
- assert "__all__" in stub.names
- public_names_in_stub = {m for m, o in stub.names.items() if o.module_public}
- names_in_stub_not_runtime = sorted(public_names_in_stub - runtime_all_as_set)
- names_in_runtime_not_stub = sorted(runtime_all_as_set - public_names_in_stub)
- if not (names_in_runtime_not_stub or names_in_stub_not_runtime):
- return
- yield Error(
- object_path + ["__all__"],
- (
- "names exported from the stub do not correspond to the names exported at runtime. "
- "This is probably due to things being missing from the stub or an inaccurate `__all__` in the stub"
- ),
- # Pass in MISSING instead of the stub and runtime objects, as the line numbers aren't very
- # relevant here, and it makes for a prettier error message
- # This means this error will be ignored when using `--ignore-missing-stub`, which is
- # desirable in at least the `names_in_runtime_not_stub` case
- stub_object=MISSING,
- runtime_object=MISSING,
- stub_desc=(
- f"Names exported in the stub but not at runtime: " f"{names_in_stub_not_runtime}"
- ),
- runtime_desc=(
- f"Names exported at runtime but not in the stub: " f"{names_in_runtime_not_stub}"
- ),
- )
- def _get_imported_symbol_names(runtime: types.ModuleType) -> frozenset[str] | None:
- """Retrieve the names in the global namespace which are known to be imported.
- 1). Use inspect to retrieve the source code of the module
- 2). Use symtable to parse the source and retrieve names that are known to be imported
- from other modules.
- If either of the above steps fails, return `None`.
- Note that if a set of names is returned,
- it won't include names imported via `from foo import *` imports.
- """
- try:
- source = inspect.getsource(runtime)
- except (OSError, TypeError, SyntaxError):
- return None
- if not source.strip():
- # The source code for the module was an empty file,
- # no point in parsing it with symtable
- return frozenset()
- try:
- module_symtable = symtable.symtable(source, runtime.__name__, "exec")
- except SyntaxError:
- return None
- return frozenset(sym.get_name() for sym in module_symtable.get_symbols() if sym.is_imported())
- @verify.register(nodes.MypyFile)
- def verify_mypyfile(
- stub: nodes.MypyFile, runtime: MaybeMissing[types.ModuleType], object_path: list[str]
- ) -> Iterator[Error]:
- if isinstance(runtime, Missing):
- yield Error(object_path, "is not present at runtime", stub, runtime)
- return
- if not isinstance(runtime, types.ModuleType):
- yield Error(object_path, "is not a module", stub, runtime)
- return
- runtime_all_as_set: set[str] | None
- if hasattr(runtime, "__all__"):
- runtime_all_as_set = set(runtime.__all__)
- if "__all__" in stub.names:
- # Only verify the contents of the stub's __all__
- # if the stub actually defines __all__
- yield from _verify_exported_names(object_path, stub, runtime_all_as_set)
- else:
- runtime_all_as_set = None
- # Check things in the stub
- to_check = {
- m
- for m, o in stub.names.items()
- if not o.module_hidden and (not is_probably_private(m) or hasattr(runtime, m))
- }
- imported_symbols = _get_imported_symbol_names(runtime)
- def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
- """Heuristics to determine whether a name originates from another module."""
- obj = getattr(r, attr)
- if isinstance(obj, types.ModuleType):
- return False
- if callable(obj):
- # It's highly likely to be a class or a function if it's callable,
- # so the __module__ attribute will give a good indication of which module it comes from
- try:
- obj_mod = obj.__module__
- except Exception:
- pass
- else:
- if isinstance(obj_mod, str):
- return bool(obj_mod == r.__name__)
- if imported_symbols is not None:
- return attr not in imported_symbols
- return True
- runtime_public_contents = (
- runtime_all_as_set
- if runtime_all_as_set is not None
- else {
- m
- for m in dir(runtime)
- if not is_probably_private(m)
- # Filter out objects that originate from other modules (best effort). Note that in the
- # absence of __all__, we don't have a way to detect explicit / intentional re-exports
- # at runtime
- and _belongs_to_runtime(runtime, m)
- }
- )
- # Check all things declared in module's __all__, falling back to our best guess
- to_check.update(runtime_public_contents)
- to_check.difference_update(IGNORED_MODULE_DUNDERS)
- for entry in sorted(to_check):
- stub_entry = stub.names[entry].node if entry in stub.names else MISSING
- if isinstance(stub_entry, nodes.MypyFile):
- # Don't recursively check exported modules, since that leads to infinite recursion
- continue
- assert stub_entry is not None
- try:
- runtime_entry = getattr(runtime, entry, MISSING)
- except Exception:
- # Catch all exceptions in case the runtime raises an unexpected exception
- # from __getattr__ or similar.
- continue
- yield from verify(stub_entry, runtime_entry, object_path + [entry])
- def _verify_final(
- stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str]
- ) -> Iterator[Error]:
- try:
- class SubClass(runtime): # type: ignore[misc]
- pass
- except TypeError:
- # Enum classes are implicitly @final
- if not stub.is_final and not issubclass(runtime, enum.Enum):
- yield Error(
- object_path,
- "cannot be subclassed at runtime, but isn't marked with @final in the stub",
- stub,
- runtime,
- stub_desc=repr(stub),
- )
- except Exception:
- # The class probably wants its subclasses to do something special.
- # Examples: ctypes.Array, ctypes._SimpleCData
- pass
- # Runtime class might be annotated with `@final`:
- try:
- runtime_final = getattr(runtime, "__final__", False)
- except Exception:
- runtime_final = False
- if runtime_final and not stub.is_final:
- yield Error(
- object_path,
- "has `__final__` attribute, but isn't marked with @final in the stub",
- stub,
- runtime,
- stub_desc=repr(stub),
- )
- def _verify_metaclass(
- 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, as are runtime TypedDicts; ignoring.
- # TODO: check that metaclasses are identical?
- 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.
- yield Error(
- object_path,
- "is inconsistent, metaclass differs",
- stub,
- runtime,
- stub_desc="N/A",
- runtime_desc=f"{runtime_metaclass}",
- )
- elif (
- runtime_metaclass is type
- and stub.metaclass_type is not None
- # We ignore extra `ABCMeta` metaclass on stubs, this might be typing hack.
- # We also ignore `builtins.type` metaclass as an implementation detail in mypy.
- and not mypy.types.is_named_instance(
- stub.metaclass_type, ("abc.ABCMeta", "builtins.type")
- )
- ):
- # This means that our stub has a metaclass that is not present at runtime.
- yield Error(
- object_path,
- "metaclass mismatch",
- stub,
- runtime,
- stub_desc=f"{stub.metaclass_type.type.fullname}",
- runtime_desc="N/A",
- )
- @verify.register(nodes.TypeInfo)
- def verify_typeinfo(
- stub: nodes.TypeInfo, runtime: MaybeMissing[type[Any]], object_path: list[str]
- ) -> Iterator[Error]:
- if isinstance(runtime, Missing):
- yield Error(object_path, "is not present at runtime", stub, runtime, stub_desc=repr(stub))
- return
- if not isinstance(runtime, type):
- yield Error(object_path, "is not a type", stub, runtime, stub_desc=repr(stub))
- return
- yield from _verify_final(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 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
- if entry.startswith("__") and not entry.endswith("__"):
- mangled_entry = f"_{stub.name.lstrip('_')}{entry}"
- stub_to_verify = next((t.names[entry].node for t in stub.mro if entry in t.names), MISSING)
- assert stub_to_verify is not None
- try:
- try:
- runtime_attr = getattr(runtime, mangled_entry)
- except AttributeError:
- runtime_attr = inspect.getattr_static(runtime, mangled_entry, MISSING)
- except Exception:
- # Catch all exceptions in case the runtime raises an unexpected exception
- # from __getattr__ or similar.
- continue
- # Do not error for an object missing from the stub
- # If the runtime object is a types.WrapperDescriptorType object
- # and has a non-special dunder name.
- # The vast majority of these are false positives.
- if not (
- isinstance(stub_to_verify, Missing)
- and isinstance(runtime_attr, types.WrapperDescriptorType)
- and is_dunder(mangled_entry, exclude_special=True)
- ):
- yield from verify(stub_to_verify, runtime_attr, object_path + [entry])
- def _static_lookup_runtime(object_path: list[str]) -> MaybeMissing[Any]:
- static_runtime = importlib.import_module(object_path[0])
- for entry in object_path[1:]:
- try:
- static_runtime = inspect.getattr_static(static_runtime, entry)
- except AttributeError:
- # This can happen with mangled names, ignore for now.
- # TODO: pass more information about ancestors of nodes/objects to verify, so we don't
- # have to do this hacky lookup. Would be useful in several places.
- return MISSING
- return static_runtime
- def _verify_static_class_methods(
- stub: nodes.FuncBase, runtime: Any, static_runtime: MaybeMissing[Any], object_path: list[str]
- ) -> Iterator[str]:
- if stub.name in ("__new__", "__init_subclass__", "__class_getitem__"):
- # Special cased by Python, so don't bother checking
- return
- if inspect.isbuiltin(runtime):
- # The isinstance checks don't work reliably for builtins, e.g. datetime.datetime.now, so do
- # something a little hacky that seems to work well
- probably_class_method = isinstance(getattr(runtime, "__self__", None), type)
- if probably_class_method and not stub.is_class:
- yield "runtime is a classmethod but stub is not"
- if not probably_class_method and stub.is_class:
- yield "stub is a classmethod but runtime is not"
- return
- if static_runtime is MISSING:
- return
- if isinstance(static_runtime, classmethod) and not stub.is_class:
- yield "runtime is a classmethod but stub is not"
- if not isinstance(static_runtime, classmethod) and stub.is_class:
- yield "stub is a classmethod but runtime is not"
- if isinstance(static_runtime, staticmethod) and not stub.is_static:
- yield "runtime is a staticmethod but stub is not"
- if not isinstance(static_runtime, staticmethod) and stub.is_static:
- yield "stub is a staticmethod but runtime is not"
- def _verify_arg_name(
- stub_arg: nodes.Argument, runtime_arg: inspect.Parameter, function_name: str
- ) -> Iterator[str]:
- """Checks whether argument names match."""
- # Ignore exact names for most dunder methods
- if is_dunder(function_name, exclude_special=True):
- return
- def strip_prefix(s: str, prefix: str) -> str:
- return s[len(prefix) :] if s.startswith(prefix) else s
- if strip_prefix(stub_arg.variable.name, "__") == runtime_arg.name:
- return
- def names_approx_match(a: str, b: str) -> bool:
- a = a.strip("_")
- b = b.strip("_")
- return a.startswith(b) or b.startswith(a) or len(a) == 1 or len(b) == 1
- # Be more permissive about names matching for positional-only arguments
- if runtime_arg.kind == inspect.Parameter.POSITIONAL_ONLY and names_approx_match(
- stub_arg.variable.name, runtime_arg.name
- ):
- return
- # This comes up with namedtuples, so ignore
- if stub_arg.variable.name == "_self":
- return
- yield (
- f'stub argument "{stub_arg.variable.name}" '
- f'differs from runtime argument "{runtime_arg.name}"'
- )
- def _verify_arg_default_value(
- stub_arg: nodes.Argument, runtime_arg: inspect.Parameter
- ) -> Iterator[str]:
- """Checks whether argument default values are compatible."""
- if runtime_arg.default != inspect.Parameter.empty:
- if stub_arg.kind.is_required():
- yield (
- f'runtime argument "{runtime_arg.name}" '
- "has a default value but stub argument does not"
- )
- else:
- runtime_type = get_mypy_type_of_runtime_value(runtime_arg.default)
- # Fallback to the type annotation type if var type is missing. The type annotation
- # is an UnboundType, but I don't know enough to know what the pros and cons here are.
- # UnboundTypes have ugly question marks following them, so default to var type.
- # Note we do this same fallback when constructing signatures in from_overloadedfuncdef
- stub_type = stub_arg.variable.type or stub_arg.type_annotation
- if isinstance(stub_type, mypy.types.TypeVarType):
- stub_type = stub_type.upper_bound
- if (
- runtime_type is not None
- and stub_type is not None
- # Avoid false positives for marker objects
- and type(runtime_arg.default) != object
- # And ellipsis
- and runtime_arg.default is not ...
- and not is_subtype_helper(runtime_type, stub_type)
- ):
- yield (
- f'runtime argument "{runtime_arg.name}" '
- f"has a default value of type {runtime_type}, "
- f"which is incompatible with stub argument type {stub_type}"
- )
- if stub_arg.initializer is not None:
- stub_default = evaluate_expression(stub_arg.initializer)
- if (
- stub_default is not UNKNOWN
- and stub_default is not ...
- and (
- stub_default != runtime_arg.default
- # We want the types to match exactly, e.g. in case the stub has
- # True and the runtime has 1 (or vice versa).
- or type(stub_default) is not type(runtime_arg.default) # noqa: E721
- )
- ):
- yield (
- f'runtime argument "{runtime_arg.name}" '
- f"has a default value of {runtime_arg.default!r}, "
- f"which is different from stub argument default {stub_default!r}"
- )
- else:
- if stub_arg.kind.is_optional():
- yield (
- f'stub argument "{stub_arg.variable.name}" has a default value '
- f"but runtime argument does not"
- )
- def maybe_strip_cls(name: str, args: list[nodes.Argument]) -> list[nodes.Argument]:
- if args and name in ("__init_subclass__", "__class_getitem__"):
- # These are implicitly classmethods. If the stub chooses not to have @classmethod, we
- # should remove the cls argument
- if args[0].variable.name == "cls":
- return args[1:]
- return args
- class Signature(Generic[T]):
- def __init__(self) -> None:
- self.pos: list[T] = []
- self.kwonly: dict[str, T] = {}
- self.varpos: T | None = None
- self.varkw: T | None = None
- def __str__(self) -> str:
- def get_name(arg: Any) -> str:
- if isinstance(arg, inspect.Parameter):
- return arg.name
- if isinstance(arg, nodes.Argument):
- return arg.variable.name
- raise AssertionError
- def get_type(arg: Any) -> str | None:
- if isinstance(arg, inspect.Parameter):
- return None
- if isinstance(arg, nodes.Argument):
- return str(arg.variable.type or arg.type_annotation)
- raise AssertionError
- def has_default(arg: Any) -> bool:
- if isinstance(arg, inspect.Parameter):
- return bool(arg.default != inspect.Parameter.empty)
- if isinstance(arg, nodes.Argument):
- return arg.kind.is_optional()
- raise AssertionError
- def get_desc(arg: Any) -> str:
- arg_type = get_type(arg)
- return (
- get_name(arg)
- + (f": {arg_type}" if arg_type else "")
- + (" = ..." if has_default(arg) else "")
- )
- kw_only = sorted(self.kwonly.values(), key=lambda a: (has_default(a), get_name(a)))
- ret = "def ("
- ret += ", ".join(
- [get_desc(arg) for arg in self.pos]
- + (["*" + get_name(self.varpos)] if self.varpos else (["*"] if self.kwonly else []))
- + [get_desc(arg) for arg in kw_only]
- + (["**" + get_name(self.varkw)] if self.varkw else [])
- )
- ret += ")"
- return ret
- @staticmethod
- def from_funcitem(stub: nodes.FuncItem) -> Signature[nodes.Argument]:
- stub_sig: Signature[nodes.Argument] = Signature()
- stub_args = maybe_strip_cls(stub.name, stub.arguments)
- for stub_arg in stub_args:
- if stub_arg.kind.is_positional():
- stub_sig.pos.append(stub_arg)
- elif stub_arg.kind.is_named():
- stub_sig.kwonly[stub_arg.variable.name] = stub_arg
- elif stub_arg.kind == nodes.ARG_STAR:
- stub_sig.varpos = stub_arg
- elif stub_arg.kind == nodes.ARG_STAR2:
- stub_sig.varkw = stub_arg
- else:
- raise AssertionError
- return stub_sig
- @staticmethod
- def from_inspect_signature(signature: inspect.Signature) -> Signature[inspect.Parameter]:
- runtime_sig: Signature[inspect.Parameter] = Signature()
- for runtime_arg in signature.parameters.values():
- if runtime_arg.kind in (
- inspect.Parameter.POSITIONAL_ONLY,
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
- ):
- runtime_sig.pos.append(runtime_arg)
- elif runtime_arg.kind == inspect.Parameter.KEYWORD_ONLY:
- runtime_sig.kwonly[runtime_arg.name] = runtime_arg
- elif runtime_arg.kind == inspect.Parameter.VAR_POSITIONAL:
- runtime_sig.varpos = runtime_arg
- elif runtime_arg.kind == inspect.Parameter.VAR_KEYWORD:
- runtime_sig.varkw = runtime_arg
- else:
- raise AssertionError
- return runtime_sig
- @staticmethod
- def from_overloadedfuncdef(stub: nodes.OverloadedFuncDef) -> Signature[nodes.Argument]:
- """Returns a Signature from an OverloadedFuncDef.
- If life were simple, to verify_overloadedfuncdef, we'd just verify_funcitem for each of its
- items. Unfortunately, life isn't simple and overloads are pretty deceitful. So instead, we
- try and combine the overload's items into a single signature that is compatible with any
- lies it might try to tell.
- """
- # For most dunder methods, just assume all args are positional-only
- assume_positional_only = is_dunder(stub.name, exclude_special=True)
- all_args: dict[str, list[tuple[nodes.Argument, int]]] = {}
- for func in map(_resolve_funcitem_from_decorator, stub.items):
- assert func is not None
- args = maybe_strip_cls(stub.name, func.arguments)
- for index, arg in enumerate(args):
- # For positional-only args, we allow overloads to have different names for the same
- # argument. To accomplish this, we just make up a fake index-based name.
- name = (
- f"__{index}"
- if arg.variable.name.startswith("__") or assume_positional_only
- else arg.variable.name
- )
- all_args.setdefault(name, []).append((arg, index))
- def get_position(arg_name: str) -> int:
- # We just need this to return the positional args in the correct order.
- return max(index for _, index in all_args[arg_name])
- def get_type(arg_name: str) -> mypy.types.ProperType:
- with mypy.state.state.strict_optional_set(True):
- all_types = [
- arg.variable.type or arg.type_annotation for arg, _ in all_args[arg_name]
- ]
- return mypy.typeops.make_simplified_union([t for t in all_types if t])
- def get_kind(arg_name: str) -> nodes.ArgKind:
- kinds = {arg.kind for arg, _ in all_args[arg_name]}
- if nodes.ARG_STAR in kinds:
- return nodes.ARG_STAR
- if nodes.ARG_STAR2 in kinds:
- return nodes.ARG_STAR2
- # The logic here is based on two tenets:
- # 1) If an arg is ever optional (or unspecified), it is optional
- # 2) If an arg is ever positional, it is positional
- is_opt = (
- len(all_args[arg_name]) < len(stub.items)
- or nodes.ARG_OPT in kinds
- or nodes.ARG_NAMED_OPT in kinds
- )
- is_pos = nodes.ARG_OPT in kinds or nodes.ARG_POS in kinds
- if is_opt:
- return nodes.ARG_OPT if is_pos else nodes.ARG_NAMED_OPT
- return nodes.ARG_POS if is_pos else nodes.ARG_NAMED
- sig: Signature[nodes.Argument] = Signature()
- for arg_name in sorted(all_args, key=get_position):
- # example_arg_name gives us a real name (in case we had a fake index-based name)
- example_arg_name = all_args[arg_name][0][0].variable.name
- arg = nodes.Argument(
- nodes.Var(example_arg_name, get_type(arg_name)),
- type_annotation=None,
- initializer=None,
- kind=get_kind(arg_name),
- )
- if arg.kind.is_positional():
- sig.pos.append(arg)
- elif arg.kind.is_named():
- sig.kwonly[arg.variable.name] = arg
- elif arg.kind == nodes.ARG_STAR:
- sig.varpos = arg
- elif arg.kind == nodes.ARG_STAR2:
- sig.varkw = arg
- else:
- raise AssertionError
- return sig
- def _verify_signature(
- stub: Signature[nodes.Argument], runtime: Signature[inspect.Parameter], function_name: str
- ) -> Iterator[str]:
- # Check positional arguments match up
- for stub_arg, runtime_arg in zip(stub.pos, runtime.pos):
- yield from _verify_arg_name(stub_arg, runtime_arg, function_name)
- yield from _verify_arg_default_value(stub_arg, runtime_arg)
- if (
- runtime_arg.kind == inspect.Parameter.POSITIONAL_ONLY
- and not stub_arg.pos_only
- and not stub_arg.variable.name.startswith("__")
- and not stub_arg.variable.name.strip("_") == "self"
- and not is_dunder(function_name, exclude_special=True) # noisy for dunder methods
- ):
- yield (
- f'stub argument "{stub_arg.variable.name}" should be positional-only '
- f'(rename with a leading double underscore, i.e. "__{runtime_arg.name}")'
- )
- if (
- runtime_arg.kind != inspect.Parameter.POSITIONAL_ONLY
- and (stub_arg.pos_only or stub_arg.variable.name.startswith("__"))
- and not is_dunder(function_name, exclude_special=True) # noisy for dunder methods
- ):
- yield (
- f'stub argument "{stub_arg.variable.name}" should be positional or keyword '
- "(remove leading double underscore)"
- )
- # Check unmatched positional args
- if len(stub.pos) > len(runtime.pos):
- # There are cases where the stub exhaustively lists out the extra parameters the function
- # would take through *args. Hence, a) if runtime accepts *args, we don't check whether the
- # runtime has all of the stub's parameters, b) below, we don't enforce that the stub takes
- # *args, since runtime logic may prevent arbitrary arguments from actually being accepted.
- if runtime.varpos is None:
- for stub_arg in stub.pos[len(runtime.pos) :]:
- # If the variable is in runtime.kwonly, it's just mislabelled as not a
- # keyword-only argument
- if stub_arg.variable.name not in runtime.kwonly:
- yield f'runtime does not have argument "{stub_arg.variable.name}"'
- else:
- yield f'stub argument "{stub_arg.variable.name}" is not keyword-only'
- if stub.varpos is not None:
- yield f'runtime does not have *args argument "{stub.varpos.variable.name}"'
- elif len(stub.pos) < len(runtime.pos):
- for runtime_arg in runtime.pos[len(stub.pos) :]:
- if runtime_arg.name not in stub.kwonly:
- yield f'stub does not have argument "{runtime_arg.name}"'
- else:
- yield f'runtime argument "{runtime_arg.name}" is not keyword-only'
- # Checks involving *args
- if len(stub.pos) <= len(runtime.pos) or runtime.varpos is None:
- if stub.varpos is None and runtime.varpos is not None:
- yield f'stub does not have *args argument "{runtime.varpos.name}"'
- if stub.varpos is not None and runtime.varpos is None:
- yield f'runtime does not have *args argument "{stub.varpos.variable.name}"'
- # Check keyword-only args
- for arg in sorted(set(stub.kwonly) & set(runtime.kwonly)):
- stub_arg, runtime_arg = stub.kwonly[arg], runtime.kwonly[arg]
- yield from _verify_arg_name(stub_arg, runtime_arg, function_name)
- yield from _verify_arg_default_value(stub_arg, runtime_arg)
- # Check unmatched keyword-only args
- if runtime.varkw is None or not set(runtime.kwonly).issubset(set(stub.kwonly)):
- # There are cases where the stub exhaustively lists out the extra parameters the function
- # would take through **kwargs. Hence, a) if runtime accepts **kwargs (and the stub hasn't
- # exhaustively listed out params), we don't check whether the runtime has all of the stub's
- # parameters, b) below, we don't enforce that the stub takes **kwargs, since runtime logic
- # may prevent arbitrary keyword arguments from actually being accepted.
- for arg in sorted(set(stub.kwonly) - set(runtime.kwonly)):
- if arg in {runtime_arg.name for runtime_arg in runtime.pos}:
- # Don't report this if we've reported it before
- if arg not in {runtime_arg.name for runtime_arg in runtime.pos[len(stub.pos) :]}:
- yield f'runtime argument "{arg}" is not keyword-only'
- else:
- yield f'runtime does not have argument "{arg}"'
- for arg in sorted(set(runtime.kwonly) - set(stub.kwonly)):
- if arg in {stub_arg.variable.name for stub_arg in stub.pos}:
- # Don't report this if we've reported it before
- if not (
- runtime.varpos is None
- and arg in {stub_arg.variable.name for stub_arg in stub.pos[len(runtime.pos) :]}
- ):
- yield f'stub argument "{arg}" is not keyword-only'
- else:
- yield f'stub does not have argument "{arg}"'
- # Checks involving **kwargs
- if stub.varkw is None and runtime.varkw is not None:
- # As mentioned above, don't enforce that the stub takes **kwargs.
- # Also check against positional parameters, to avoid a nitpicky message when an argument
- # isn't marked as keyword-only
- stub_pos_names = {stub_arg.variable.name for stub_arg in stub.pos}
- # Ideally we'd do a strict subset check, but in practice the errors from that aren't useful
- if not set(runtime.kwonly).issubset(set(stub.kwonly) | stub_pos_names):
- yield f'stub does not have **kwargs argument "{runtime.varkw.name}"'
- if stub.varkw is not None and runtime.varkw is None:
- yield f'runtime does not have **kwargs argument "{stub.varkw.variable.name}"'
- @verify.register(nodes.FuncItem)
- def verify_funcitem(
- stub: nodes.FuncItem, runtime: MaybeMissing[Any], object_path: list[str]
- ) -> Iterator[Error]:
- if isinstance(runtime, Missing):
- yield Error(object_path, "is not present at runtime", stub, runtime)
- return
- if not is_probably_a_function(runtime):
- yield Error(object_path, "is not a function", stub, runtime)
- if not callable(runtime):
- return
- # Look the object up statically, to avoid binding by the descriptor protocol
- static_runtime = _static_lookup_runtime(object_path)
- if isinstance(stub, nodes.FuncDef):
- for error_text in _verify_abstract_status(stub, runtime):
- yield Error(object_path, error_text, stub, runtime)
- for error_text in _verify_final_method(stub, runtime, static_runtime):
- yield Error(object_path, error_text, stub, runtime)
- for message in _verify_static_class_methods(stub, runtime, static_runtime, object_path):
- yield Error(object_path, "is inconsistent, " + message, stub, runtime)
- signature = safe_inspect_signature(runtime)
- runtime_is_coroutine = inspect.iscoroutinefunction(runtime)
- if signature:
- stub_sig = Signature.from_funcitem(stub)
- runtime_sig = Signature.from_inspect_signature(signature)
- runtime_sig_desc = f'{"async " if runtime_is_coroutine else ""}def {signature}'
- stub_desc = str(stub_sig)
- else:
- runtime_sig_desc, stub_desc = None, None
- # Don't raise an error if the stub is a coroutine, but the runtime isn't.
- # That results in false positives.
- # See https://github.com/python/typeshed/issues/7344
- if runtime_is_coroutine and not stub.is_coroutine:
- yield Error(
- object_path,
- 'is an "async def" function at runtime, but not in the stub',
- stub,
- runtime,
- stub_desc=stub_desc,
- runtime_desc=runtime_sig_desc,
- )
- if not signature:
- return
- for message in _verify_signature(stub_sig, runtime_sig, function_name=stub.name):
- yield Error(
- object_path,
- "is inconsistent, " + message,
- stub,
- runtime,
- runtime_desc=runtime_sig_desc,
- )
- @verify.register(Missing)
- def verify_none(
- stub: Missing, runtime: MaybeMissing[Any], object_path: list[str]
- ) -> Iterator[Error]:
- yield Error(object_path, "is not present in stub", stub, runtime)
- @verify.register(nodes.Var)
- def verify_var(
- stub: nodes.Var, runtime: MaybeMissing[Any], object_path: list[str]
- ) -> Iterator[Error]:
- if isinstance(runtime, Missing):
- # Don't always yield an error here, because we often can't find instance variables
- if len(object_path) <= 2:
- yield Error(object_path, "is not present at runtime", stub, runtime)
- return
- if (
- stub.is_initialized_in_class
- and is_read_only_property(runtime)
- and (stub.is_settable_property or not stub.is_property)
- ):
- yield Error(object_path, "is read-only at runtime but not in the stub", stub, runtime)
- runtime_type = get_mypy_type_of_runtime_value(runtime)
- if (
- runtime_type is not None
- and stub.type is not None
- and not is_subtype_helper(runtime_type, stub.type)
- ):
- should_error = True
- # Avoid errors when defining enums, since runtime_type is the enum itself, but we'd
- # annotate it with the type of runtime.value
- if isinstance(runtime, enum.Enum):
- runtime_type = get_mypy_type_of_runtime_value(runtime.value)
- if runtime_type is not None and is_subtype_helper(runtime_type, stub.type):
- should_error = False
- if should_error:
- yield Error(
- object_path, f"variable differs from runtime type {runtime_type}", stub, runtime
- )
- @verify.register(nodes.OverloadedFuncDef)
- def verify_overloadedfuncdef(
- stub: nodes.OverloadedFuncDef, runtime: MaybeMissing[Any], object_path: list[str]
- ) -> Iterator[Error]:
- if isinstance(runtime, Missing):
- yield Error(object_path, "is not present at runtime", stub, runtime)
- return
- if stub.is_property:
- # Any property with a setter is represented as an OverloadedFuncDef
- if is_read_only_property(runtime):
- yield Error(object_path, "is read-only at runtime but not in the stub", stub, runtime)
- return
- if not is_probably_a_function(runtime):
- yield Error(object_path, "is not a function", stub, runtime)
- if not callable(runtime):
- return
- # mypy doesn't allow overloads where one overload is abstract but another isn't,
- # so it should be okay to just check whether the first overload is abstract or not.
- #
- # TODO: Mypy *does* allow properties where e.g. the getter is abstract but the setter is not;
- # and any property with a setter is represented as an OverloadedFuncDef internally;
- # not sure exactly what (if anything) we should do about that.
- first_part = stub.items[0]
- if isinstance(first_part, nodes.Decorator) and first_part.is_overload:
- for msg in _verify_abstract_status(first_part.func, runtime):
- yield Error(object_path, msg, stub, runtime)
- # Look the object up statically, to avoid binding by the descriptor protocol
- static_runtime = _static_lookup_runtime(object_path)
- for message in _verify_static_class_methods(stub, runtime, static_runtime, object_path):
- yield Error(object_path, "is inconsistent, " + message, stub, runtime)
- # TODO: Should call _verify_final_method here,
- # but overloaded final methods in stubs cause a stubtest crash: see #14950
- signature = safe_inspect_signature(runtime)
- if not signature:
- return
- stub_sig = Signature.from_overloadedfuncdef(stub)
- runtime_sig = Signature.from_inspect_signature(signature)
- for message in _verify_signature(stub_sig, runtime_sig, function_name=stub.name):
- # TODO: This is a little hacky, but the addition here is super useful
- if "has a default value of type" in message:
- message += (
- ". This is often caused by overloads failing to account for explicitly passing "
- "in the default value."
- )
- yield Error(
- object_path,
- "is inconsistent, " + message,
- stub,
- runtime,
- stub_desc=(str(stub.type)) + f"\nInferred signature: {stub_sig}",
- runtime_desc="def " + str(signature),
- )
- @verify.register(nodes.TypeVarExpr)
- def verify_typevarexpr(
- stub: nodes.TypeVarExpr, runtime: MaybeMissing[Any], object_path: list[str]
- ) -> Iterator[Error]:
- if isinstance(runtime, Missing):
- # We seem to insert these typevars into NamedTuple stubs, but they
- # don't exist at runtime. Just ignore!
- if stub.name == "_NT":
- return
- yield Error(object_path, "is not present at runtime", stub, runtime)
- return
- if not isinstance(runtime, TypeVar):
- yield Error(object_path, "is not a TypeVar", stub, runtime)
- return
- @verify.register(nodes.ParamSpecExpr)
- def verify_paramspecexpr(
- stub: nodes.ParamSpecExpr, runtime: MaybeMissing[Any], object_path: list[str]
- ) -> Iterator[Error]:
- if isinstance(runtime, Missing):
- yield Error(object_path, "is not present at runtime", stub, runtime)
- return
- maybe_paramspec_types = (
- getattr(typing, "ParamSpec", None),
- getattr(typing_extensions, "ParamSpec", None),
- )
- paramspec_types = tuple(t for t in maybe_paramspec_types if t is not None)
- if not paramspec_types or not isinstance(runtime, paramspec_types):
- yield Error(object_path, "is not a ParamSpec", stub, runtime)
- return
- def _verify_readonly_property(stub: nodes.Decorator, runtime: Any) -> Iterator[str]:
- assert stub.func.is_property
- if isinstance(runtime, property):
- yield from _verify_final_method(stub.func, runtime.fget, MISSING)
- return
- if inspect.isdatadescriptor(runtime):
- # It's enough like a property...
- return
- # Sometimes attributes pretend to be properties, for instance, to express that they
- # are read only. So allowlist if runtime_type matches the return type of stub.
- runtime_type = get_mypy_type_of_runtime_value(runtime)
- func_type = (
- stub.func.type.ret_type if isinstance(stub.func.type, mypy.types.CallableType) else None
- )
- if (
- runtime_type is not None
- and func_type is not None
- and is_subtype_helper(runtime_type, func_type)
- ):
- return
- yield "is inconsistent, cannot reconcile @property on stub with runtime object"
- def _verify_abstract_status(stub: nodes.FuncDef, runtime: Any) -> Iterator[str]:
- stub_abstract = stub.abstract_status == nodes.IS_ABSTRACT
- runtime_abstract = getattr(runtime, "__isabstractmethod__", False)
- # The opposite can exist: some implementations omit `@abstractmethod` decorators
- if runtime_abstract and not stub_abstract:
- item_type = "property" if stub.is_property else "method"
- yield f"is inconsistent, runtime {item_type} is abstract but stub is not"
- def _verify_final_method(
- stub: nodes.FuncDef, runtime: Any, static_runtime: MaybeMissing[Any]
- ) -> Iterator[str]:
- if stub.is_final:
- return
- if getattr(runtime, "__final__", False) or (
- static_runtime is not MISSING and getattr(static_runtime, "__final__", False)
- ):
- yield "is decorated with @final at runtime, but not in the stub"
- def _resolve_funcitem_from_decorator(dec: nodes.OverloadPart) -> nodes.FuncItem | None:
- """Returns a FuncItem that corresponds to the output of the decorator.
- Returns None if we can't figure out what that would be. For convenience, this function also
- accepts FuncItems.
- """
- if isinstance(dec, nodes.FuncItem):
- return dec
- if dec.func.is_property:
- return None
- def apply_decorator_to_funcitem(
- decorator: nodes.Expression, func: nodes.FuncItem
- ) -> nodes.FuncItem | None:
- if not isinstance(decorator, nodes.RefExpr):
- return None
- if not decorator.fullname:
- # Happens with namedtuple
- return None
- if (
- decorator.fullname in ("builtins.staticmethod", "abc.abstractmethod")
- or decorator.fullname in mypy.types.OVERLOAD_NAMES
- ):
- return func
- if decorator.fullname == "builtins.classmethod":
- if func.arguments[0].variable.name not in ("cls", "mcs", "metacls"):
- raise StubtestFailure(
- f"unexpected class argument name {func.arguments[0].variable.name!r} "
- f"in {dec.fullname}"
- )
- # FuncItem is written so that copy.copy() actually works, even when compiled
- ret = copy.copy(func)
- # Remove the cls argument, since it's not present in inspect.signature of classmethods
- ret.arguments = ret.arguments[1:]
- return ret
- # Just give up on any other decorators. After excluding properties, we don't run into
- # anything else when running on typeshed's stdlib.
- return None
- func: nodes.FuncItem = dec.func
- for decorator in dec.original_decorators:
- resulting_func = apply_decorator_to_funcitem(decorator, func)
- if resulting_func is None:
- return None
- func = resulting_func
- return func
- @verify.register(nodes.Decorator)
- def verify_decorator(
- stub: nodes.Decorator, runtime: MaybeMissing[Any], object_path: list[str]
- ) -> Iterator[Error]:
- if isinstance(runtime, Missing):
- yield Error(object_path, "is not present at runtime", stub, runtime)
- return
- if stub.func.is_property:
- for message in _verify_readonly_property(stub, runtime):
- yield Error(object_path, message, stub, runtime)
- for message in _verify_abstract_status(stub.func, runtime):
- yield Error(object_path, message, stub, runtime)
- return
- func = _resolve_funcitem_from_decorator(stub)
- if func is not None:
- yield from verify(func, runtime, object_path)
- @verify.register(nodes.TypeAlias)
- def verify_typealias(
- stub: nodes.TypeAlias, runtime: MaybeMissing[Any], object_path: list[str]
- ) -> Iterator[Error]:
- stub_target = mypy.types.get_proper_type(stub.target)
- stub_desc = f"Type alias for {stub_target}"
- if isinstance(runtime, Missing):
- yield Error(object_path, "is not present at runtime", stub, runtime, stub_desc=stub_desc)
- return
- runtime_origin = get_origin(runtime) or runtime
- if isinstance(stub_target, mypy.types.Instance):
- if not isinstance(runtime_origin, type):
- yield Error(
- object_path,
- "is inconsistent, runtime is not a type",
- stub,
- runtime,
- stub_desc=stub_desc,
- )
- return
- stub_origin = stub_target.type
- # Do our best to figure out the fullname of the runtime object...
- runtime_name: object
- try:
- runtime_name = runtime_origin.__qualname__
- except AttributeError:
- runtime_name = getattr(runtime_origin, "__name__", MISSING)
- if isinstance(runtime_name, str):
- runtime_module: object = getattr(runtime_origin, "__module__", MISSING)
- if isinstance(runtime_module, str):
- if runtime_module == "collections.abc" or (
- runtime_module == "re" and runtime_name in {"Match", "Pattern"}
- ):
- runtime_module = "typing"
- runtime_fullname = f"{runtime_module}.{runtime_name}"
- if re.fullmatch(rf"_?{re.escape(stub_origin.fullname)}", runtime_fullname):
- # Okay, we're probably fine.
- return
- # Okay, either we couldn't construct a fullname
- # or the fullname of the stub didn't match the fullname of the runtime.
- # Fallback to a full structural check of the runtime vis-a-vis the stub.
- yield from verify(stub_origin, runtime_origin, object_path)
- return
- if isinstance(stub_target, mypy.types.UnionType):
- # complain if runtime is not a Union or UnionType
- if runtime_origin is not Union and (
- not (sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType))
- ):
- yield Error(object_path, "is not a Union", stub, runtime, stub_desc=str(stub_target))
- # could check Union contents here...
- return
- if isinstance(stub_target, mypy.types.TupleType):
- if tuple not in getattr(runtime_origin, "__mro__", ()):
- yield Error(
- object_path, "is not a subclass of tuple", stub, runtime, stub_desc=stub_desc
- )
- # could check Tuple contents here...
- return
- if isinstance(stub_target, mypy.types.CallableType):
- if runtime_origin is not collections.abc.Callable:
- yield Error(
- object_path, "is not a type alias for Callable", stub, runtime, stub_desc=stub_desc
- )
- # could check Callable contents here...
- return
- if isinstance(stub_target, mypy.types.AnyType):
- return
- yield Error(object_path, "is not a recognised type alias", stub, runtime, stub_desc=stub_desc)
- # ====================
- # Helpers
- # ====================
- IGNORED_MODULE_DUNDERS: typing_extensions.Final = frozenset(
- {
- "__file__",
- "__doc__",
- "__name__",
- "__builtins__",
- "__package__",
- "__cached__",
- "__loader__",
- "__spec__",
- "__annotations__",
- "__path__", # mypy adds __path__ to packages, but C packages don't have it
- "__getattr__", # resulting behaviour might be typed explicitly
- # Created by `warnings.warn`, does not make much sense to have in stubs:
- "__warningregistry__",
- # TODO: remove the following from this list
- "__author__",
- "__version__",
- "__copyright__",
- }
- )
- IGNORABLE_CLASS_DUNDERS: typing_extensions.Final = frozenset(
- {
- # Special attributes
- "__dict__",
- "__annotations__",
- "__text_signature__",
- "__weakref__",
- "__del__", # Only ever called when an object is being deleted, who cares?
- "__hash__",
- "__getattr__", # resulting behaviour might be typed explicitly
- "__setattr__", # defining this on a class can cause worse type checking
- "__vectorcalloffset__", # undocumented implementation detail of the vectorcall protocol
- # isinstance/issubclass hooks that type-checkers don't usually care about
- "__instancecheck__",
- "__subclasshook__",
- "__subclasscheck__",
- # python2 only magic methods:
- "__cmp__",
- "__nonzero__",
- "__unicode__",
- "__div__",
- # cython methods
- "__pyx_vtable__",
- # Pickle methods
- "__setstate__",
- "__getstate__",
- "__getnewargs__",
- "__getinitargs__",
- "__reduce_ex__",
- "__reduce__",
- # ctypes weirdness
- "__ctype_be__",
- "__ctype_le__",
- "__ctypes_from_outparam__",
- # mypy limitations
- "__abstractmethods__", # Classes with metaclass=ABCMeta inherit this attribute
- "__new_member__", # If an enum defines __new__, the method is renamed as __new_member__
- "__dataclass_fields__", # Generated by dataclasses
- "__dataclass_params__", # Generated by dataclasses
- "__doc__", # mypy's semanal for namedtuples assumes this is str, not Optional[str]
- # Added to all protocol classes on 3.12+ (or if using typing_extensions.Protocol)
- "__protocol_attrs__",
- "__callable_proto_members_only__",
- # typing implementation details, consider removing some of these:
- "__parameters__",
- "__origin__",
- "__args__",
- "__orig_bases__",
- "__final__", # Has a specialized check
- # Consider removing __slots__?
- "__slots__",
- }
- )
- def is_probably_private(name: str) -> bool:
- return name.startswith("_") and not is_dunder(name)
- def is_probably_a_function(runtime: Any) -> bool:
- return (
- isinstance(runtime, (types.FunctionType, types.BuiltinFunctionType))
- or isinstance(runtime, (types.MethodType, types.BuiltinMethodType))
- or (inspect.ismethoddescriptor(runtime) and callable(runtime))
- )
- def is_read_only_property(runtime: object) -> bool:
- return isinstance(runtime, property) and runtime.fset is None
- def safe_inspect_signature(runtime: Any) -> inspect.Signature | None:
- try:
- return inspect.signature(runtime)
- except Exception:
- # inspect.signature throws ValueError all the time
- # catch RuntimeError because of https://bugs.python.org/issue39504
- # catch TypeError because of https://github.com/python/typeshed/pull/5762
- # catch AttributeError because of inspect.signature(_curses.window.border)
- return None
- def is_subtype_helper(left: mypy.types.Type, right: mypy.types.Type) -> bool:
- """Checks whether ``left`` is a subtype of ``right``."""
- left = mypy.types.get_proper_type(left)
- right = mypy.types.get_proper_type(right)
- if (
- isinstance(left, mypy.types.LiteralType)
- and isinstance(left.value, int)
- and left.value in (0, 1)
- and mypy.types.is_named_instance(right, "builtins.bool")
- ):
- # Pretend Literal[0, 1] is a subtype of bool to avoid unhelpful errors.
- return True
- if isinstance(right, mypy.types.TypedDictType) and mypy.types.is_named_instance(
- left, "builtins.dict"
- ):
- # Special case checks against TypedDicts
- return True
- with mypy.state.state.strict_optional_set(True):
- return mypy.subtypes.is_subtype(left, right)
- def get_mypy_type_of_runtime_value(runtime: Any) -> mypy.types.Type | None:
- """Returns a mypy type object representing the type of ``runtime``.
- Returns None if we can't find something that works.
- """
- if runtime is None:
- return mypy.types.NoneType()
- if isinstance(runtime, property):
- # Give up on properties to avoid issues with things that are typed as attributes.
- return None
- def anytype() -> mypy.types.AnyType:
- return mypy.types.AnyType(mypy.types.TypeOfAny.unannotated)
- if isinstance(
- runtime,
- (types.FunctionType, types.BuiltinFunctionType, types.MethodType, types.BuiltinMethodType),
- ):
- builtins = get_stub("builtins")
- assert builtins is not None
- type_info = builtins.names["function"].node
- assert isinstance(type_info, nodes.TypeInfo)
- fallback = mypy.types.Instance(type_info, [anytype()])
- signature = safe_inspect_signature(runtime)
- if signature:
- arg_types = []
- arg_kinds = []
- arg_names = []
- for arg in signature.parameters.values():
- arg_types.append(anytype())
- arg_names.append(
- None if arg.kind == inspect.Parameter.POSITIONAL_ONLY else arg.name
- )
- has_default = arg.default == inspect.Parameter.empty
- if arg.kind == inspect.Parameter.POSITIONAL_ONLY:
- arg_kinds.append(nodes.ARG_POS if has_default else nodes.ARG_OPT)
- elif arg.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
- arg_kinds.append(nodes.ARG_POS if has_default else nodes.ARG_OPT)
- elif arg.kind == inspect.Parameter.KEYWORD_ONLY:
- arg_kinds.append(nodes.ARG_NAMED if has_default else nodes.ARG_NAMED_OPT)
- elif arg.kind == inspect.Parameter.VAR_POSITIONAL:
- arg_kinds.append(nodes.ARG_STAR)
- elif arg.kind == inspect.Parameter.VAR_KEYWORD:
- arg_kinds.append(nodes.ARG_STAR2)
- else:
- raise AssertionError
- else:
- arg_types = [anytype(), anytype()]
- arg_kinds = [nodes.ARG_STAR, nodes.ARG_STAR2]
- arg_names = [None, None]
- return mypy.types.CallableType(
- arg_types,
- arg_kinds,
- arg_names,
- ret_type=anytype(),
- fallback=fallback,
- is_ellipsis_args=True,
- )
- # Try and look up a stub for the runtime object
- stub = get_stub(type(runtime).__module__)
- if stub is None:
- return None
- type_name = type(runtime).__name__
- if type_name not in stub.names:
- return None
- type_info = stub.names[type_name].node
- if isinstance(type_info, nodes.Var):
- return type_info.type
- if not isinstance(type_info, nodes.TypeInfo):
- return None
- if isinstance(runtime, tuple):
- # Special case tuples so we construct a valid mypy.types.TupleType
- optional_items = [get_mypy_type_of_runtime_value(v) for v in runtime]
- items = [(i if i is not None else anytype()) for i in optional_items]
- fallback = mypy.types.Instance(type_info, [anytype()])
- return mypy.types.TupleType(items, fallback)
- fallback = mypy.types.Instance(type_info, [anytype() for _ in type_info.type_vars])
- value: bool | int | str
- if isinstance(runtime, bytes):
- value = bytes_to_human_readable_repr(runtime)
- elif isinstance(runtime, enum.Enum):
- value = runtime.name
- elif isinstance(runtime, (bool, int, str)):
- value = runtime
- else:
- return fallback
- return mypy.types.LiteralType(value=value, fallback=fallback)
- # ====================
- # Build and entrypoint
- # ====================
- _all_stubs: dict[str, nodes.MypyFile] = {}
- def build_stubs(modules: list[str], options: Options, find_submodules: bool = False) -> list[str]:
- """Uses mypy to construct stub objects for the given modules.
- This sets global state that ``get_stub`` can access.
- Returns all modules we might want to check. If ``find_submodules`` is False, this is equal
- to ``modules``.
- :param modules: List of modules to build stubs for.
- :param options: Mypy options for finding and building stubs.
- :param find_submodules: Whether to attempt to find submodules of the given modules as well.
- """
- data_dir = mypy.build.default_data_dir()
- search_path = mypy.modulefinder.compute_search_paths([], options, data_dir)
- find_module_cache = mypy.modulefinder.FindModuleCache(
- search_path, fscache=None, options=options
- )
- all_modules = []
- sources = []
- for module in modules:
- all_modules.append(module)
- if not find_submodules:
- module_path = find_module_cache.find_module(module)
- if not isinstance(module_path, str):
- # test_module will yield an error later when it can't find stubs
- continue
- sources.append(mypy.modulefinder.BuildSource(module_path, module, None))
- else:
- found_sources = find_module_cache.find_modules_recursive(module)
- sources.extend(found_sources)
- # find submodules via mypy
- all_modules.extend(s.module for s in found_sources if s.module not in all_modules)
- # find submodules via pkgutil
- try:
- runtime = silent_import_module(module)
- all_modules.extend(
- m.name
- for m in pkgutil.walk_packages(runtime.__path__, runtime.__name__ + ".")
- if m.name not in all_modules
- )
- except KeyboardInterrupt:
- raise
- except BaseException:
- pass
- if sources:
- try:
- res = mypy.build.build(sources=sources, options=options)
- except mypy.errors.CompileError as e:
- raise StubtestFailure(f"failed mypy compile:\n{e}") from e
- if res.errors:
- raise StubtestFailure("mypy build errors:\n" + "\n".join(res.errors))
- global _all_stubs
- _all_stubs = res.files
- return all_modules
- def get_stub(module: str) -> nodes.MypyFile | None:
- """Returns a stub object for the given module, if we've built one."""
- return _all_stubs.get(module)
- def get_typeshed_stdlib_modules(
- custom_typeshed_dir: str | None, version_info: tuple[int, int] | None = None
- ) -> list[str]:
- """Returns a list of stdlib modules in typeshed (for current Python version)."""
- stdlib_py_versions = mypy.modulefinder.load_stdlib_py_versions(custom_typeshed_dir)
- if version_info is None:
- version_info = sys.version_info[0:2]
- def exists_in_version(module: str) -> bool:
- assert version_info is not None
- parts = module.split(".")
- for i in range(len(parts), 0, -1):
- current_module = ".".join(parts[:i])
- if current_module in stdlib_py_versions:
- minver, maxver = stdlib_py_versions[current_module]
- return version_info >= minver and (maxver is None or version_info <= maxver)
- return False
- if custom_typeshed_dir:
- typeshed_dir = Path(custom_typeshed_dir)
- else:
- typeshed_dir = Path(mypy.build.default_data_dir()) / "typeshed"
- stdlib_dir = typeshed_dir / "stdlib"
- modules = []
- for path in stdlib_dir.rglob("*.pyi"):
- if path.stem == "__init__":
- path = path.parent
- module = ".".join(path.relative_to(stdlib_dir).parts[:-1] + (path.stem,))
- if exists_in_version(module):
- modules.append(module)
- return sorted(modules)
- def get_allowlist_entries(allowlist_file: str) -> Iterator[str]:
- def strip_comments(s: str) -> str:
- try:
- return s[: s.index("#")].strip()
- except ValueError:
- return s.strip()
- with open(allowlist_file) as f:
- for line in f:
- entry = strip_comments(line)
- if entry:
- yield entry
- class _Arguments:
- modules: list[str]
- concise: bool
- ignore_missing_stub: bool
- ignore_positional_only: bool
- allowlist: list[str]
- generate_allowlist: bool
- ignore_unused_allowlist: bool
- mypy_config_file: str
- custom_typeshed_dir: str
- check_typeshed: bool
- version: str
- def test_stubs(args: _Arguments, use_builtins_fixtures: bool = False) -> int:
- """This is stubtest! It's time to test the stubs!"""
- # Load the allowlist. This is a series of strings corresponding to Error.object_desc
- # Values in the dict will store whether we used the allowlist entry or not.
- allowlist = {
- entry: False
- for allowlist_file in args.allowlist
- for entry in get_allowlist_entries(allowlist_file)
- }
- allowlist_regexes = {entry: re.compile(entry) for entry in allowlist}
- # If we need to generate an allowlist, we store Error.object_desc for each error here.
- generated_allowlist = set()
- modules = args.modules
- if args.check_typeshed:
- if args.modules:
- print(
- _style("error:", color="red", bold=True),
- "cannot pass both --check-typeshed and a list of modules",
- )
- return 1
- modules = get_typeshed_stdlib_modules(args.custom_typeshed_dir)
- # typeshed added a stub for __main__, but that causes stubtest to check itself
- annoying_modules = {"antigravity", "this", "__main__"}
- modules = [m for m in modules if m not in annoying_modules]
- if not modules:
- print(_style("error:", color="red", bold=True), "no modules to check")
- return 1
- options = Options()
- options.incremental = False
- options.custom_typeshed_dir = args.custom_typeshed_dir
- if options.custom_typeshed_dir:
- options.abs_custom_typeshed_dir = os.path.abspath(args.custom_typeshed_dir)
- options.config_file = args.mypy_config_file
- options.use_builtins_fixtures = use_builtins_fixtures
- if options.config_file:
- def set_strict_flags() -> None: # not needed yet
- return
- parse_config_file(options, set_strict_flags, options.config_file, sys.stdout, sys.stderr)
- try:
- modules = build_stubs(modules, options, find_submodules=not args.check_typeshed)
- except StubtestFailure as stubtest_failure:
- print(
- _style("error:", color="red", bold=True),
- f"not checking stubs due to {stubtest_failure}",
- )
- return 1
- exit_code = 0
- error_count = 0
- for module in modules:
- for error in test_module(module):
- # Filter errors
- if args.ignore_missing_stub and error.is_missing_stub():
- continue
- if args.ignore_positional_only and error.is_positional_only_related():
- continue
- if error.object_desc in allowlist:
- allowlist[error.object_desc] = True
- continue
- is_allowlisted = False
- for w in allowlist:
- if allowlist_regexes[w].fullmatch(error.object_desc):
- allowlist[w] = True
- is_allowlisted = True
- break
- if is_allowlisted:
- continue
- # We have errors, so change exit code, and output whatever necessary
- exit_code = 1
- if args.generate_allowlist:
- generated_allowlist.add(error.object_desc)
- continue
- print(error.get_description(concise=args.concise))
- error_count += 1
- # Print unused allowlist entries
- if not args.ignore_unused_allowlist:
- for w in allowlist:
- # Don't consider an entry unused if it regex-matches the empty string
- # This lets us allowlist errors that don't manifest at all on some systems
- if not allowlist[w] and not allowlist_regexes[w].fullmatch(""):
- exit_code = 1
- error_count += 1
- print(f"note: unused allowlist entry {w}")
- # Print the generated allowlist
- if args.generate_allowlist:
- for e in sorted(generated_allowlist):
- print(e)
- exit_code = 0
- elif not args.concise:
- if error_count:
- print(
- _style(
- f"Found {error_count} error{plural_s(error_count)}"
- f" (checked {len(modules)} module{plural_s(modules)})",
- color="red",
- bold=True,
- )
- )
- else:
- print(
- _style(
- f"Success: no issues found in {len(modules)} module{plural_s(modules)}",
- color="green",
- bold=True,
- )
- )
- return exit_code
- def parse_options(args: list[str]) -> _Arguments:
- parser = argparse.ArgumentParser(
- description="Compares stubs to objects introspected from the runtime."
- )
- parser.add_argument("modules", nargs="*", help="Modules to test")
- parser.add_argument(
- "--concise",
- action="store_true",
- help="Makes stubtest's output more concise, one line per error",
- )
- parser.add_argument(
- "--ignore-missing-stub",
- action="store_true",
- help="Ignore errors for stub missing things that are present at runtime",
- )
- parser.add_argument(
- "--ignore-positional-only",
- action="store_true",
- help="Ignore errors for whether an argument should or shouldn't be positional-only",
- )
- parser.add_argument(
- "--allowlist",
- "--whitelist",
- action="append",
- metavar="FILE",
- default=[],
- help=(
- "Use file as an allowlist. Can be passed multiple times to combine multiple "
- "allowlists. Allowlists can be created with --generate-allowlist. Allowlists "
- "support regular expressions."
- ),
- )
- parser.add_argument(
- "--generate-allowlist",
- "--generate-whitelist",
- action="store_true",
- help="Print an allowlist (to stdout) to be used with --allowlist",
- )
- parser.add_argument(
- "--ignore-unused-allowlist",
- "--ignore-unused-whitelist",
- action="store_true",
- help="Ignore unused allowlist entries",
- )
- parser.add_argument(
- "--mypy-config-file",
- metavar="FILE",
- help=("Use specified mypy config file to determine mypy plugins and mypy path"),
- )
- parser.add_argument(
- "--custom-typeshed-dir", metavar="DIR", help="Use the custom typeshed in DIR"
- )
- parser.add_argument(
- "--check-typeshed", action="store_true", help="Check all stdlib modules in typeshed"
- )
- parser.add_argument(
- "--version", action="version", version="%(prog)s " + mypy.version.__version__
- )
- return parser.parse_args(args, namespace=_Arguments())
- def main() -> int:
- mypy.util.check_python_version("stubtest")
- return test_stubs(parse_options(sys.argv[1:]))
- if __name__ == "__main__":
- sys.exit(main())
|