Skip to content

Commit 9d41f83

Browse files
AlexWaygooderlend-aaslandsobolevnhugovk
authored
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 <[email protected]> Co-authored-by: Nikita Sobolev <[email protected]> Co-authored-by: Hugo van Kemenade <[email protected]>
1 parent a6bcc8f commit 9d41f83

File tree

6 files changed

+101
-24
lines changed

6 files changed

+101
-24
lines changed

.github/dependabot.yml

+7
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,10 @@ updates:
1212
update-types:
1313
- "version-update:semver-minor"
1414
- "version-update:semver-patch"
15+
- package-ecosystem: "pip"
16+
directory: "/Tools/clinic/"
17+
schedule:
18+
interval: "monthly"
19+
labels:
20+
- "skip issue"
21+
- "skip news"

.github/workflows/mypy.yml

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Workflow to run mypy on select parts of the CPython repo
2+
name: mypy
3+
4+
on:
5+
push:
6+
branches:
7+
- main
8+
pull_request:
9+
paths:
10+
- "Tools/clinic/**"
11+
- ".github/workflows/mypy.yml"
12+
workflow_dispatch:
13+
14+
permissions:
15+
contents: read
16+
17+
env:
18+
PIP_DISABLE_PIP_VERSION_CHECK: 1
19+
FORCE_COLOR: 1
20+
TERM: xterm-256color # needed for FORCE_COLOR to work on mypy on Ubuntu, see https://github.com/python/mypy/issues/13817
21+
22+
concurrency:
23+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
24+
cancel-in-progress: true
25+
26+
jobs:
27+
mypy:
28+
name: Run mypy on Tools/clinic/
29+
runs-on: ubuntu-latest
30+
timeout-minutes: 10
31+
steps:
32+
- uses: actions/checkout@v3
33+
- uses: actions/setup-python@v4
34+
with:
35+
python-version: "3.x"
36+
cache: pip
37+
cache-dependency-path: Tools/clinic/requirements-dev.txt
38+
- run: pip install -r Tools/clinic/requirements-dev.txt
39+
- run: mypy --config-file Tools/clinic/mypy.ini

Tools/clinic/clinic.py

+26-14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import abc
99
import ast
10+
import builtins as bltns
1011
import collections
1112
import contextlib
1213
import copy
@@ -26,7 +27,9 @@
2627
import traceback
2728
import types
2829

30+
from collections.abc import Callable
2931
from types import *
32+
from typing import Any, NamedTuple
3033

3134
# TODO:
3235
#
@@ -78,19 +81,26 @@ def __repr__(self):
7881

7982
sig_end_marker = '--'
8083

84+
Appender = Callable[[str], None]
85+
Outputter = Callable[[None], str]
8186

82-
_text_accumulator_nt = collections.namedtuple("_text_accumulator", "text append output")
87+
class _TextAccumulator(NamedTuple):
88+
text: list[str]
89+
append: Appender
90+
output: Outputter
8391

8492
def _text_accumulator():
8593
text = []
8694
def output():
8795
s = ''.join(text)
8896
text.clear()
8997
return s
90-
return _text_accumulator_nt(text, text.append, output)
98+
return _TextAccumulator(text, text.append, output)
9199

92100

93-
text_accumulator_nt = collections.namedtuple("text_accumulator", "text append")
101+
class TextAccumulator(NamedTuple):
102+
text: list[str]
103+
append: Appender
94104

95105
def text_accumulator():
96106
"""
@@ -104,7 +114,7 @@ def text_accumulator():
104114
empties the accumulator.
105115
"""
106116
text, append, output = _text_accumulator()
107-
return text_accumulator_nt(append, output)
117+
return TextAccumulator(append, output)
108118

109119

110120
def warn_or_fail(fail=False, *args, filename=None, line_number=None):
@@ -1925,8 +1935,10 @@ def dump(self):
19251935
# maps strings to Language objects.
19261936
# "languages" maps the name of the language ("C", "Python").
19271937
# "extensions" maps the file extension ("c", "py").
1938+
LangDict = dict[str, Callable[[str], Language]]
1939+
19281940
languages = { 'C': CLanguage, 'Python': PythonLanguage }
1929-
extensions = { name: CLanguage for name in "c cc cpp cxx h hh hpp hxx".split() }
1941+
extensions: LangDict = { name: CLanguage for name in "c cc cpp cxx h hh hpp hxx".split() }
19301942
extensions['py'] = PythonLanguage
19311943

19321944

