matplotlib
319 строк · 10.8 Кб
1#!/usr/bin/env python
2"""
3Perform AST checks to validate consistency of type hints with implementation.
4
5NOTE: in most cases ``stubtest`` (distributed as part of ``mypy``) should be preferred
6
7This script was written before the configuration of ``stubtest`` was well understood.
8It still has some utility, particularly for checking certain deprecations or other
9decorators which modify the runtime signature where you want the type hint to match
10the python source rather than runtime signature, perhaps.
11
12The basic kinds of checks performed are:
13
14- Set of items defined by the stubs vs implementation
15- Missing stub: MISSING_STUB = 1
16- Missing implementation: MISSING_IMPL = 2
17- Signatures of functions/methods
18- Positional Only Args: POS_ARGS = 4
19- Keyword or Positional Args: ARGS = 8
20- Variadic Positional Args: VARARG = 16
21- Keyword Only Args: KWARGS = 32
22- Variadic Keyword Only Args: VARKWARG = 64
23
24There are some exceptions to when these are checked:
25
26- Set checks (MISSING_STUB/MISSING_IMPL) only apply at the module level
27- i.e. not for classes
28- Inheritance makes the set arithmetic harder when only loading AST
29- Attributes also make it more complicated when defined in init
30- Functions type hinted with ``overload`` are ignored for argument checking
31- Usually this means the implementation is less strict in signature but will raise
32if an invalid signature is used, type checking allows such errors to be caught by
33the type checker instead of at runtime.
34- Private attribute/functions are ignored
35- Not expecting a type hint
36- applies to anything beginning, but not ending in ``__``
37- If ``__all__`` is defined, also applies to anything not in ``__all__``
38- Deprecated methods are not checked for missing stub
39- Only applies to wholesale deprecation, not deprecation of an individual arg
40- Other kinds of deprecations (e.g. argument deletion or rename) the type hint should
41match the current python definition line still.
42- For renames, the new name is used
43- For deletions/make keyword only, it is removed upon expiry
44
45Usage:
46
47Currently there is not any argument handling/etc, so all configuration is done in
48source.
49Since stubtest has almost completely superseded this script, this is unlikely to change.
50
51The categories outlined above can each be ignored, and ignoring multiple can be done
52using the bitwise or (``|``) operator, e.g. ``ARGS | VARKWARG``.
53
54This can be done globally or on a per file basis, by editing ``per_file_ignore``.
55For the latter, the key is the Pathlib Path to the affected file, and the value is the
56integer ignore.
57
58Must be run from repository root:
59
60``python tools/check_typehints.py``
61"""
62
63import ast64import pathlib65import sys66
67MISSING_STUB = 168MISSING_IMPL = 269POS_ARGS = 470ARGS = 871VARARG = 1672KWARGS = 3273VARKWARG = 6474
75
76def check_file(path, ignore=0):77stubpath = path.with_suffix(".pyi")78ret = 079if not stubpath.exists():80return 0, 081tree = ast.parse(path.read_text())82stubtree = ast.parse(stubpath.read_text())83return check_namespace(tree, stubtree, path, ignore)84
85
86def check_namespace(tree, stubtree, path, ignore=0):87ret = 088count = 089tree_items = set(90i.name91for i in tree.body92if hasattr(i, "name") and (not i.name.startswith("_") or i.name.endswith("__"))93)94stubtree_items = set(95i.name96for i in stubtree.body97if hasattr(i, "name") and (not i.name.startswith("_") or i.name.endswith("__"))98)99
100for item in tree.body:101if isinstance(item, ast.Assign):102tree_items |= set(103i.id104for i in item.targets105if hasattr(i, "id")106and (not i.id.startswith("_") or i.id.endswith("__"))107)108for target in item.targets:109if isinstance(target, ast.Tuple):110tree_items |= set(i.id for i in target.elts)111elif isinstance(item, ast.AnnAssign):112tree_items |= {item.target.id}113for item in stubtree.body:114if isinstance(item, ast.Assign):115stubtree_items |= set(116i.id117for i in item.targets118if hasattr(i, "id")119and (not i.id.startswith("_") or i.id.endswith("__"))120)121for target in item.targets:122if isinstance(target, ast.Tuple):123stubtree_items |= set(i.id for i in target.elts)124elif isinstance(item, ast.AnnAssign):125stubtree_items |= {item.target.id}126
127try:128all_ = ast.literal_eval(ast.unparse(get_subtree(tree, "__all__").value))129except ValueError:130all_ = []131
132if all_:133missing = (tree_items - stubtree_items) & set(all_)134else:135missing = tree_items - stubtree_items136
137deprecated = set()138for item_name in missing:139item = get_subtree(tree, item_name)140if hasattr(item, "decorator_list"):141if "deprecated" in [142i.func.attr143for i in item.decorator_list144if hasattr(i, "func") and hasattr(i.func, "attr")145]:146deprecated |= {item_name}147
148if missing - deprecated and ~ignore & MISSING_STUB:149print(f"{path}: {missing - deprecated} missing from stubs")150ret |= MISSING_STUB151count += 1152
153non_class_or_func = set()154for item_name in stubtree_items - tree_items:155try:156get_subtree(tree, item_name)157except ValueError:158pass159else:160non_class_or_func |= {item_name}161
162missing_implementation = stubtree_items - tree_items - non_class_or_func163if missing_implementation and ~ignore & MISSING_IMPL:164print(f"{path}: {missing_implementation} in stubs and not source")165ret |= MISSING_IMPL166count += 1167
168for item_name in tree_items & stubtree_items:169item = get_subtree(tree, item_name)170stubitem = get_subtree(stubtree, item_name)171if isinstance(item, ast.FunctionDef) and isinstance(stubitem, ast.FunctionDef):172err, c = check_function(item, stubitem, f"{path}::{item_name}", ignore)173ret |= err174count += c175if isinstance(item, ast.ClassDef):176# Ignore set differences for classes... while it would be nice to have177# inheritance and attributes set in init/methods make both presence and178# absence of nodes spurious179err, c = check_namespace(180item,181stubitem,182f"{path}::{item_name}",183ignore | MISSING_STUB | MISSING_IMPL,184)185ret |= err186count += c187
188return ret, count189
190
191def check_function(item, stubitem, path, ignore):192ret = 0193count = 0194
195# if the stub calls overload, assume it knows what its doing196overloaded = "overload" in [197i.id for i in stubitem.decorator_list if hasattr(i, "id")198]199if overloaded:200return 0, 0201
202item_posargs = [a.arg for a in item.args.posonlyargs]203stubitem_posargs = [a.arg for a in stubitem.args.posonlyargs]204if item_posargs != stubitem_posargs and ~ignore & POS_ARGS:205print(206f"{path} {item.name} posargs differ: {item_posargs} vs {stubitem_posargs}"207)208ret |= POS_ARGS209count += 1210
211item_args = [a.arg for a in item.args.args]212stubitem_args = [a.arg for a in stubitem.args.args]213if item_args != stubitem_args and ~ignore & ARGS:214print(f"{path} args differ for {item.name}: {item_args} vs {stubitem_args}")215ret |= ARGS216count += 1217
218item_vararg = item.args.vararg219stubitem_vararg = stubitem.args.vararg220if ~ignore & VARARG:221if (item_vararg is None) ^ (stubitem_vararg is None):222if item_vararg:223print(224f"{path} {item.name} vararg differ: "225f"{item_vararg.arg} vs {stubitem_vararg}"226)227else:228print(229f"{path} {item.name} vararg differ: "230f"{item_vararg} vs {stubitem_vararg.arg}"231)232ret |= VARARG233count += 1234elif item_vararg is None:235pass236elif item_vararg.arg != stubitem_vararg.arg:237print(238f"{path} {item.name} vararg differ: "239f"{item_vararg.arg} vs {stubitem_vararg.arg}"240)241ret |= VARARG242count += 1243
244item_kwonlyargs = [a.arg for a in item.args.kwonlyargs]245stubitem_kwonlyargs = [a.arg for a in stubitem.args.kwonlyargs]246if item_kwonlyargs != stubitem_kwonlyargs and ~ignore & KWARGS:247print(248f"{path} {item.name} kwonlyargs differ: "249f"{item_kwonlyargs} vs {stubitem_kwonlyargs}"250)251ret |= KWARGS252count += 1253
254item_kwarg = item.args.kwarg255stubitem_kwarg = stubitem.args.kwarg256if ~ignore & VARKWARG:257if (item_kwarg is None) ^ (stubitem_kwarg is None):258if item_kwarg:259print(260f"{path} {item.name} varkwarg differ: "261f"{item_kwarg.arg} vs {stubitem_kwarg}"262)263else:264print(265f"{path} {item.name} varkwarg differ: "266f"{item_kwarg} vs {stubitem_kwarg.arg}"267)268ret |= VARKWARG269count += 1270elif item_kwarg is None:271pass272elif item_kwarg.arg != stubitem_kwarg.arg:273print(274f"{path} {item.name} varkwarg differ: "275f"{item_kwarg.arg} vs {stubitem_kwarg.arg}"276)277ret |= VARKWARG278count += 1279
280return ret, count281
282
283def get_subtree(tree, name):284for item in tree.body:285if isinstance(item, ast.Assign):286if name in [i.id for i in item.targets if hasattr(i, "id")]:287return item288for target in item.targets:289if isinstance(target, ast.Tuple):290if name in [i.id for i in target.elts]:291return item292if isinstance(item, ast.AnnAssign):293if name == item.target.id:294return item295if not hasattr(item, "name"):296continue297if item.name == name:298return item299raise ValueError(f"no such item {name} in tree")300
301
302if __name__ == "__main__":303out = 0304count = 0305basedir = pathlib.Path("lib/matplotlib")306per_file_ignore = {307# Edge cases for items set via `get_attr`, etc308basedir / "__init__.py": MISSING_IMPL,309# Base class has **kwargs, subclasses have more specific310basedir / "ticker.py": VARKWARG,311basedir / "layout_engine.py": VARKWARG,312}313for f in basedir.rglob("**/*.py"):314err, c = check_file(f, ignore=0 | per_file_ignore.get(f, 0))315out |= err316count += c317print("\n")318print(f"{count} total errors found")319sys.exit(out)320