From ed56168dea739cf7af9b67214d907940cd44d11a Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 12 Mar 2025 12:29:21 -0400 Subject: [PATCH 1/6] Re-enable mypy and fix most issues --- distutils/archive_util.py | 21 ++++++++++++------- distutils/cmd.py | 13 +++++++----- distutils/command/install_lib.py | 2 +- distutils/compilers/C/base.py | 10 ++++----- distutils/dir_util.py | 9 ++++---- distutils/dist.py | 24 ++++++++++------------ distutils/filelist.py | 10 ++++----- distutils/sysconfig.py | 23 +++++++++++---------- distutils/tests/test_build_ext.py | 2 +- distutils/tests/test_check.py | 15 +------------- distutils/tests/unix_compat.py | 7 ++++++- distutils/util.py | 21 +++++++------------ mypy.ini | 34 +++++++++++++++++++++++++++++++ pyproject.toml | 7 +++---- 14 files changed, 113 insertions(+), 85 deletions(-) diff --git a/distutils/archive_util.py b/distutils/archive_util.py index d860f552..6da341c7 100644 --- a/distutils/archive_util.py +++ b/distutils/archive_util.py @@ -6,19 +6,23 @@ from __future__ import annotations import os +from types import ModuleType from typing import Literal, overload -try: - import zipfile -except ImportError: - zipfile = None - - from ._log import log from .dir_util import mkpath from .errors import DistutilsExecError from .spawn import spawn +zipfile: ModuleType | None = None +try: + import zipfile +except ImportError: + pass + +# mypy: disable-error-code="attr-defined" +# We have to be more flexible than simply checking for `sys.platform != "win32"` +# https://github.com/python/mypy/issues/1393 try: from pwd import getpwnam except ImportError: @@ -116,7 +120,10 @@ def _set_uid_gid(tarinfo): return tarinfo if not dry_run: - tar = tarfile.open(archive_name, f'w|{tar_compression[compress]}') + tar = tarfile.open( + archive_name, + f'w|{tar_compression[compress]}', # type: ignore[call-overload] # typeshed only exposes literal open modes + ) try: tar.add(base_dir, filter=_set_uid_gid) finally: diff --git a/distutils/cmd.py b/distutils/cmd.py index 241621bd..f90c13d8 100644 --- a/distutils/cmd.py +++ b/distutils/cmd.py @@ -12,7 +12,7 @@ import sys from abc import abstractmethod from collections.abc import Callable, MutableSequence -from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, overload +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast, overload from . import _modified, archive_util, dir_util, file_util, util from ._log import log @@ -70,6 +70,8 @@ class Command: list[tuple[str, str, str]] | list[tuple[str, str | None, str]] ] = [] + description = "" + # -- Creation/initialization methods ------------------------------- def __init__(self, dist: Distribution) -> None: @@ -330,7 +332,8 @@ def get_finalized_command(self, command: str, create: bool = True) -> Command: 'command', call its 'ensure_finalized()' method, and return the finalized command object. """ - cmd_obj = self.distribution.get_command_obj(command, create) + # TODO: Raise a more descriptive error when cmd_obj is None ? + cmd_obj = cast(Command, self.distribution.get_command_obj(command, create)) cmd_obj.ensure_finalized() return cmd_obj @@ -504,7 +507,7 @@ def make_archive( owner: str | None = None, group: str | None = None, ) -> str: - return archive_util.make_archive( + return archive_util.make_archive( # type: ignore[misc] # Mypy bailed out base_name, format, root_dir, @@ -533,7 +536,7 @@ def make_file( timestamp checks. """ if skip_msg is None: - skip_msg = f"skipping {outfile} (inputs unchanged)" + skip_msg = f"skipping {outfile!r} (inputs unchanged)" # Allow 'infiles' to be a single string if isinstance(infiles, str): @@ -542,7 +545,7 @@ def make_file( raise TypeError("'infiles' must be a string, or a list or tuple of strings") if exec_msg is None: - exec_msg = "generating {} from {}".format(outfile, ', '.join(infiles)) + exec_msg = f"generating {outfile!r} from {', '.join(infiles)}" # If 'outfile' must be regenerated (either because it doesn't # exist, is out-of-date, or the 'force' flag is true) then diff --git a/distutils/command/install_lib.py b/distutils/command/install_lib.py index 2aababf8..d7d0c51f 100644 --- a/distutils/command/install_lib.py +++ b/distutils/command/install_lib.py @@ -120,7 +120,7 @@ def install(self) -> list[str] | Any: self.warn( f"'{self.build_dir}' does not exist -- no Python modules to install" ) - return + return None return outfiles def byte_compile(self, files) -> None: diff --git a/distutils/compilers/C/base.py b/distutils/compilers/C/base.py index 4767b7f3..30845f7a 100644 --- a/distutils/compilers/C/base.py +++ b/distutils/compilers/C/base.py @@ -120,12 +120,12 @@ class Compiler: } language_order: ClassVar[list[str]] = ["c++", "objc", "c"] - include_dirs = [] + include_dirs: list[str] = [] """ include dirs specific to this compiler class """ - library_dirs = [] + library_dirs: list[str] = [] """ library dirs specific to this compiler class """ @@ -148,14 +148,14 @@ def __init__( self.macros: list[_Macro] = [] # 'include_dirs': a list of directories to search for include files - self.include_dirs: list[str] = [] + self.include_dirs = [] # 'libraries': a list of libraries to include in any link # (library names, not filenames: eg. "foo" not "libfoo.a") self.libraries: list[str] = [] # 'library_dirs': a list of directories to search for libraries - self.library_dirs: list[str] = [] + self.library_dirs = [] # 'runtime_library_dirs': a list of directories to search for # shared libraries/objects at runtime @@ -862,7 +862,7 @@ def library_dir_option(self, dir: str) -> str: """ raise NotImplementedError - def runtime_library_dir_option(self, dir: str) -> str: + def runtime_library_dir_option(self, dir: str) -> str | list[str]: """Return the compiler option to add 'dir' to the list of directories searched for runtime libraries. """ diff --git a/distutils/dir_util.py b/distutils/dir_util.py index d9782602..1d7b9db7 100644 --- a/distutils/dir_util.py +++ b/distutils/dir_util.py @@ -57,10 +57,11 @@ def mkpath(name: pathlib.Path, mode=0o777, verbose=True, dry_run=False) -> None: if verbose and not name.is_dir(): log.info("creating %s", name) - try: - dry_run or name.mkdir(mode=mode, parents=True, exist_ok=True) - except OSError as exc: - raise DistutilsFileError(f"could not create '{name}': {exc.args[-1]}") + if not dry_run: + try: + name.mkdir(mode=mode, parents=True, exist_ok=True) + except OSError as exc: + raise DistutilsFileError(f"could not create '{name}': {exc.args[-1]}") @mkpath.register diff --git a/distutils/dist.py b/distutils/dist.py index 69d42016..64da315e 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -183,7 +183,7 @@ def __init__(self, attrs: MutableMapping[str, Any] | None = None) -> None: # no # can 1) quickly figure out which class to instantiate when # we need to create a new command object, and 2) have a way # for the setup script to override command classes - self.cmdclass = {} + self.cmdclass: dict[str, type[Command]] = {} # 'command_packages' is a list of packages in which commands # are searched for. The factory for command 'foo' is expected @@ -575,11 +575,10 @@ def _parse_command_opts(self, parser, args): # noqa: C901 hasattr(cmd_class, 'user_options') and isinstance(cmd_class.user_options, list) ): - msg = ( - "command class %s must provide " + raise DistutilsClassError( + f"command class {cmd_class} must provide " "'user_options' attribute (a list of tuples)" ) - raise DistutilsClassError(msg % cmd_class) # If the command class has a list of negative alias options, # merge it in with the global negative aliases. @@ -864,9 +863,7 @@ def get_command_obj( self, command: str, create: Literal[True] = True ) -> Command: ... @overload - def get_command_obj( - self, command: str, create: Literal[False] - ) -> Command | None: ... + def get_command_obj(self, command: str, create: bool) -> Command | None: ... def get_command_obj(self, command: str, create: bool = True) -> Command | None: """Return the command object for 'command'. Normally this object is cached on a previous call to 'get_command_obj()'; if no command @@ -1027,22 +1024,22 @@ def has_pure_modules(self) -> bool: return len(self.packages or self.py_modules or []) > 0 def has_ext_modules(self) -> bool: - return self.ext_modules and len(self.ext_modules) > 0 + return bool(self.ext_modules and len(self.ext_modules) > 0) def has_c_libraries(self) -> bool: - return self.libraries and len(self.libraries) > 0 + return bool(self.libraries and len(self.libraries) > 0) def has_modules(self) -> bool: return self.has_pure_modules() or self.has_ext_modules() def has_headers(self) -> bool: - return self.headers and len(self.headers) > 0 + return bool(self.headers and len(self.headers) > 0) def has_scripts(self) -> bool: - return self.scripts and len(self.scripts) > 0 + return bool(self.scripts and len(self.scripts) > 0) def has_data_files(self) -> bool: - return self.data_files and len(self.data_files) > 0 + return bool(self.data_files and len(self.data_files) > 0) def is_pure(self) -> bool: return ( @@ -1168,6 +1165,7 @@ def _read_field(name: str) -> str | None: value = msg[name] if value and value != "UNKNOWN": return value + return None def _read_list(name): values = msg.get_all(name, None) @@ -1196,7 +1194,7 @@ def _read_list(name): self.description = _read_field('summary') if 'keywords' in msg: - self.keywords = _read_field('keywords').split(',') + self.keywords = _read_field('keywords').split(',') # type:ignore[union-attr] # Manually checked self.platforms = _read_list('platform') self.classifiers = _read_list('classifier') diff --git a/distutils/filelist.py b/distutils/filelist.py index 70dc0fde..b50d654a 100644 --- a/distutils/filelist.py +++ b/distutils/filelist.py @@ -200,7 +200,7 @@ def process_template_line(self, line: str) -> None: # noqa: C901 @overload def include_pattern( self, - pattern: str, + pattern: str | None, anchor: bool = True, prefix: str | None = None, is_regex: Literal[False] = False, @@ -224,7 +224,7 @@ def include_pattern( ) -> bool: ... def include_pattern( self, - pattern: str | re.Pattern, + pattern: str | re.Pattern | None, anchor: bool = True, prefix: str | None = None, is_regex: bool = False, @@ -262,7 +262,7 @@ def include_pattern( if self.allfiles is None: self.findall() - for name in self.allfiles: + for name in self.allfiles: # type: ignore[union-attr] # Calling findall fills allfiles if pattern_re.search(name): self.debug_print(" adding " + name) self.files.append(name) @@ -272,7 +272,7 @@ def include_pattern( @overload def exclude_pattern( self, - pattern: str, + pattern: str | None, anchor: bool = True, prefix: str | None = None, is_regex: Literal[False] = False, @@ -296,7 +296,7 @@ def exclude_pattern( ) -> bool: ... def exclude_pattern( self, - pattern: str | re.Pattern, + pattern: str | re.Pattern | None, anchor: bool = True, prefix: str | None = None, is_regex: bool = False, diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index e5facaec..61032d9c 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -246,14 +246,6 @@ def get_python_lib( sys.base_exec_prefix -- i.e., ignore 'plat_specific'. """ - if IS_PYPY and sys.version_info < (3, 8): - # PyPy-specific schema - if prefix is None: - prefix = PREFIX - if standard_lib: - return os.path.join(prefix, "lib-python", sys.version_info.major) - return os.path.join(prefix, 'site-packages') - early_prefix = prefix if prefix is None: @@ -334,6 +326,14 @@ def customize_compiler(compiler: CCompiler) -> None: 'AR', 'ARFLAGS', ) + assert isinstance(cc, str) + assert isinstance(cxx, str) + assert isinstance(cflags, str) + assert isinstance(ccshared, str) + assert isinstance(ldshared, str) + assert isinstance(ldcxxshared, str) + assert isinstance(ar_flags, str) + assert isinstance(shlib_suffix, str) cxxflags = cflags @@ -365,6 +365,7 @@ def customize_compiler(compiler: CCompiler) -> None: ldcxxshared = _add_flags(ldcxxshared, 'CPP') ar = os.environ.get('AR', ar) + assert isinstance(ar, str) archiver = ar + ' ' + os.environ.get('ARFLAGS', ar_flags) cc_cmd = cc + ' ' + cflags @@ -386,7 +387,7 @@ def customize_compiler(compiler: CCompiler) -> None: if 'RANLIB' in os.environ and compiler.executables.get('ranlib', None): compiler.set_executables(ranlib=os.environ['RANLIB']) - compiler.shared_lib_extension = shlib_suffix + compiler.shared_lib_extension = shlib_suffix # type: ignore[misc] # Assigning to ClassVar def get_config_h_filename() -> str: @@ -559,8 +560,8 @@ def expand_makefile_vars(s, vars): @overload def get_config_vars() -> dict[str, str | int]: ... @overload -def get_config_vars(arg: str, /, *args: str) -> list[str | int]: ... -def get_config_vars(*args: str) -> list[str | int] | dict[str, str | int]: +def get_config_vars(arg: str, /, *args: str) -> list[str | int | None]: ... +def get_config_vars(*args: str) -> list[str | int | None] | dict[str, str | int]: """With no arguments, return a dictionary of all configuration variables relevant for the current platform. Generally this includes everything needed to build extensions and install both pure modules and diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 4274890a..e5610743 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -113,7 +113,7 @@ def test_build_ext(self): @staticmethod def _test_xx(): - import xx + import xx # type: ignore[import-not-found] # Module generated for tests for attr in ('error', 'foo', 'new', 'roj'): assert hasattr(xx, attr) diff --git a/distutils/tests/test_check.py b/distutils/tests/test_check.py index b672b1f9..f2ab1b78 100644 --- a/distutils/tests/test_check.py +++ b/distutils/tests/test_check.py @@ -8,12 +8,6 @@ import pytest -try: - import pygments -except ImportError: - pygments = None - - HERE = os.path.dirname(__file__) @@ -180,14 +174,7 @@ def foo(): cmd = check(dist) cmd.check_restructuredtext() msgs = cmd._check_rst_data(rest_with_code) - if pygments is not None: - assert len(msgs) == 0 - else: - assert len(msgs) == 1 - assert ( - str(msgs[0][1]) - == 'Cannot analyze code. Pygments package not found.' - ) + assert len(msgs) == 0 def test_check_all(self): with pytest.raises(DistutilsSetupError): diff --git a/distutils/tests/unix_compat.py b/distutils/tests/unix_compat.py index a5d9ee45..88b5296a 100644 --- a/distutils/tests/unix_compat.py +++ b/distutils/tests/unix_compat.py @@ -1,10 +1,15 @@ +from __future__ import annotations + import sys +from types import ModuleType +grp: ModuleType | None = None +pwd: ModuleType | None = None try: import grp import pwd except ImportError: - grp = pwd = None + pass import pytest diff --git a/distutils/util.py b/distutils/util.py index 6dbe049f..763f588d 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -151,7 +151,9 @@ def change_root( if not os.path.isabs(pathname): return os.path.join(new_root, pathname) else: - return os.path.join(new_root, pathname[1:]) + # type-ignore: This makes absolutes os.Pathlike unsupported in this branch. + # Either this or we don't support bytes-based paths, or we complexify this branch. + return os.path.join(new_root, pathname[1:]) # type: ignore[index] elif os.name == 'nt': (drive, path) = os.path.splitdrive(pathname) @@ -232,14 +234,9 @@ def grok_environment_error(exc: object, prefix: str = "error: ") -> str: # Needed by 'split_quoted()' -_wordchars_re = _squote_re = _dquote_re = None - - -def _init_regex(): - global _wordchars_re, _squote_re, _dquote_re - _wordchars_re = re.compile(rf'[^\\\'\"{string.whitespace} ]*') - _squote_re = re.compile(r"'(?:[^'\\]|\\.)*'") - _dquote_re = re.compile(r'"(?:[^"\\]|\\.)*"') +_wordchars_re = re.compile(rf'[^\\\'\"{string.whitespace} ]*') +_squote_re = re.compile(r"'(?:[^'\\]|\\.)*'") +_dquote_re = re.compile(r'"(?:[^"\\]|\\.)*"') def split_quoted(s: str) -> list[str]: @@ -256,8 +253,6 @@ def split_quoted(s: str) -> list[str]: # This is a nice algorithm for splitting up a single string, since it # doesn't require character-by-character examination. It was a little # bit of a brain-bender to get it working right, though... - if _wordchars_re is None: - _init_regex() s = s.strip() words = [] @@ -265,6 +260,7 @@ def split_quoted(s: str) -> list[str]: while s: m = _wordchars_re.match(s, pos) + assert m is not None end = m.end() if end == len(s): words.append(s[:end]) @@ -305,9 +301,6 @@ def split_quoted(s: str) -> list[str]: return words -# split_quoted () - - def execute( func: Callable[[Unpack[_Ts]], object], args: tuple[Unpack[_Ts]], diff --git a/mypy.ini b/mypy.ini index efcb8cbc..cd2466a7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -13,3 +13,37 @@ explicit_package_bases = True disable_error_code = # Disable due to many false positives overload-overlap, +# local + + # TODO: Resolve and re-enable these gradually + operator, + attr-defined, + arg-type, + assignment, + call-overload, + return-value, + +exclude = (?x)( + # Exclude test folders and private modules for now + ^distutils/tests/ + ^distutils/_.+? + ) + +# stdlib's test module is not typed on typeshed +[mypy-test.*] +ignore_missing_imports = True + +# https://github.com/jaraco/jaraco.envs/issues/7 +# https://github.com/jaraco/jaraco.envs/pull/8 +[mypy-jaraco.envs.*] +ignore_missing_imports = True + +# https://github.com/jaraco/jaraco.path/issues/2 +# https://github.com/jaraco/jaraco.path/pull/7 +[mypy-jaraco.path.*] +ignore_missing_imports = True + +# https://github.com/jaraco/jaraco.text/issues/17 +# https://github.com/jaraco/jaraco.text/pull/23 +[mypy-jaraco.text.*] +ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index 9cbb3590..58d7dc57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,12 +77,11 @@ type = [ "pytest-mypy", # local + "types-docutils", ] [tool.setuptools_scm] - -[tool.pytest-enabler.mypy] -# Disabled due to jaraco/skeleton#143 -# Disabled as distutils isn't ready yet +[tool.pyright] +reportShadowedImports= "none" From eaabc0997d3ae7328a5f621a2da38a3410133ff4 Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 2 Apr 2025 23:35:22 -0400 Subject: [PATCH 2/6] FIx two typing issues --- distutils/archive_util.py | 5 +---- distutils/compilers/C/unix.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/distutils/archive_util.py b/distutils/archive_util.py index 6da341c7..10fe7299 100644 --- a/distutils/archive_util.py +++ b/distutils/archive_util.py @@ -120,10 +120,7 @@ def _set_uid_gid(tarinfo): return tarinfo if not dry_run: - tar = tarfile.open( - archive_name, - f'w|{tar_compression[compress]}', # type: ignore[call-overload] # typeshed only exposes literal open modes - ) + tar = tarfile.open(archive_name, f'w|{tar_compression[compress]}') try: tar.add(base_dir, filter=_set_uid_gid) finally: diff --git a/distutils/compilers/C/unix.py b/distutils/compilers/C/unix.py index e8a53d45..77ff4edc 100644 --- a/distutils/compilers/C/unix.py +++ b/distutils/compilers/C/unix.py @@ -323,7 +323,7 @@ def _is_gcc(self): compiler = os.path.basename(shlex.split(cc_var)[0]) return "gcc" in compiler or "g++" in compiler - def runtime_library_dir_option(self, dir: str) -> str | list[str]: # type: ignore[override] # Fixed in pypa/distutils#339 + def runtime_library_dir_option(self, dir: str) -> str | list[str]: # XXX Hackish, at the very least. See Python bug #445902: # https://bugs.python.org/issue445902 # Linkers on different platforms need different options to From 0712432e6e1a2cde0d2575be2529fda7cc606e4c Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 2 Apr 2025 23:44:13 -0400 Subject: [PATCH 3/6] Discard changes to pyproject.toml --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9bd77fd0..8df264b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,3 @@ type = [ [tool.setuptools_scm] - -[tool.pyright] -reportShadowedImports= "none" From 7e46ea7a28f477f9f9288b78c011d164c66251ba Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 4 May 2025 11:25:21 -0400 Subject: [PATCH 4/6] Fix all assignment --- distutils/_modified.py | 4 +++- distutils/archive_util.py | 2 +- distutils/cmd.py | 2 +- distutils/command/bdist_rpm.py | 2 +- distutils/command/build_py.py | 2 +- distutils/command/install.py | 4 ++-- distutils/dist.py | 3 ++- distutils/extension.py | 3 +-- distutils/fancy_getopt.py | 15 ++++++++------- mypy.ini | 12 ++++-------- 10 files changed, 24 insertions(+), 25 deletions(-) diff --git a/distutils/_modified.py b/distutils/_modified.py index f64cab7d..0e7494c1 100644 --- a/distutils/_modified.py +++ b/distutils/_modified.py @@ -7,7 +7,9 @@ from collections.abc import Callable, Iterable from typing import Literal, TypeVar -from jaraco.functools import splat +from jaraco.functools import ( + splat, # jaraco/jaraco.functools#30 +) from .compat.py39 import zip_strict from .errors import DistutilsFileError diff --git a/distutils/archive_util.py b/distutils/archive_util.py index 10fe7299..55730367 100644 --- a/distutils/archive_util.py +++ b/distutils/archive_util.py @@ -274,7 +274,7 @@ def make_archive( if base_dir is None: base_dir = os.curdir - kwargs = {'dry_run': dry_run} + kwargs: dict[str, str | bool | None] = {'dry_run': dry_run} try: format_info = ARCHIVE_FORMATS[format] diff --git a/distutils/cmd.py b/distutils/cmd.py index f90c13d8..c783e302 100644 --- a/distutils/cmd.py +++ b/distutils/cmd.py @@ -109,7 +109,7 @@ def __init__(self, dist: Distribution) -> None: # timestamps, but methods defined *here* assume that # 'self.force' exists for all commands. So define it here # just to be safe. - self.force = None + self.force: bool | None = None # The 'help' flag is just used for command-line parsing, so # none of that complicated bureaucracy is needed. diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py index 357b4e86..542c70b7 100644 --- a/distutils/command/bdist_rpm.py +++ b/distutils/command/bdist_rpm.py @@ -288,7 +288,7 @@ def run(self) -> None: # noqa: C901 spec_dir = self.dist_dir self.mkpath(spec_dir) else: - rpm_dir = {} + rpm_dir: dict[str, str] = {} for d in ('SOURCES', 'SPECS', 'BUILD', 'RPMS', 'SRPMS'): rpm_dir[d] = os.path.join(self.rpm_base, d) self.mkpath(rpm_dir[d]) diff --git a/distutils/command/build_py.py b/distutils/command/build_py.py index a20b076f..154e25b6 100644 --- a/distutils/command/build_py.py +++ b/distutils/command/build_py.py @@ -360,7 +360,7 @@ def build_modules(self) -> None: self.build_module(module, module_file, package) def build_packages(self) -> None: - for package in self.packages: + for package in self.packages or (): # Get list of (package, module, module_file) tuples based on # scanning the package directory. 'package' is only included # in the tuple so that 'find_modules()' and diff --git a/distutils/command/install.py b/distutils/command/install.py index 8421d54e..7b4c553c 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -256,8 +256,8 @@ def initialize_options(self) -> None: # These select only the installation base; it's up to the user to # specify the installation scheme (currently, that means supplying # the --install-{platlib,purelib,scripts,data} options). - self.install_base = None - self.install_platbase = None + self.install_base: str | None = None + self.install_platbase: str | None = None self.root: str | None = None # These options are the actual installation directories; if not diff --git a/distutils/dist.py b/distutils/dist.py index 35d6b66a..39b2c686 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -23,6 +23,7 @@ Literal, TypeVar, Union, + cast, overload, ) @@ -848,7 +849,7 @@ def get_command_class(self, command: str) -> type[Command]: continue try: - klass = getattr(module, klass_name) + klass = cast(type[Command], getattr(module, klass_name)) except AttributeError: raise DistutilsModuleError( f"invalid command '{command}' (no class '{klass_name}' in module '{module_name}')" diff --git a/distutils/extension.py b/distutils/extension.py index f5141126..d61ce77e 100644 --- a/distutils/extension.py +++ b/distutils/extension.py @@ -144,8 +144,7 @@ def __init__( # If there are unknown keyword options, warn about them if len(kw) > 0: - options = [repr(option) for option in kw] - options = ', '.join(sorted(options)) + options = ', '.join(sorted([repr(option) for option in kw])) msg = f"Unknown Extension options: {options}" warnings.warn(msg) diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py index 7d079118..e9407fee 100644 --- a/distutils/fancy_getopt.py +++ b/distutils/fancy_getopt.py @@ -249,6 +249,7 @@ def getopt(self, args: Sequence[str] | None = None, object=None): # noqa: C901 raise DistutilsArgError(msg) for opt, val in opts: + value: int | str = val if len(opt) == 2 and opt[0] == '-': # it's a short option opt = self.short2long[opt[1]] else: @@ -260,21 +261,21 @@ def getopt(self, args: Sequence[str] | None = None, object=None): # noqa: C901 opt = alias if not self.takes_arg[opt]: # boolean option? - assert val == '', "boolean option can't have value" + assert value == '', "boolean option can't have value" alias = self.negative_alias.get(opt) if alias: opt = alias - val = 0 + value = 0 else: - val = 1 + value = 1 attr = self.attr_name[opt] # The only repeating option at the moment is 'verbose'. # It has a negative option -q quiet, which should set verbose = False. - if val and self.repeat.get(attr) is not None: - val = getattr(object, attr, 0) + 1 - setattr(object, attr, val) - self.option_order.append((opt, val)) + if value and self.repeat.get(attr) is not None: + value = getattr(object, attr, 0) + 1 + setattr(object, attr, value) + self.option_order.append((opt, value)) # for opts if created_object: diff --git a/mypy.ini b/mypy.ini index 7053c510..20053514 100644 --- a/mypy.ini +++ b/mypy.ini @@ -16,20 +16,16 @@ disable_error_code = # local + # Code that is too dynamic using variable command names; + # and code that uses platform checks mypy doesn't understand + attr-defined, # TODO: Resolve and re-enable these gradually operator, - attr-defined, arg-type, - assignment, + ; assignment, call-overload, return-value, -exclude = (?x)( - # Exclude test folders and private modules for now - ^distutils/tests/ - ^distutils/_.+? - ) - # stdlib's test module is not typed on typeshed [mypy-test.*] ignore_missing_imports = True From 9e029d717d05f312ab60dad64d89d960c1f7ea8c Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 4 May 2025 11:41:35 -0400 Subject: [PATCH 5/6] Fix all call-overload --- distutils/archive_util.py | 19 ++++++++++++------- mypy.ini | 2 -- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/distutils/archive_util.py b/distutils/archive_util.py index 55730367..2bd1e4e9 100644 --- a/distutils/archive_util.py +++ b/distutils/archive_util.py @@ -6,6 +6,7 @@ from __future__ import annotations import os +from collections.abc import Callable from types import ModuleType from typing import Literal, overload @@ -89,16 +90,15 @@ def make_tarball( 'xz': 'xz', None: '', } - compress_ext = {'gzip': '.gz', 'bzip2': '.bz2', 'xz': '.xz'} + compress_ext = {'gzip': '.gz', 'bzip2': '.bz2', 'xz': '.xz', None: ''} # flags for compression program, each element of list will be an argument - if compress is not None and compress not in compress_ext.keys(): + if compress not in compress_ext.keys(): raise ValueError( - "bad value for 'compress': must be None, 'gzip', 'bzip2', 'xz'" + f"bad value for 'compress': must be one of {list(compress_ext.keys())!r}" ) - archive_name = base_name + '.tar' - archive_name += compress_ext.get(compress, '') + archive_name = base_name + '.tar' + compress_ext[compress] mkpath(os.path.dirname(archive_name), dry_run=dry_run) @@ -120,7 +120,10 @@ def _set_uid_gid(tarinfo): return tarinfo if not dry_run: - tar = tarfile.open(archive_name, f'w|{tar_compression[compress]}') + tar = tarfile.open( + archive_name, + f'w|{tar_compression[compress]}', # type: ignore[call-overload] # Typeshed doesn't allow non-literal string here + ) try: tar.add(base_dir, filter=_set_uid_gid) finally: @@ -195,7 +198,9 @@ def make_zipfile( # noqa: C901 return zip_filename -ARCHIVE_FORMATS = { +ARCHIVE_FORMATS: dict[ + str, tuple[Callable[..., str], list[tuple[str, str | None]], str] +] = { 'gztar': (make_tarball, [('compress', 'gzip')], "gzip'ed tar-file"), 'bztar': (make_tarball, [('compress', 'bzip2')], "bzip2'ed tar-file"), 'xztar': (make_tarball, [('compress', 'xz')], "xz'ed tar-file"), diff --git a/mypy.ini b/mypy.ini index 20053514..aafe5406 100644 --- a/mypy.ini +++ b/mypy.ini @@ -22,8 +22,6 @@ disable_error_code = # TODO: Resolve and re-enable these gradually operator, arg-type, - ; assignment, - call-overload, return-value, # stdlib's test module is not typed on typeshed From 5033f535389617000c6a43d93b33a4f534fa2e73 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 4 May 2025 11:43:19 -0400 Subject: [PATCH 6/6] Fix all return-value (except for the newer_pairwise issue) --- distutils/compilers/C/base.py | 4 ++-- mypy.ini | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/distutils/compilers/C/base.py b/distutils/compilers/C/base.py index 93385e13..cdb085ed 100644 --- a/distutils/compilers/C/base.py +++ b/distutils/compilers/C/base.py @@ -414,7 +414,7 @@ def _fix_compile_args( output_dir: str | None, macros: list[_Macro] | None, include_dirs: list[str] | tuple[str, ...] | None, - ) -> tuple[str, list[_Macro], list[str]]: + ) -> tuple[str | None, list[_Macro], list[str]]: """Typecheck and fix-up some of the arguments to the 'compile()' method, and return fixed-up values. Specifically: if 'output_dir' is None, replaces it with 'self.output_dir'; ensures that 'macros' @@ -466,7 +466,7 @@ def _prep_compile(self, sources, output_dir, depends=None): def _fix_object_args( self, objects: list[str] | tuple[str, ...], output_dir: str | None - ) -> tuple[list[str], str]: + ) -> tuple[list[str], str | None]: """Typecheck and fix up some arguments supplied to various methods. Specifically: ensure that 'objects' is a list; if output_dir is None, replace with self.output_dir. Return fixed versions of diff --git a/mypy.ini b/mypy.ini index aafe5406..3f67a0f6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -22,7 +22,6 @@ disable_error_code = # TODO: Resolve and re-enable these gradually operator, arg-type, - return-value, # stdlib's test module is not typed on typeshed [mypy-test.*]