@@ -2558,15 +2570,15 @@ class CConverter(metaclass=CConverterAutoRegister):
25582570
"""
25592571

25602572
# The C name to use for this variable.
2561-
name = None
2573+
name: str | None = None
25622574

25632575
# The Python name to use for this variable.
2564-
py_name = None
2576+
py_name: str | None = None
25652577

25662578
# The C type to use for this variable.
25672579
# 'type' should be a Python string specifying the type, e.g. "int".
25682580
# If this is a pointer type, the type string should end with ' *'.
2569-
type = None
2581+
type: str | None = None
25702582

25712583
# The Python default value for this parameter, as a Python value.
25722584
# Or the magic value "unspecified" if there is no default.
@@ -2577,15 +2589,15 @@ class CConverter(metaclass=CConverterAutoRegister):
25772589

25782590
# If not None, default must be isinstance() of this type.
25792591
# (You can also specify a tuple of types.)
2580-
default_type = None
2592+
default_type: bltns.type[Any] | tuple[bltns.type[Any], ...] | None = None
25812593

25822594
# "default" converted into a C value, as a string.
25832595
# Or None if there is no default.
2584-
c_default = None
2596+
c_default: str | None = None
25852597

25862598
# "default" converted into a Python value, as a string.
25872599
# Or None if there is no default.
2588-
py_default = None
2600+
py_default: str | None = None
25892601

25902602
# The default value used to initialize the C variable when
25912603
# there is no default, but not specifying a default may
@@ -2597,14 +2609,14 @@ class CConverter(metaclass=CConverterAutoRegister):
25972609
#
25982610
# This value is specified as a string.
25992611
# Every non-abstract subclass should supply a valid value.
2600-
c_ignored_default = 'NULL'
2612+
c_ignored_default: str = 'NULL'
26012613

26022614
# If true, wrap with Py_UNUSED.
26032615
unused = False
26042616

26052617
# The C converter *function* to be used, if any.
26062618
# (If this is not None, format_unit must be 'O&'.)
2607-
converter = None
2619+
converter: str | None = None
26082620

26092621
# Should Argument Clinic add a '&' before the name of
26102622
# the variable when passing it into the _impl function?
@@ -3432,7 +3444,7 @@ class robuffer: pass
34323444
def str_converter_key(types, encoding, zeroes):
34333445
return (frozenset(types), bool(encoding), bool(zeroes))
34343446

3435-
str_converter_argument_map = {}
3447+
str_converter_argument_map: dict[str, str] = {}
34363448

34373449
class str_converter(CConverter):
34383450
type = 'const char *'

Tools/clinic/cpp.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import re
22
import sys
3+
from collections.abc import Callable
34

4-
def negate(condition):
5+
6+
TokenAndCondition = tuple[str, str]
7+
TokenStack = list[TokenAndCondition]
8+
9+
def negate(condition: str) -> str:
510
"""
611
Returns a CPP conditional that is the opposite of the conditional passed in.
712
"""
@@ -22,28 +27,29 @@ class Monitor:
2227
Anyway this implementation seems to work well enough for the CPython sources.
2328
"""
2429

30+
is_a_simple_defined: Callable[[str], re.Match[str] | None]
2531
is_a_simple_defined = re.compile(r'^defined\s*\(\s*[A-Za-z0-9_]+\s*\)$').match
2632

27-
def __init__(self, filename=None, *, verbose=False):
28-
self.stack = []
33+
def __init__(self, filename=None, *, verbose: bool = False):
34+
self.stack: TokenStack = []
2935
self.in_comment = False
30-
self.continuation = None
36+
self.continuation: str | None = None
3137
self.line_number = 0
3238
self.filename = filename
3339
self.verbose = verbose
3440

35-
def __repr__(self):
41+
def __repr__(self) -> str:
3642
return ''.join((
3743
'<Monitor ',
3844
str(id(self)),
3945
" line=", str(self.line_number),
4046
" condition=", repr(self.condition()),
4147
">"))
4248

43-
def status(self):
49+
def status(self) -> str:
4450
return str(self.line_number).rjust(4) + ": " + self.condition()
4551

46-
def condition(self):
52+
def condition(self) -> str:
4753
"""
4854
Returns the current preprocessor state, as a single #if condition.
4955
"""
@@ -62,15 +68,15 @@ def close(self):
6268
if self.stack:
6369
self.fail("Ended file while still in a preprocessor conditional block!")
6470

65-
def write(self, s):
71+
def write(self, s: str) -> None:
6672
for line in s.split("\n"):
6773
self.writeline(line)
6874

69-
def writeline(self, line):
75+
def writeline(self, line: str) -> None:
7076
self.line_number += 1
7177
line = line.strip()
7278

73-
def pop_stack():
79+
def pop_stack() -> TokenAndCondition:
7480
if not self.stack:
7581
self.fail("#" + token + " without matching #if / #ifdef / #ifndef!")
7682
return self.stack.pop()

Tools/clinic/mypy.ini

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[mypy]
2+
# make sure clinic can still be run on Python 3.10
3+
python_version = 3.10
4+
pretty = True
5+
enable_error_code = ignore-without-code
6+
disallow_any_generics = True
7+
strict_concatenate = True
8+
warn_redundant_casts = True
9+
warn_unused_ignores = True
10+
warn_unused_configs = True
11+
files = Tools/clinic/

Tools/clinic/requirements-dev.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Requirements file for external linters and checks we run on Tools/clinic/ in CI
2+
mypy==1.2.0

0 commit comments

Comments
 (0)