]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-104050: Run mypy on `clinic.py` in CI (#104421)
authorAlex Waygood <Alex.Waygood@Gmail.com>
Mon, 15 May 2023 08:49:28 +0000 (09:49 +0100)
committerGitHub <noreply@github.com>
Mon, 15 May 2023 08:49:28 +0000 (08:49 +0000)
* Add basic mypy workflow to CI
* Make the type check pass

---------

Co-authored-by: Erlend E. Aasland <erlend.aasland@protonmail.com>
Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
.github/dependabot.yml
.github/workflows/mypy.yml [new file with mode: 0644]
Tools/clinic/clinic.py
Tools/clinic/cpp.py
Tools/clinic/mypy.ini [new file with mode: 0644]
Tools/clinic/requirements-dev.txt [new file with mode: 0644]

index 555e246e402bf93c9befe6adbc61d9cc195a48cb..f026b0f5f9454aec25786d3876e66df31c99fdf7 100644 (file)
@@ -12,3 +12,10 @@ updates:
         update-types:
           - "version-update:semver-minor"
           - "version-update:semver-patch"
+  - package-ecosystem: "pip"
+    directory: "/Tools/clinic/"
+    schedule:
+      interval: "monthly"
+    labels:
+      - "skip issue"
+      - "skip news"
diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml
new file mode 100644 (file)
index 0000000..1315bb5
--- /dev/null
@@ -0,0 +1,39 @@
+# Workflow to run mypy on select parts of the CPython repo
+name: mypy
+
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    paths:
+      - "Tools/clinic/**"
+      - ".github/workflows/mypy.yml"
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+env:
+  PIP_DISABLE_PIP_VERSION_CHECK: 1
+  FORCE_COLOR: 1
+  TERM: xterm-256color  # needed for FORCE_COLOR to work on mypy on Ubuntu, see https://github.com/python/mypy/issues/13817
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
+jobs:
+  mypy:
+    name: Run mypy on Tools/clinic/
+    runs-on: ubuntu-latest
+    timeout-minutes: 10
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          python-version: "3.x"
+          cache: pip
+          cache-dependency-path: Tools/clinic/requirements-dev.txt
+      - run: pip install -r Tools/clinic/requirements-dev.txt
+      - run: mypy --config-file Tools/clinic/mypy.ini
index 19c4cd299f0bbb38c64dee5213d99775564eba14..4270fb3cc566133fbe14cbdd68a2d7af14bd89fc 100755 (executable)
@@ -7,6 +7,7 @@
 
 import abc
 import ast
+import builtins as bltns
 import collections
 import contextlib
 import copy
@@ -26,7 +27,9 @@ import textwrap
 import traceback
 import types
 
+from collections.abc import Callable
 from types import *
+from typing import Any, NamedTuple
 
 # TODO:
 #
@@ -78,8 +81,13 @@ unknown = Unknown()
 
 sig_end_marker = '--'
 
+Appender = Callable[[str], None]
+Outputter = Callable[[None], str]
 
-_text_accumulator_nt = collections.namedtuple("_text_accumulator", "text append output")
+class _TextAccumulator(NamedTuple):
+    text: list[str]
+    append: Appender
+    output: Outputter
 
 def _text_accumulator():
     text = []
@@ -87,10 +95,12 @@ def _text_accumulator():
         s = ''.join(text)
         text.clear()
         return s
-    return _text_accumulator_nt(text, text.append, output)
+    return _TextAccumulator(text, text.append, output)
 
 
-text_accumulator_nt = collections.namedtuple("text_accumulator", "text append")
+class TextAccumulator(NamedTuple):
+    text: list[str]
+    append: Appender
 
 def text_accumulator():
     """
@@ -104,7 +114,7 @@ def text_accumulator():
        empties the accumulator.
     """
     text, append, output = _text_accumulator()
-    return text_accumulator_nt(append, output)
+    return TextAccumulator(append, output)
 
 
 def warn_or_fail(fail=False, *args, filename=None, line_number=None):
@@ -1925,8 +1935,10 @@ class Destination:
 # maps strings to Language objects.
 # "languages" maps the name of the language ("C", "Python").
 # "extensions" maps the file extension ("c", "py").
+LangDict = dict[str, Callable[[str], Language]]
+
 languages = { 'C': CLanguage, 'Python': PythonLanguage }
-extensions = { name: CLanguage for name in "c cc cpp cxx h hh hpp hxx".split() }
+extensions: LangDict = { name: CLanguage for name in "c cc cpp cxx h hh hpp hxx".split() }
 extensions['py'] = PythonLanguage
 
 
@@ -2558,15 +2570,15 @@ class CConverter(metaclass=CConverterAutoRegister):
     """
 
     # The C name to use for this variable.
-    name = None
+    name: str | None = None
 
     # The Python name to use for this variable.
-    py_name = None
+    py_name: str | None = None
 
     # The C type to use for this variable.
     # 'type' should be a Python string specifying the type, e.g. "int".
     # If this is a pointer type, the type string should end with ' *'.
-    type = None
+    type: str | None = None
 
     # The Python default value for this parameter, as a Python value.
     # Or the magic value "unspecified" if there is no default.
@@ -2577,15 +2589,15 @@ class CConverter(metaclass=CConverterAutoRegister):
 
     # If not None, default must be isinstance() of this type.
     # (You can also specify a tuple of types.)
-    default_type = None
+    default_type: bltns.type[Any] | tuple[bltns.type[Any], ...] | None = None
 
     # "default" converted into a C value, as a string.
     # Or None if there is no default.
-    c_default = None
+    c_default: str | None = None
 
     # "default" converted into a Python value, as a string.
     # Or None if there is no default.
-    py_default = None
+    py_default: str | None = None
 
     # The default value used to initialize the C variable when
     # there is no default, but not specifying a default may
@@ -2597,14 +2609,14 @@ class CConverter(metaclass=CConverterAutoRegister):
     #
     # This value is specified as a string.
     # Every non-abstract subclass should supply a valid value.
-    c_ignored_default = 'NULL'
+    c_ignored_default: str = 'NULL'
 
     # If true, wrap with Py_UNUSED.
     unused = False
 
     # The C converter *function* to be used, if any.
     # (If this is not None, format_unit must be 'O&'.)
-    converter = None
+    converter: str | None = None
 
     # Should Argument Clinic add a '&' before the name of
     # the variable when passing it into the _impl function?
@@ -3432,7 +3444,7 @@ class robuffer: pass
 def str_converter_key(types, encoding, zeroes):
     return (frozenset(types), bool(encoding), bool(zeroes))
 
-str_converter_argument_map = {}
+str_converter_argument_map: dict[str, str] = {}
 
 class str_converter(CConverter):
     type = 'const char *'
index 77f5f9696a6d848f92558b7210e2b4593a96f4fb..bc2cc713aac39405060e0bc541af8dd55fea8336 100644 (file)
@@ -1,7 +1,12 @@
 import re
 import sys
+from collections.abc import Callable
 
-def negate(condition):
+
+TokenAndCondition = tuple[str, str]
+TokenStack = list[TokenAndCondition]
+
+def negate(condition: str) -> str:
     """
     Returns a CPP conditional that is the opposite of the conditional passed in.
     """
@@ -22,17 +27,18 @@ class Monitor:
     Anyway this implementation seems to work well enough for the CPython sources.
     """
 
+    is_a_simple_defined: Callable[[str], re.Match[str] | None]
     is_a_simple_defined = re.compile(r'^defined\s*\(\s*[A-Za-z0-9_]+\s*\)$').match
 
-    def __init__(self, filename=None, *, verbose=False):
-        self.stack = []
+    def __init__(self, filename=None, *, verbose: bool = False):
+        self.stack: TokenStack = []
         self.in_comment = False
-        self.continuation = None
+        self.continuation: str | None = None
         self.line_number = 0
         self.filename = filename
         self.verbose = verbose
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return ''.join((
             '<Monitor ',
             str(id(self)),
@@ -40,10 +46,10 @@ class Monitor:
             " condition=", repr(self.condition()),
             ">"))
 
-    def status(self):
+    def status(self) -> str:
         return str(self.line_number).rjust(4) + ": " + self.condition()
 
-    def condition(self):
+    def condition(self) -> str:
         """
         Returns the current preprocessor state, as a single #if condition.
         """
@@ -62,15 +68,15 @@ class Monitor:
         if self.stack:
             self.fail("Ended file while still in a preprocessor conditional block!")
 
-    def write(self, s):
+    def write(self, s: str) -> None:
         for line in s.split("\n"):
             self.writeline(line)
 
-    def writeline(self, line):
+    def writeline(self, line: str) -> None:
         self.line_number += 1
         line = line.strip()
 
-        def pop_stack():
+        def pop_stack() -> TokenAndCondition:
             if not self.stack:
                 self.fail("#" + token + " without matching #if / #ifdef / #ifndef!")
             return self.stack.pop()
diff --git a/Tools/clinic/mypy.ini b/Tools/clinic/mypy.ini
new file mode 100644 (file)
index 0000000..3c5643e
--- /dev/null
@@ -0,0 +1,11 @@
+[mypy]
+# make sure clinic can still be run on Python 3.10
+python_version = 3.10
+pretty = True
+enable_error_code = ignore-without-code
+disallow_any_generics = True
+strict_concatenate = True
+warn_redundant_casts = True
+warn_unused_ignores = True
+warn_unused_configs = True
+files = Tools/clinic/
diff --git a/Tools/clinic/requirements-dev.txt b/Tools/clinic/requirements-dev.txt
new file mode 100644 (file)
index 0000000..7e0aa37
--- /dev/null
@@ -0,0 +1,2 @@
+# Requirements file for external linters and checks we run on Tools/clinic/ in CI
+mypy==1.2.0