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 d860f552..2bd1e4e9 100644 --- a/distutils/archive_util.py +++ b/distutils/archive_util.py @@ -6,19 +6,24 @@ from __future__ import annotations import os +from collections.abc import Callable +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: @@ -85,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) @@ -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 doesn't allow non-literal string here + ) try: tar.add(base_dir, filter=_set_uid_gid) finally: @@ -191,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"), @@ -270,7 +279,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 241621bd..c783e302 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: @@ -107,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. @@ -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/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/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 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/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 b9552a8b..39b2c686 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -23,6 +23,7 @@ Literal, TypeVar, Union, + cast, overload, ) @@ -576,11 +577,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. @@ -849,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}')" @@ -865,9 +865,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 @@ -1198,7 +1196,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/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/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 7ddc869a..be2016d8 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -324,6 +324,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 @@ -355,6 +363,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 @@ -376,7 +385,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: @@ -549,8 +558,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/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 17e86fed..62235cc9 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 146222a5..3f67a0f6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -16,20 +16,12 @@ 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, - call-overload, - return-value, - index, - type-var, - func-returns-value, - union-attr, - str-bytes-safe, - misc, - has-type, # stdlib's test module is not typed on typeshed [mypy-test.*]