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@protonmail.com>
Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
This commit is contained in:
Alex Waygood 2023-05-15 09:49:28 +01:00 committed by GitHub
parent a6bcc8fb92
commit 9d41f83c58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 101 additions and 24 deletions

View file

@ -12,3 +12,10 @@ updates:
update-types: update-types:
- "version-update:semver-minor" - "version-update:semver-minor"
- "version-update:semver-patch" - "version-update:semver-patch"
- package-ecosystem: "pip"
directory: "/Tools/clinic/"
schedule:
interval: "monthly"
labels:
- "skip issue"
- "skip news"

39
.github/workflows/mypy.yml vendored Normal file
View file

@ -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

View file

@ -7,6 +7,7 @@
import abc import abc
import ast import ast
import builtins as bltns
import collections import collections
import contextlib import contextlib
import copy import copy
@ -26,7 +27,9 @@ import textwrap
import traceback import traceback
import types import types
from collections.abc import Callable
from types import * from types import *
from typing import Any, NamedTuple
# TODO: # TODO:
# #
@ -78,8 +81,13 @@ unknown = Unknown()
sig_end_marker = '--' 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(): def _text_accumulator():
text = [] text = []
@ -87,10 +95,12 @@ def _text_accumulator():
s = ''.join(text) s = ''.join(text)
text.clear() text.clear()
return s 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(): def text_accumulator():
""" """
@ -104,7 +114,7 @@ def text_accumulator():
empties the accumulator. empties the accumulator.
""" """
text, append, output = _text_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): def warn_or_fail(fail=False, *args, filename=None, line_number=None):
@ -1925,8 +1935,10 @@ class Destination:
# maps strings to Language objects. # maps strings to Language objects.
# "languages" maps the name of the language ("C", "Python"). # "languages" maps the name of the language ("C", "Python").
# "extensions" maps the file extension ("c", "py"). # "extensions" maps the file extension ("c", "py").
LangDict = dict[str, Callable[[str], Language]]
languages = { 'C': CLanguage, 'Python': PythonLanguage } 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 extensions['py'] = PythonLanguage
@ -2558,15 +2570,15 @@ class CConverter(metaclass=CConverterAutoRegister):
""" """
# The C name to use for this variable. # The C name to use for this variable.
name = None name: str | None = None
# The Python name to use for this variable. # The Python name to use for this variable.
py_name = None py_name: str | None = None
# The C type to use for this variable. # The C type to use for this variable.
# 'type' should be a Python string specifying the type, e.g. "int". # 'type' should be a Python string specifying the type, e.g. "int".
# If this is a pointer type, the type string should end with ' *'. # 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. # The Python default value for this parameter, as a Python value.
# Or the magic value "unspecified" if there is no default. # 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. # If not None, default must be isinstance() of this type.
# (You can also specify a tuple of types.) # (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. # "default" converted into a C value, as a string.
# Or None if there is no default. # Or None if there is no default.
c_default = None c_default: str | None = None
# "default" converted into a Python value, as a string. # "default" converted into a Python value, as a string.
# Or None if there is no default. # 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 # The default value used to initialize the C variable when
# there is no default, but not specifying a default may # 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. # This value is specified as a string.
# Every non-abstract subclass should supply a valid value. # Every non-abstract subclass should supply a valid value.
c_ignored_default = 'NULL' c_ignored_default: str = 'NULL'
# If true, wrap with Py_UNUSED. # If true, wrap with Py_UNUSED.
unused = False unused = False
# The C converter *function* to be used, if any. # The C converter *function* to be used, if any.
# (If this is not None, format_unit must be 'O&'.) # (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 # Should Argument Clinic add a '&' before the name of
# the variable when passing it into the _impl function? # the variable when passing it into the _impl function?
@ -3432,7 +3444,7 @@ class robuffer: pass
def str_converter_key(types, encoding, zeroes): def str_converter_key(types, encoding, zeroes):
return (frozenset(types), bool(encoding), bool(zeroes)) return (frozenset(types), bool(encoding), bool(zeroes))
str_converter_argument_map = {} str_converter_argument_map: dict[str, str] = {}
class str_converter(CConverter): class str_converter(CConverter):
type = 'const char *' type = 'const char *'

View file

@ -1,7 +1,12 @@
import re import re
import sys 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. 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. 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 is_a_simple_defined = re.compile(r'^defined\s*\(\s*[A-Za-z0-9_]+\s*\)$').match
def __init__(self, filename=None, *, verbose=False): def __init__(self, filename=None, *, verbose: bool = False):
self.stack = [] self.stack: TokenStack = []
self.in_comment = False self.in_comment = False
self.continuation = None self.continuation: str | None = None
self.line_number = 0 self.line_number = 0
self.filename = filename self.filename = filename
self.verbose = verbose self.verbose = verbose
def __repr__(self): def __repr__(self) -> str:
return ''.join(( return ''.join((
'<Monitor ', '<Monitor ',
str(id(self)), str(id(self)),
@ -40,10 +46,10 @@ class Monitor:
" condition=", repr(self.condition()), " condition=", repr(self.condition()),
">")) ">"))
def status(self): def status(self) -> str:
return str(self.line_number).rjust(4) + ": " + self.condition() 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. Returns the current preprocessor state, as a single #if condition.
""" """
@ -62,15 +68,15 @@ class Monitor:
if self.stack: if self.stack:
self.fail("Ended file while still in a preprocessor conditional block!") 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"): for line in s.split("\n"):
self.writeline(line) self.writeline(line)
def writeline(self, line): def writeline(self, line: str) -> None:
self.line_number += 1 self.line_number += 1
line = line.strip() line = line.strip()
def pop_stack(): def pop_stack() -> TokenAndCondition:
if not self.stack: if not self.stack:
self.fail("#" + token + " without matching #if / #ifdef / #ifndef!") self.fail("#" + token + " without matching #if / #ifdef / #ifndef!")
return self.stack.pop() return self.stack.pop()

11
Tools/clinic/mypy.ini Normal file
View file

@ -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/

View file

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