diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 4b6e487a418d..1d08a690d8f2 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -1,51 +1,51 @@ # Non-typed (and private) modules/functions -matplotlib.backends.* -matplotlib.tests.* -matplotlib.pylab.* -matplotlib._.* -matplotlib.rcsetup._listify_validator -matplotlib.rcsetup._validate_linestyle -matplotlib.ft2font.Glyph -matplotlib.testing.jpl_units.* -matplotlib.sphinxext.* +matplotlib\.backends\..* +matplotlib\.tests(\..*)? +matplotlib\.pylab\..* +matplotlib\._.* +matplotlib\.rcsetup\._listify_validator +matplotlib\.rcsetup\._validate_linestyle +matplotlib\.ft2font\.Glyph +matplotlib\.testing\.jpl_units\..* +matplotlib\.sphinxext(\..*)? # set methods have heavy dynamic usage of **kwargs, with differences for subclasses # which results in technically inconsistent signatures, but not actually a problem -matplotlib.*\.set$ +matplotlib\..*\.set$ # Typed inline, inconsistencies largely due to imports -matplotlib.pyplot.* -matplotlib.typing.* +matplotlib\.pyplot\..* +matplotlib\.typing\..* # Other decorator modifying signature # Backcompat decorator which does not modify runtime reported signature -matplotlib.offsetbox.*Offset[Bb]ox.get_offset +matplotlib\.offsetbox\..*Offset[Bb]ox\.get_offset # Inconsistent super/sub class parameter name (maybe rename for consistency) -matplotlib.projections.polar.RadialLocator.nonsingular -matplotlib.ticker.LogLocator.nonsingular -matplotlib.ticker.LogitLocator.nonsingular +matplotlib\.projections\.polar\.RadialLocator\.nonsingular +matplotlib\.ticker\.LogLocator\.nonsingular +matplotlib\.ticker\.LogitLocator\.nonsingular # Stdlib/Enum considered inconsistent (no fault of ours, I don't think) -matplotlib.backend_bases._Mode.__new__ -matplotlib.units.Number.__hash__ +matplotlib\.backend_bases\._Mode\.__new__ +matplotlib\.units\.Number\.__hash__ # 3.6 Pending deprecations -matplotlib.figure.Figure.set_constrained_layout -matplotlib.figure.Figure.set_constrained_layout_pads -matplotlib.figure.Figure.set_tight_layout +matplotlib\.figure\.Figure\.set_constrained_layout +matplotlib\.figure\.Figure\.set_constrained_layout_pads +matplotlib\.figure\.Figure\.set_tight_layout # Maybe should be abstractmethods, required for subclasses, stubs define once -matplotlib.tri.*TriInterpolator.__call__ -matplotlib.tri.*TriInterpolator.gradient +matplotlib\.tri\..*TriInterpolator\.__call__ +matplotlib\.tri\..*TriInterpolator\.gradient # TypeVar used only in type hints -matplotlib.backend_bases.FigureCanvasBase._T -matplotlib.backend_managers.ToolManager._T -matplotlib.spines.Spine._T +matplotlib\.backend_bases\.FigureCanvasBase\._T +matplotlib\.backend_managers\.ToolManager\._T +matplotlib\.spines\.Spine\._T # Parameter inconsistency due to 3.10 deprecation -matplotlib.figure.FigureBase.get_figure +matplotlib\.figure\.FigureBase\.get_figure # getitem method only exists for 3.10 deprecation backcompatability -matplotlib.inset.InsetIndicator.__getitem__ +matplotlib\.inset\.InsetIndicator\.__getitem__ diff --git a/tools/stubtest.py b/tools/stubtest.py index 77676595cbf8..b79ab2f40dd0 100644 --- a/tools/stubtest.py +++ b/tools/stubtest.py @@ -1,6 +1,7 @@ import ast import os import pathlib +import re import subprocess import sys import tempfile @@ -12,10 +13,19 @@ class Visitor(ast.NodeVisitor): - def __init__(self, filepath, output): + def __init__(self, filepath, output, existing_allowed): self.filepath = filepath self.context = list(filepath.with_suffix("").relative_to(lib).parts) self.output = output + self.existing_allowed = existing_allowed + + def _is_already_allowed(self, parts): + # Skip outputting a path if it's already allowed before. + candidates = ['.'.join(parts[:s]) for s in range(1, len(parts))] + for allow in self.existing_allowed: + if any(allow.fullmatch(path) for path in candidates): + return True + return False def visit_FunctionDef(self, node): # delete_parameter adds a private sentinel value that leaks @@ -43,7 +53,9 @@ def visit_FunctionDef(self, node): ): parents.insert(0, parent.name) parent = parent.parent - self.output.write(f"{'.'.join(self.context + parents)}.{node.name}\n") + parts = [*self.context, *parents, node.name] + if not self._is_already_allowed(parts): + self.output.write("\\.".join(parts) + "\n") break def visit_ClassDef(self, node): @@ -62,20 +74,28 @@ def visit_ClassDef(self, node): # for setters on items with only a getter for substitutions in aliases.values(): parts = self.context + parents + [node.name] - self.output.write( - "\n".join( - f"{'.'.join(parts)}.[gs]et_{a}\n" for a in substitutions - ) - ) + for a in substitutions: + if not (self._is_already_allowed([*parts, f"get_{a}"]) and + self._is_already_allowed([*parts, f"set_{a}"])): + self.output.write("\\.".join([*parts, f"[gs]et_{a}\n"])) for child in ast.iter_child_nodes(node): self.visit(child) +existing_allowed = [] +with (root / 'ci/mypy-stubtest-allowlist.txt').open() as f: + for line in f: + line, _, _ = line.partition('#') + line = line.strip() + if line: + existing_allowed.append(re.compile(line)) + + with tempfile.TemporaryDirectory() as d: p = pathlib.Path(d) / "allowlist.txt" with p.open("wt") as f: for path in mpl.glob("**/*.py"): - v = Visitor(path, f) + v = Visitor(path, f, existing_allowed) tree = ast.parse(path.read_text()) # Assign parents to tree so they can be backtraced