[Python-checkins] gh-104050: Run mypy on `clinic.py` in CI (#104421)
AlexWaygood
webhook-mailer at python.org
Mon May 15 04:49:35 EDT 2023
https://github.com/python/cpython/commit/9d41f83c58e6dc2fc6eb4b91f803551850b0adeb
commit: 9d41f83c58e6dc2fc6eb4b91f803551850b0adeb
branch: main
author: Alex Waygood <Alex.Waygood at Gmail.com>
committer: AlexWaygood <Alex.Waygood at Gmail.com>
date: 2023-05-15T08:49:28Z
summary:
gh-104050: Run mypy on `clinic.py` in CI (#104421)
* Add basic mypy workflow to CI
* Make the type check pass
---------
Co-authored-by: Erlend E. Aasland <erlend.aasland at protonmail.com>
Co-authored-by: Nikita Sobolev <mail at sobolevn.me>
Co-authored-by: Hugo van Kemenade <hugovk at users.noreply.github.com>
files:
A .github/workflows/mypy.yml
A Tools/clinic/mypy.ini
A Tools/clinic/requirements-dev.txt
M .github/dependabot.yml
M Tools/clinic/clinic.py
M Tools/clinic/cpp.py
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 555e246e402b..f026b0f5f945 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -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
index 000000000000..1315bb5a966f
--- /dev/null
+++ b/.github/workflows/mypy.yml
@@ -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 at v3
+ - uses: actions/setup-python at 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
diff --git a/Tools/clinic/clinic.py b/Tools/clinic/clinic.py
index 19c4cd299f0b..4270fb3cc566 100755
--- a/Tools/clinic/clinic.py
+++ b/Tools/clinic/clinic.py
@@ -7,6 +7,7 @@
import abc
import ast
+import builtins as bltns
import collections
import contextlib
import copy
@@ -26,7 +27,9 @@
import traceback
import types
+from collections.abc import Callable
from types import *
+from typing import Any, NamedTuple
# TODO:
#
@@ -78,8 +81,13 @@ def __repr__(self):
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 output():
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 @@ def dump(self):
# 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 *'
diff --git a/Tools/clinic/cpp.py b/Tools/clinic/cpp.py
index 77f5f9696a6d..bc2cc713aac3 100644
--- a/Tools/clinic/cpp.py
+++ b/Tools/clinic/cpp.py
@@ -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 @@ def __repr__(self):
" 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 @@ def close(self):
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
index 000000000000..3c5643e789be
--- /dev/null
+++ b/Tools/clinic/mypy.ini
@@ -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
index 000000000000..7e0aa37a46d7
--- /dev/null
+++ b/Tools/clinic/requirements-dev.txt
@@ -0,0 +1,2 @@
+# Requirements file for external linters and checks we run on Tools/clinic/ in CI
+mypy==1.2.0
More information about the Python-checkins
mailing list