diff --git a/.github/workflows/mypy-stubtest.yml b/.github/workflows/mypy-stubtest.yml index 6da6f607642c..4cb225258f4f 100644 --- a/.github/workflows/mypy-stubtest.yml +++ b/.github/workflows/mypy-stubtest.yml @@ -37,10 +37,7 @@ jobs: REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -o pipefail - MPLBACKEND=agg python -m mypy.stubtest \ - --mypy-config-file pyproject.toml \ - --allowlist ci/mypy-stubtest-allowlist.txt \ - matplotlib | \ + MPLBACKEND=agg python tools/stubtest.py | \ reviewdog \ -efm '%Eerror: %m' \ -efm '%CStub: in file %f:%l' \ diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 6b1959d49b0e..e9256de10b36 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -42,99 +42,14 @@ matplotlib.cm.register_cmap matplotlib.cm.unregister_cmap # 3.8 deprecations -matplotlib.cbook.get_sample_data matplotlib.contour.ContourSet.allkinds matplotlib.contour.ContourSet.allsegs matplotlib.contour.ContourSet.tcolors matplotlib.contour.ContourSet.tlinewidths -matplotlib.ticker.LogLocator.__init__ -matplotlib.ticker.LogLocator.set_params # positional-only argument name lacking leading underscores matplotlib.axes._base._AxesBase.axis -# Aliases (dynamically generated, not type hinted) -matplotlib.collections.Collection.get_aa -matplotlib.collections.Collection.get_antialiaseds -matplotlib.collections.Collection.get_dashes -matplotlib.collections.Collection.get_ec -matplotlib.collections.Collection.get_edgecolors -matplotlib.collections.Collection.get_facecolors -matplotlib.collections.Collection.get_fc -matplotlib.collections.Collection.get_linestyles -matplotlib.collections.Collection.get_linewidths -matplotlib.collections.Collection.get_ls -matplotlib.collections.Collection.get_lw -matplotlib.collections.Collection.get_transOffset -matplotlib.collections.Collection.set_aa -matplotlib.collections.Collection.set_antialiaseds -matplotlib.collections.Collection.set_dashes -matplotlib.collections.Collection.set_ec -matplotlib.collections.Collection.set_edgecolors -matplotlib.collections.Collection.set_facecolors -matplotlib.collections.Collection.set_fc -matplotlib.collections.Collection.set_linestyles -matplotlib.collections.Collection.set_linewidths -matplotlib.collections.Collection.set_ls -matplotlib.collections.Collection.set_lw -matplotlib.collections.Collection.set_transOffset -matplotlib.lines.Line2D.get_aa -matplotlib.lines.Line2D.get_c -matplotlib.lines.Line2D.get_ds -matplotlib.lines.Line2D.get_ls -matplotlib.lines.Line2D.get_lw -matplotlib.lines.Line2D.get_mec -matplotlib.lines.Line2D.get_mew -matplotlib.lines.Line2D.get_mfc -matplotlib.lines.Line2D.get_mfcalt -matplotlib.lines.Line2D.get_ms -matplotlib.lines.Line2D.set_aa -matplotlib.lines.Line2D.set_c -matplotlib.lines.Line2D.set_ds -matplotlib.lines.Line2D.set_ls -matplotlib.lines.Line2D.set_lw -matplotlib.lines.Line2D.set_mec -matplotlib.lines.Line2D.set_mew -matplotlib.lines.Line2D.set_mfc -matplotlib.lines.Line2D.set_mfcalt -matplotlib.lines.Line2D.set_ms -matplotlib.patches.Patch.get_aa -matplotlib.patches.Patch.get_ec -matplotlib.patches.Patch.get_fc -matplotlib.patches.Patch.get_ls -matplotlib.patches.Patch.get_lw -matplotlib.patches.Patch.set_aa -matplotlib.patches.Patch.set_ec -matplotlib.patches.Patch.set_fc -matplotlib.patches.Patch.set_ls -matplotlib.patches.Patch.set_lw -matplotlib.text.Text.get_c -matplotlib.text.Text.get_family -matplotlib.text.Text.get_font -matplotlib.text.Text.get_font_properties -matplotlib.text.Text.get_ha -matplotlib.text.Text.get_name -matplotlib.text.Text.get_size -matplotlib.text.Text.get_style -matplotlib.text.Text.get_va -matplotlib.text.Text.get_variant -matplotlib.text.Text.get_weight -matplotlib.text.Text.set_c -matplotlib.text.Text.set_family -matplotlib.text.Text.set_font -matplotlib.text.Text.set_font_properties -matplotlib.text.Text.set_ha -matplotlib.text.Text.set_ma -matplotlib.text.Text.set_name -matplotlib.text.Text.set_size -matplotlib.text.Text.set_stretch -matplotlib.text.Text.set_style -matplotlib.text.Text.set_va -matplotlib.text.Text.set_variant -matplotlib.text.Text.set_weight -matplotlib.axes._base._AxesBase.get_fc -matplotlib.axes._base._AxesBase.set_fc - # Maybe should be abstractmethods, required for subclasses, stubs define once matplotlib.tri.*TriInterpolator.__call__ matplotlib.tri.*TriInterpolator.gradient diff --git a/doc/devel/contribute.rst b/doc/devel/contribute.rst index 9b53a80ab374..85c061b7ce8f 100644 --- a/doc/devel/contribute.rst +++ b/doc/devel/contribute.rst @@ -425,11 +425,7 @@ Introducing updated on introduction. - Items decorated with ``@_api.delete_parameter`` should include a default value hint for the deleted parameter, even if it did not previously have one (e.g. - ``param: = ...``). Even so, the decorator changes the default value to a - sentinel value which should not be included in the type stub. Thus, Mypy Stubtest - needs to be informed of the inconsistency by placing the method into - :file:`ci/mypy-stubtest-allowlist.txt` under a heading indicating the deprecation - version number. + ``param: = ...``). Expiring ~~~~~~~~ @@ -452,11 +448,11 @@ Expiring will have been updated at introduction, and require no change now. - Items decorated with ``@_api.delete_parameter`` will need to be updated to the final signature, in the same way as the ``.py`` file signature is updated. - The entry in :file:`ci/mypy-stubtest-allowlist.txt` should be removed. - - Any other entries in :file:`ci/mypy-stubtest-allowlist.txt` under a version's - deprecations should be double checked, as only ``delete_parameter`` should normally - require that mechanism for deprecation. For removed items that were not in the stub - file, only deleting from the allowlist is required. + - Any entries in :file:`ci/mypy-stubtest-allowlist.txt` which indicate a deprecation + version should be double checked. In most cases this is not needed, though some + items were never type hinted in the first place and were added to this file + instead. For removed items that were not in the stub file, only deleting from the + allowlist is required. Adding new API -------------- diff --git a/tools/stubtest.py b/tools/stubtest.py new file mode 100644 index 000000000000..7c93d2dae157 --- /dev/null +++ b/tools/stubtest.py @@ -0,0 +1,84 @@ +import ast +import os +import pathlib +import subprocess +import sys +import tempfile + +root = pathlib.Path(__file__).parent.parent + +lib = root / "lib" +mpl = lib / "matplotlib" + + +class Visitor(ast.NodeVisitor): + def __init__(self, filepath, output): + self.filepath = filepath + self.context = list(filepath.with_suffix("").relative_to(lib).parts) + self.output = output + + def visit_FunctionDef(self, node): + if any("delete_parameter" in ast.unparse(line) for line in node.decorator_list): + parents = [] + if hasattr(node, "parent"): + parent = node.parent + while hasattr(parent, "parent") and not isinstance(parent, ast.Module): + parents.insert(0, parent.name) + parent = parent.parent + self.output.write(f"{'.'.join(self.context + parents)}.{node.name}\n") + + def visit_ClassDef(self, node): + for dec in node.decorator_list: + if "define_aliases" in ast.unparse(dec): + parents = [] + if hasattr(node, "parent"): + parent = node.parent + while hasattr(parent, "parent") and not isinstance( + parent, ast.Module + ): + parents.insert(0, parent.name) + parent = parent.parent + aliases = ast.literal_eval(dec.args[0]) + # Written as a regex rather than two lines to avoid unused entries + # 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 child in ast.iter_child_nodes(node): + self.visit(child) + + +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) + tree = ast.parse(path.read_text()) + + # Assign parents to tree so they can be backtraced + for node in ast.walk(tree): + for child in ast.iter_child_nodes(node): + child.parent = node + + v.visit(tree) + proc = subprocess.run( + [ + "stubtest", + "--mypy-config-file=pyproject.toml", + "--allowlist=ci/mypy-stubtest-allowlist.txt", + f"--allowlist={p}", + "matplotlib", + ], + cwd=root, + env=os.environ | {"MPLBACKEND": "agg"}, + ) + try: + os.unlink(f.name) + except OSError: + pass + +sys.exit(proc.returncode)