diff --git a/newsfragments/4478.feature.rst b/newsfragments/4478.feature.rst new file mode 100644 index 0000000000..bd53339464 --- /dev/null +++ b/newsfragments/4478.feature.rst @@ -0,0 +1 @@ +Synced with pypa/distutils@c97a3db2f including better support for free threaded Python on Windows (pypa/distutils#310), improved typing support, and linter accommodations. diff --git a/pyproject.toml b/pyproject.toml index a19d4ac164..643b59591a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,7 @@ core = [ # for distutils "jaraco.collections", - "jaraco.functools>=4", + "jaraco.functools >= 4", "packaging", "more_itertools", ] diff --git a/setuptools/_distutils/cmd.py b/setuptools/_distutils/cmd.py index 2bb97956ab..9c6fa6566c 100644 --- a/setuptools/_distutils/cmd.py +++ b/setuptools/_distutils/cmd.py @@ -4,15 +4,21 @@ in the distutils.command package. """ +from __future__ import annotations + import logging import os import re import sys +from collections.abc import Callable +from typing import Any, ClassVar, TypeVar, overload from . import _modified, archive_util, dir_util, file_util, util from ._log import log from .errors import DistutilsOptionError +_CommandT = TypeVar("_CommandT", bound="Command") + class Command: """Abstract base class for defining command classes, the "worker bees" @@ -44,7 +50,14 @@ class Command: # 'sub_commands' is usually defined at the *end* of a class, because # predicates can be unbound methods, so they must already have been # defined. The canonical example is the "install" command. - sub_commands = [] + sub_commands: ClassVar[ # Any to work around variance issues + list[tuple[str, Callable[[Any], bool] | None]] + ] = [] + + user_options: ClassVar[ + # Specifying both because list is invariant. Avoids mypy override assignment issues + list[tuple[str, str, str]] | list[tuple[str, str | None, str]] + ] = [] # -- Creation/initialization methods ------------------------------- @@ -305,7 +318,17 @@ def get_finalized_command(self, command, create=True): # XXX rename to 'get_reinitialized_command()'? (should do the # same in dist.py, if so) - def reinitialize_command(self, command, reinit_subcommands=False): + @overload + def reinitialize_command( + self, command: str, reinit_subcommands: bool = False + ) -> Command: ... + @overload + def reinitialize_command( + self, command: _CommandT, reinit_subcommands: bool = False + ) -> _CommandT: ... + def reinitialize_command( + self, command: str | Command, reinit_subcommands=False + ) -> Command: return self.distribution.reinitialize_command(command, reinit_subcommands) def run_command(self, command): diff --git a/setuptools/_distutils/command/bdist.py b/setuptools/_distutils/command/bdist.py index f334075159..1ec3c35f40 100644 --- a/setuptools/_distutils/command/bdist.py +++ b/setuptools/_distutils/command/bdist.py @@ -5,6 +5,7 @@ import os import warnings +from typing import ClassVar from ..core import Command from ..errors import DistutilsOptionError, DistutilsPlatformError @@ -23,7 +24,7 @@ def show_formats(): pretty_printer.print_help("List of available distribution formats:") -class ListCompat(dict): +class ListCompat(dict[str, tuple[str, str]]): # adapter to allow for Setuptools compatibility in format_commands def append(self, item): warnings.warn( @@ -70,7 +71,7 @@ class bdist(Command): ] # The following commands do not take a format option from bdist - no_format_option = ('bdist_rpm',) + no_format_option: ClassVar[tuple[str, ...]] = ('bdist_rpm',) # This won't do in reality: will need to distinguish RPM-ish Linux, # Debian-ish Linux, Solaris, FreeBSD, ..., Windows, Mac OS. diff --git a/setuptools/_distutils/command/build.py b/setuptools/_distutils/command/build.py index caf55073af..ccd2c706a3 100644 --- a/setuptools/_distutils/command/build.py +++ b/setuptools/_distutils/command/build.py @@ -113,7 +113,8 @@ def finalize_options(self): # noqa: C901 self.build_temp = os.path.join(self.build_base, 'temp' + plat_specifier) if self.build_scripts is None: self.build_scripts = os.path.join( - self.build_base, 'scripts-%d.%d' % sys.version_info[:2] + self.build_base, + f'scripts-{sys.version_info.major}.{sys.version_info.minor}', ) if self.executable is None and sys.executable: diff --git a/setuptools/_distutils/command/build_clib.py b/setuptools/_distutils/command/build_clib.py index a600d09373..3e1832768b 100644 --- a/setuptools/_distutils/command/build_clib.py +++ b/setuptools/_distutils/command/build_clib.py @@ -16,6 +16,7 @@ import os from distutils._log import log +from typing import ClassVar from ..core import Command from ..errors import DistutilsSetupError @@ -31,7 +32,7 @@ def show_compilers(): class build_clib(Command): description = "build C/C++ libraries used by Python extensions" - user_options = [ + user_options: ClassVar[list[tuple[str, str, str]]] = [ ('build-clib=', 'b', "directory to build C/C++ libraries to"), ('build-temp=', 't', "directory to put temporary build by-products"), ('debug', 'g', "compile with debugging information"), @@ -138,8 +139,7 @@ def check_library_list(self, libraries): if '/' in name or (os.sep != '/' and os.sep in name): raise DistutilsSetupError( - f"bad library name '{lib[0]}': " - "may not contain directory separators" + f"bad library name '{lib[0]}': may not contain directory separators" ) if not isinstance(build_info, dict): diff --git a/setuptools/_distutils/command/build_ext.py b/setuptools/_distutils/command/build_ext.py index a7e3038be6..cf60bd0ad8 100644 --- a/setuptools/_distutils/command/build_ext.py +++ b/setuptools/_distutils/command/build_ext.py @@ -23,7 +23,7 @@ ) from ..extension import Extension from ..sysconfig import customize_compiler, get_config_h_filename, get_python_version -from ..util import get_platform, is_mingw +from ..util import get_platform, is_freethreaded, is_mingw # An extension name is just a dot-separated list of Python NAMEs (ie. # the same as a fully-qualified module name). @@ -333,6 +333,12 @@ def run(self): # noqa: C901 if os.name == 'nt' and self.plat_name != get_platform(): self.compiler.initialize(self.plat_name) + # The official Windows free threaded Python installer doesn't set + # Py_GIL_DISABLED because its pyconfig.h is shared with the + # default build, so define it here (pypa/setuptools#4662). + if os.name == 'nt' and is_freethreaded(): + self.compiler.define_macro('Py_GIL_DISABLED', '1') + # And make sure that any compile/link-related options (which might # come from the command-line or from the setup script) are set in # that CCompiler object -- that way, they automatically apply to @@ -437,8 +443,7 @@ def check_extensions_list(self, extensions): # noqa: C901 for macro in macros: if not (isinstance(macro, tuple) and len(macro) in (1, 2)): raise DistutilsSetupError( - "'macros' element of build info dict " - "must be 1- or 2-tuple" + "'macros' element of build info dict must be 1- or 2-tuple" ) if len(macro) == 1: ext.undef_macros.append(macro[0]) @@ -666,8 +671,7 @@ def find_swig(self): return "swig.exe" else: raise DistutilsPlatformError( - "I don't know how to find (much less run) SWIG " - f"on platform '{os.name}'" + f"I don't know how to find (much less run) SWIG on platform '{os.name}'" ) # -- Name generators ----------------------------------------------- diff --git a/setuptools/_distutils/command/build_scripts.py b/setuptools/_distutils/command/build_scripts.py index 9e5963c243..1c6fd3caff 100644 --- a/setuptools/_distutils/command/build_scripts.py +++ b/setuptools/_distutils/command/build_scripts.py @@ -8,6 +8,7 @@ from distutils import sysconfig from distutils._log import log from stat import ST_MODE +from typing import ClassVar from .._modified import newer from ..core import Command @@ -25,7 +26,7 @@ class build_scripts(Command): description = "\"build\" scripts (copy and fixup #! line)" - user_options = [ + user_options: ClassVar[list[tuple[str, str, str]]] = [ ('build-dir=', 'd', "directory to \"build\" (copy) to"), ('force', 'f', "forcibly build everything (ignore file timestamps"), ('executable=', 'e', "specify final destination interpreter path"), diff --git a/setuptools/_distutils/command/check.py b/setuptools/_distutils/command/check.py index 93d754e73d..078c1ce87e 100644 --- a/setuptools/_distutils/command/check.py +++ b/setuptools/_distutils/command/check.py @@ -4,6 +4,7 @@ """ import contextlib +from typing import ClassVar from ..core import Command from ..errors import DistutilsSetupError @@ -41,15 +42,12 @@ class check(Command): """This command checks the meta-data of the package.""" description = "perform some checks on the package" - user_options = [ + user_options: ClassVar[list[tuple[str, str, str]]] = [ ('metadata', 'm', 'Verify meta-data'), ( 'restructuredtext', 'r', - ( - 'Checks if long string meta-data syntax ' - 'are reStructuredText-compliant' - ), + 'Checks if long string meta-data syntax are reStructuredText-compliant', ), ('strict', 's', 'Will exit with an error if a check fails'), ] diff --git a/setuptools/_distutils/command/command_template b/setuptools/_distutils/command/command_template index 6106819db8..a4a751ad3c 100644 --- a/setuptools/_distutils/command/command_template +++ b/setuptools/_distutils/command/command_template @@ -8,18 +8,18 @@ Implements the Distutils 'x' command. __revision__ = "$Id$" from distutils.core import Command +from typing import ClassVar class x(Command): - # Brief (40-50 characters) description of the command description = "" # List of option tuples: long name, short name (None if no short # name), and help string. - user_options = [('', '', - ""), - ] + user_options: ClassVar[list[tuple[str, str, str]]] = [ + ('', '', ""), + ] def initialize_options(self): self. = None diff --git a/setuptools/_distutils/command/install.py b/setuptools/_distutils/command/install.py index ceb453e041..9400995024 100644 --- a/setuptools/_distutils/command/install.py +++ b/setuptools/_distutils/command/install.py @@ -407,8 +407,8 @@ def finalize_options(self): # noqa: C901 'dist_version': self.distribution.get_version(), 'dist_fullname': self.distribution.get_fullname(), 'py_version': py_version, - 'py_version_short': '%d.%d' % sys.version_info[:2], - 'py_version_nodot': '%d%d' % sys.version_info[:2], + 'py_version_short': f'{sys.version_info.major}.{sys.version_info.minor}', + 'py_version_nodot': f'{sys.version_info.major}{sys.version_info.minor}', 'sys_prefix': prefix, 'prefix': prefix, 'sys_exec_prefix': exec_prefix, diff --git a/setuptools/_distutils/command/install_data.py b/setuptools/_distutils/command/install_data.py index a90ec3b4d0..36f5bcc8bf 100644 --- a/setuptools/_distutils/command/install_data.py +++ b/setuptools/_distutils/command/install_data.py @@ -9,7 +9,7 @@ import functools import os -from typing import Iterable +from collections.abc import Iterable from ..core import Command from ..util import change_root, convert_path @@ -22,8 +22,7 @@ class install_data(Command): ( 'install-dir=', 'd', - "base directory for installing data files " - "[default: installation base dir]", + "base directory for installing data files [default: installation base dir]", ), ('root=', None, "install everything relative to this alternate root directory"), ('force', 'f', "force installation (overwrite existing files)"), diff --git a/setuptools/_distutils/command/install_egg_info.py b/setuptools/_distutils/command/install_egg_info.py index 4fbb3440ab..230e94ab46 100644 --- a/setuptools/_distutils/command/install_egg_info.py +++ b/setuptools/_distutils/command/install_egg_info.py @@ -8,6 +8,7 @@ import os import re import sys +from typing import ClassVar from .. import dir_util from .._log import log @@ -18,7 +19,7 @@ class install_egg_info(Command): """Install an .egg-info file for the package""" description = "Install package's PKG-INFO metadata as an .egg-info file" - user_options = [ + user_options: ClassVar[list[tuple[str, str, str]]] = [ ('install-dir=', 'd', "directory to install to"), ] @@ -31,11 +32,9 @@ def basename(self): Allow basename to be overridden by child class. Ref pypa/distutils#2. """ - return "%s-%s-py%d.%d.egg-info" % ( - to_filename(safe_name(self.distribution.get_name())), - to_filename(safe_version(self.distribution.get_version())), - *sys.version_info[:2], - ) + name = to_filename(safe_name(self.distribution.get_name())) + version = to_filename(safe_version(self.distribution.get_version())) + return f"{name}-{version}-py{sys.version_info.major}.{sys.version_info.minor}.egg-info" def finalize_options(self): self.set_undefined_options('install_lib', ('install_dir', 'install_dir')) diff --git a/setuptools/_distutils/command/install_headers.py b/setuptools/_distutils/command/install_headers.py index fbb3b242ea..586121e089 100644 --- a/setuptools/_distutils/command/install_headers.py +++ b/setuptools/_distutils/command/install_headers.py @@ -3,6 +3,8 @@ Implements the Distutils 'install_headers' command, to install C/C++ header files to the Python include directory.""" +from typing import ClassVar + from ..core import Command @@ -10,7 +12,7 @@ class install_headers(Command): description = "install C/C++ header files" - user_options = [ + user_options: ClassVar[list[tuple[str, str, str]]] = [ ('install-dir=', 'd', "directory to install header files to"), ('force', 'f', "force installation (overwrite existing files)"), ] diff --git a/setuptools/_distutils/command/sdist.py b/setuptools/_distutils/command/sdist.py index d723a1c9fb..acb3a41650 100644 --- a/setuptools/_distutils/command/sdist.py +++ b/setuptools/_distutils/command/sdist.py @@ -8,6 +8,7 @@ from distutils._log import log from glob import glob from itertools import filterfalse +from typing import ClassVar from ..core import Command from ..errors import DistutilsOptionError, DistutilsTemplateError @@ -114,7 +115,7 @@ def checking_metadata(self): sub_commands = [('check', checking_metadata)] - READMES = ('README', 'README.txt', 'README.rst') + READMES: ClassVar[tuple[str, ...]] = ('README', 'README.txt', 'README.rst') def initialize_options(self): # 'template' and 'manifest' are, respectively, the names of @@ -362,8 +363,7 @@ def read_template(self): # convert_path function except (DistutilsTemplateError, ValueError) as msg: self.warn( - "%s, line %d: %s" - % (template.filename, template.current_line, msg) + f"{template.filename}, line {int(template.current_line)}: {msg}" ) finally: template.close() diff --git a/setuptools/_distutils/compat/__init__.py b/setuptools/_distutils/compat/__init__.py index e12534a32c..c715ee9cc5 100644 --- a/setuptools/_distutils/compat/__init__.py +++ b/setuptools/_distutils/compat/__init__.py @@ -1,7 +1,5 @@ from __future__ import annotations -from .py38 import removeprefix - def consolidate_linker_args(args: list[str]) -> list[str] | str: """ @@ -12,4 +10,4 @@ def consolidate_linker_args(args: list[str]) -> list[str] | str: if not all(arg.startswith('-Wl,') for arg in args): return args - return '-Wl,' + ','.join(removeprefix(arg, '-Wl,') for arg in args) + return '-Wl,' + ','.join(arg.removeprefix('-Wl,') for arg in args) diff --git a/setuptools/_distutils/compat/py38.py b/setuptools/_distutils/compat/py38.py deleted file mode 100644 index afe5345553..0000000000 --- a/setuptools/_distutils/compat/py38.py +++ /dev/null @@ -1,34 +0,0 @@ -import sys - -if sys.version_info < (3, 9): - - def removesuffix(self, suffix): - # suffix='' should not call self[:-0]. - if suffix and self.endswith(suffix): - return self[: -len(suffix)] - else: - return self[:] - - def removeprefix(self, prefix): - if self.startswith(prefix): - return self[len(prefix) :] - else: - return self[:] - -else: - - def removesuffix(self, suffix): - return self.removesuffix(suffix) - - def removeprefix(self, prefix): - return self.removeprefix(prefix) - - -def aix_platform(osname, version, release): - try: - import _aix_support - - return _aix_support.aix_platform() - except ImportError: - pass - return f"{osname}-{version}.{release}" diff --git a/setuptools/_distutils/core.py b/setuptools/_distutils/core.py index bc06091abb..bd62546bdd 100644 --- a/setuptools/_distutils/core.py +++ b/setuptools/_distutils/core.py @@ -6,9 +6,12 @@ really defined in distutils.dist and distutils.cmd. """ +from __future__ import annotations + import os import sys import tokenize +from collections.abc import Iterable from .cmd import Command from .debug import DEBUG @@ -215,7 +218,7 @@ def run_commands(dist): return dist -def run_setup(script_name, script_args=None, stop_after="run"): +def run_setup(script_name, script_args: Iterable[str] | None = None, stop_after="run"): """Run a setup script in a somewhat controlled environment, and return the Distribution instance that drives things. This is useful if you need to find out the distribution meta-data (passed as diff --git a/setuptools/_distutils/dist.py b/setuptools/_distutils/dist.py index 154301baff..33ed8ebd7a 100644 --- a/setuptools/_distutils/dist.py +++ b/setuptools/_distutils/dist.py @@ -4,6 +4,8 @@ being built/installed/distributed. """ +from __future__ import annotations + import contextlib import logging import os @@ -13,6 +15,7 @@ import warnings from collections.abc import Iterable from email import message_from_file +from typing import TYPE_CHECKING, Literal, TypeVar, overload from packaging.utils import canonicalize_name, canonicalize_version @@ -27,6 +30,12 @@ from .fancy_getopt import FancyGetopt, translate_longopt from .util import check_environ, rfc822_escape, strtobool +if TYPE_CHECKING: + # type-only import because of mutual dependence between these modules + from .cmd import Command + +_CommandT = TypeVar("_CommandT", bound="Command") + # Regex to define acceptable Distutils command names. This is not *quite* # the same as a Python NAME -- I don't allow leading underscores. The fact # that they're very similar is no coincidence; the default naming scheme is @@ -139,7 +148,7 @@ def __init__(self, attrs=None): # noqa: C901 self.dry_run = False self.help = False for attr in self.display_option_names: - setattr(self, attr, 0) + setattr(self, attr, False) # Store the distribution meta-data (name, version, author, and so # forth) in a separate object -- we're getting to have enough @@ -169,7 +178,7 @@ def __init__(self, attrs=None): # noqa: C901 # and sys.argv[1:], but they can be overridden when the caller is # not necessarily a setup script run from the command-line. self.script_name = None - self.script_args = None + self.script_args: list[str] | None = None # 'command_options' is where we store command options between # parsing them (from config files, the command-line, etc.) and when @@ -269,6 +278,8 @@ def __init__(self, attrs=None): # noqa: C901 self.want_user_cfg = True if self.script_args is not None: + # Coerce any possible iterable from attrs into a list + self.script_args = list(self.script_args) for arg in self.script_args: if not arg.startswith('-'): break @@ -722,7 +733,7 @@ def print_command_list(self, commands, header, max_length): except AttributeError: description = "(no description available)" - print(" %-*s %s" % (max_length, cmd, description)) + print(f" {cmd:<{max_length}} {description}") def print_commands(self): """Print out a help message listing all available commands with a @@ -829,7 +840,15 @@ def get_command_class(self, command): raise DistutilsModuleError(f"invalid command '{command}'") - def get_command_obj(self, command, create=True): + @overload + 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 = 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 object for 'command' is in the cache, then we either create and @@ -900,7 +919,17 @@ def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 except ValueError as msg: raise DistutilsOptionError(msg) - def reinitialize_command(self, command, reinit_subcommands=False): + @overload + def reinitialize_command( + self, command: str, reinit_subcommands: bool = False + ) -> Command: ... + @overload + def reinitialize_command( + self, command: _CommandT, reinit_subcommands: bool = False + ) -> _CommandT: ... + def reinitialize_command( + self, command: str | Command, reinit_subcommands=False + ) -> Command: """Reinitializes a command to the state it was in when first returned by 'get_command_obj()': ie., initialized but not yet finalized. This provides the opportunity to sneak option diff --git a/setuptools/_distutils/extension.py b/setuptools/_distutils/extension.py index 33159079c1..e053273436 100644 --- a/setuptools/_distutils/extension.py +++ b/setuptools/_distutils/extension.py @@ -26,12 +26,14 @@ class Extension: name : string the full name of the extension, including any packages -- ie. *not* a filename or pathname, but Python dotted name - sources : [string | os.PathLike] - list of source filenames, relative to the distribution root - (where the setup script lives), in Unix form (slash-separated) - for portability. Source files may be C, C++, SWIG (.i), - platform-specific resource files, or whatever else is recognized - by the "build_ext" command as source for a Python extension. + sources : Iterable[string | os.PathLike] + iterable of source filenames (except strings, which could be misinterpreted + as a single filename), relative to the distribution root (where the setup + script lives), in Unix form (slash-separated) for portability. Can be any + non-string iterable (list, tuple, set, etc.) containing strings or + PathLike objects. Source files may be C, C++, SWIG (.i), platform-specific + resource files, or whatever else is recognized by the "build_ext" command + as source for a Python extension. include_dirs : [string] list of directories to search for C/C++ header files (in Unix form for portability) @@ -105,17 +107,23 @@ def __init__( **kw, # To catch unknown keywords ): if not isinstance(name, str): - raise AssertionError("'name' must be a string") # noqa: TRY004 - if not ( - isinstance(sources, list) - and all(isinstance(v, (str, os.PathLike)) for v in sources) - ): - raise AssertionError( - "'sources' must be a list of strings or PathLike objects." + raise TypeError("'name' must be a string") + + # handle the string case first; since strings are iterable, disallow them + if isinstance(sources, str): + raise TypeError( + "'sources' must be an iterable of strings or PathLike objects, not a string" + ) + + # now we check if it's iterable and contains valid types + try: + self.sources = list(map(os.fspath, sources)) + except TypeError: + raise TypeError( + "'sources' must be an iterable of strings or PathLike objects" ) self.name = name - self.sources = list(map(os.fspath, sources)) self.include_dirs = include_dirs or [] self.define_macros = define_macros or [] self.undef_macros = undef_macros or [] diff --git a/setuptools/_distutils/fancy_getopt.py b/setuptools/_distutils/fancy_getopt.py index 907cc2b73c..1a1d3a05da 100644 --- a/setuptools/_distutils/fancy_getopt.py +++ b/setuptools/_distutils/fancy_getopt.py @@ -8,11 +8,14 @@ * options set attributes of a passed-in object """ +from __future__ import annotations + import getopt import re import string import sys -from typing import Any, Sequence +from collections.abc import Sequence +from typing import Any from .errors import DistutilsArgError, DistutilsGetoptError @@ -167,8 +170,7 @@ def _grok_option_table(self): # noqa: C901 if not ((short is None) or (isinstance(short, str) and len(short) == 1)): raise DistutilsGetoptError( - f"invalid short option '{short}': " - "must a single character or None" + f"invalid short option '{short}': must a single character or None" ) self.repeat[long] = repeat @@ -219,7 +221,7 @@ def _grok_option_table(self): # noqa: C901 self.short_opts.append(short) self.short2long[short[0]] = long - def getopt(self, args=None, object=None): # noqa: C901 + def getopt(self, args: Sequence[str] | None = None, object=None): # noqa: C901 """Parse command-line options in args. Store as attributes on object. If 'args' is None or not supplied, uses 'sys.argv[1:]'. If @@ -351,18 +353,18 @@ def generate_help(self, header=None): # noqa: C901 # Case 1: no short option at all (makes life easy) if short is None: if text: - lines.append(" --%-*s %s" % (max_opt, long, text[0])) + lines.append(f" --{long:<{max_opt}} {text[0]}") else: - lines.append(" --%-*s " % (max_opt, long)) + lines.append(f" --{long:<{max_opt}}") # Case 2: we have a short option, so we have to include it # just after the long option else: opt_names = f"{long} (-{short})" if text: - lines.append(" --%-*s %s" % (max_opt, opt_names, text[0])) + lines.append(f" --{opt_names:<{max_opt}} {text[0]}") else: - lines.append(" --%-*s" % opt_names) + lines.append(f" --{opt_names:<{max_opt}}") for ell in text[1:]: lines.append(big_indent + ell) @@ -375,7 +377,7 @@ def print_help(self, header=None, file=None): file.write(line + "\n") -def fancy_getopt(options, negative_opt, object, args): +def fancy_getopt(options, negative_opt, object, args: Sequence[str] | None): parser = FancyGetopt(options) parser.set_negative_aliases(negative_opt) return parser.getopt(args, object) @@ -464,6 +466,6 @@ def __init__(self, options: Sequence[Any] = []): say, "How should I know?"].)""" for w in (10, 20, 30, 40): - print("width: %d" % w) + print(f"width: {w}") print("\n".join(wrap_text(text, w))) print() diff --git a/setuptools/_distutils/file_util.py b/setuptools/_distutils/file_util.py index 85ee4dafcb..0acc8cb84b 100644 --- a/setuptools/_distutils/file_util.py +++ b/setuptools/_distutils/file_util.py @@ -118,7 +118,7 @@ def copy_file( # noqa: C901 if update and not newer(src, dst): if verbose >= 1: log.debug("not copying %s (output up-to-date)", src) - return (dst, 0) + return (dst, False) try: action = _copy_action[link] @@ -132,7 +132,7 @@ def copy_file( # noqa: C901 log.info("%s %s -> %s", action, src, dst) if dry_run: - return (dst, 1) + return (dst, True) # If linking (hard or symbolic), use the appropriate system call # (Unix only, of course, but that's the caller's responsibility) @@ -146,11 +146,11 @@ def copy_file( # noqa: C901 # even under Unix, see issue #8876). pass else: - return (dst, 1) + return (dst, True) elif link == 'sym': if not (os.path.exists(dst) and os.path.samefile(src, dst)): os.symlink(src, dst) - return (dst, 1) + return (dst, True) # Otherwise (non-Mac, not linking), copy the file contents and # (optionally) copy the times and mode. @@ -165,7 +165,7 @@ def copy_file( # noqa: C901 if preserve_mode: os.chmod(dst, S_IMODE(st[ST_MODE])) - return (dst, 1) + return (dst, True) # XXX I suspect this is Unix-specific -- need porting help! diff --git a/setuptools/_distutils/filelist.py b/setuptools/_distutils/filelist.py index 44ae9e67ef..9857b19549 100644 --- a/setuptools/_distutils/filelist.py +++ b/setuptools/_distutils/filelist.py @@ -127,10 +127,7 @@ def process_template_line(self, line): # noqa: C901 for pattern in patterns: if not self.exclude_pattern(pattern, anchor=True): log.warning( - ( - "warning: no previously-included files " - "found matching '%s'" - ), + "warning: no previously-included files found matching '%s'", pattern, ) diff --git a/setuptools/_distutils/spawn.py b/setuptools/_distutils/spawn.py index 107b011397..ba280334d1 100644 --- a/setuptools/_distutils/spawn.py +++ b/setuptools/_distutils/spawn.py @@ -12,7 +12,7 @@ import subprocess import sys import warnings -from typing import Mapping +from collections.abc import Mapping from ._log import log from .debug import DEBUG diff --git a/setuptools/_distutils/sysconfig.py b/setuptools/_distutils/sysconfig.py index da1eecbe7e..358d1079dc 100644 --- a/setuptools/_distutils/sysconfig.py +++ b/setuptools/_distutils/sysconfig.py @@ -107,7 +107,7 @@ def get_python_version(): leaving off the patchlevel. Sample return values could be '1.5' or '2.2'. """ - return '%d.%d' % sys.version_info[:2] + return f'{sys.version_info.major}.{sys.version_info.minor}' def get_python_inc(plat_specific=False, prefix=None): @@ -340,6 +340,7 @@ def customize_compiler(compiler): ldshared = _add_flags(ldshared, 'LD') ldcxxshared = _add_flags(ldcxxshared, 'LD') + cflags = os.environ.get('CFLAGS', cflags) cflags = _add_flags(cflags, 'C') ldshared = _add_flags(ldshared, 'C') cxxflags = os.environ.get('CXXFLAGS', cxxflags) diff --git a/setuptools/_distutils/tests/__init__.py b/setuptools/_distutils/tests/__init__.py index 93fbf49074..5a8ab06100 100644 --- a/setuptools/_distutils/tests/__init__.py +++ b/setuptools/_distutils/tests/__init__.py @@ -8,7 +8,7 @@ """ import shutil -from typing import Sequence +from collections.abc import Sequence def missing_compiler_executable(cmd_names: Sequence[str] = []): # pragma: no cover diff --git a/setuptools/_distutils/tests/compat/py38.py b/setuptools/_distutils/tests/compat/py38.py deleted file mode 100644 index 211d3a6c50..0000000000 --- a/setuptools/_distutils/tests/compat/py38.py +++ /dev/null @@ -1,50 +0,0 @@ -# flake8: noqa - -import contextlib -import builtins -import sys - -from test.support import requires_zlib -import test.support - - -ModuleNotFoundError = getattr(builtins, 'ModuleNotFoundError', ImportError) - -try: - from test.support.warnings_helper import check_warnings -except (ModuleNotFoundError, ImportError): - from test.support import check_warnings - - -try: - from test.support.os_helper import ( - rmtree, - EnvironmentVarGuard, - unlink, - skip_unless_symlink, - temp_dir, - ) -except (ModuleNotFoundError, ImportError): - from test.support import ( - rmtree, - EnvironmentVarGuard, - unlink, - skip_unless_symlink, - temp_dir, - ) - - -try: - from test.support.import_helper import ( - DirsOnSysPath, - CleanImport, - ) -except (ModuleNotFoundError, ImportError): - from test.support import ( - DirsOnSysPath, - CleanImport, - ) - - -if sys.version_info < (3, 9): - requires_zlib = lambda: test.support.requires_zlib diff --git a/setuptools/_distutils/tests/compat/py39.py b/setuptools/_distutils/tests/compat/py39.py new file mode 100644 index 0000000000..aca3939a0c --- /dev/null +++ b/setuptools/_distutils/tests/compat/py39.py @@ -0,0 +1,40 @@ +import sys + +if sys.version_info >= (3, 10): + from test.support.import_helper import ( + CleanImport as CleanImport, + ) + from test.support.import_helper import ( + DirsOnSysPath as DirsOnSysPath, + ) + from test.support.os_helper import ( + EnvironmentVarGuard as EnvironmentVarGuard, + ) + from test.support.os_helper import ( + rmtree as rmtree, + ) + from test.support.os_helper import ( + skip_unless_symlink as skip_unless_symlink, + ) + from test.support.os_helper import ( + unlink as unlink, + ) +else: + from test.support import ( + CleanImport as CleanImport, + ) + from test.support import ( + DirsOnSysPath as DirsOnSysPath, + ) + from test.support import ( + EnvironmentVarGuard as EnvironmentVarGuard, + ) + from test.support import ( + rmtree as rmtree, + ) + from test.support import ( + skip_unless_symlink as skip_unless_symlink, + ) + from test.support import ( + unlink as unlink, + ) diff --git a/setuptools/_distutils/tests/test_bdist_rpm.py b/setuptools/_distutils/tests/test_bdist_rpm.py index 1109fdf117..75051430e2 100644 --- a/setuptools/_distutils/tests/test_bdist_rpm.py +++ b/setuptools/_distutils/tests/test_bdist_rpm.py @@ -8,8 +8,7 @@ from distutils.tests import support import pytest - -from .compat.py38 import requires_zlib +from test.support import requires_zlib SETUP_PY = """\ from distutils.core import setup diff --git a/setuptools/_distutils/tests/test_build.py b/setuptools/_distutils/tests/test_build.py index d379aca0bb..f7fe69acd1 100644 --- a/setuptools/_distutils/tests/test_build.py +++ b/setuptools/_distutils/tests/test_build.py @@ -40,7 +40,9 @@ def test_finalize_options(self): assert cmd.build_temp == wanted # build_scripts is build/scripts-x.x - wanted = os.path.join(cmd.build_base, 'scripts-%d.%d' % sys.version_info[:2]) + wanted = os.path.join( + cmd.build_base, f'scripts-{sys.version_info.major}.{sys.version_info.minor}' + ) assert cmd.build_scripts == wanted # executable is os.path.normpath(sys.executable) diff --git a/setuptools/_distutils/tests/test_build_ext.py b/setuptools/_distutils/tests/test_build_ext.py index 8bd3cef855..beeba4850c 100644 --- a/setuptools/_distutils/tests/test_build_ext.py +++ b/setuptools/_distutils/tests/test_build_ext.py @@ -19,11 +19,7 @@ ) from distutils.extension import Extension from distutils.tests import missing_compiler_executable -from distutils.tests.support import ( - TempdirManager, - copy_xxmodule_c, - fixup_build_ext, -) +from distutils.tests.support import TempdirManager, copy_xxmodule_c, fixup_build_ext from io import StringIO import jaraco.path @@ -31,7 +27,7 @@ import pytest from test import support -from .compat import py38 as import_helper +from .compat import py39 as import_helper @pytest.fixture() @@ -522,14 +518,15 @@ def _try_compile_deployment_target(self, operator, target): # pragma: no cover # at least one value we test with will not exist yet. if target[:2] < (10, 10): # for 10.1 through 10.9.x -> "10n0" - target = '%02d%01d0' % target + tmpl = '{:02}{:01}0' else: # for 10.10 and beyond -> "10nn00" if len(target) >= 2: - target = '%02d%02d00' % target + tmpl = '{:02}{:02}00' else: # 11 and later can have no minor version (11 instead of 11.0) - target = '%02d0000' % target + tmpl = '{:02}0000' + target = tmpl.format(*target) deptarget_ext = Extension( 'deptarget', [self.tmp_path / 'deptargetmodule.c'], diff --git a/setuptools/_distutils/tests/test_dir_util.py b/setuptools/_distutils/tests/test_dir_util.py index fcc37ac568..326cb34614 100644 --- a/setuptools/_distutils/tests/test_dir_util.py +++ b/setuptools/_distutils/tests/test_dir_util.py @@ -3,6 +3,7 @@ import os import pathlib import stat +import sys import unittest.mock as mock from distutils import dir_util, errors from distutils.dir_util import ( @@ -106,8 +107,9 @@ def test_copy_tree_exception_in_listdir(self): """ An exception in listdir should raise a DistutilsFileError """ - with mock.patch("os.listdir", side_effect=OSError()), pytest.raises( - errors.DistutilsFileError + with ( + mock.patch("os.listdir", side_effect=OSError()), + pytest.raises(errors.DistutilsFileError), ): src = self.tempdirs[-1] dir_util.copy_tree(src, None) @@ -123,6 +125,9 @@ class FailPath(pathlib.Path): def mkdir(self, *args, **kwargs): raise OSError("Failed to create directory") + if sys.version_info < (3, 12): + _flavour = pathlib.Path()._flavour + target = tmp_path / 'foodir' with pytest.raises(errors.DistutilsFileError): diff --git a/setuptools/_distutils/tests/test_dist.py b/setuptools/_distutils/tests/test_dist.py index 4d78a19803..2c5beebe64 100644 --- a/setuptools/_distutils/tests/test_dist.py +++ b/setuptools/_distutils/tests/test_dist.py @@ -13,6 +13,7 @@ from distutils.cmd import Command from distutils.dist import Distribution, fix_help_options from distutils.tests import support +from typing import ClassVar import jaraco.path import pytest @@ -23,7 +24,7 @@ class test_dist(Command): """Sample distutils extension command.""" - user_options = [ + user_options: ClassVar[list[tuple[str, str, str]]] = [ ("sample-option=", "S", "help text"), ] @@ -246,6 +247,12 @@ def test_find_config_files_disable(self, temp_home): # make sure --no-user-cfg disables the user cfg file assert len(all_files) - 1 == len(files) + def test_script_args_list_coercion(self): + d = Distribution(attrs={'script_args': ('build', '--no-user-cfg')}) + + # make sure script_args is a list even if it started as a different iterable + assert d.script_args == ['build', '--no-user-cfg'] + @pytest.mark.skipif( 'platform.system() == "Windows"', reason='Windows does not honor chmod 000', diff --git a/setuptools/_distutils/tests/test_extension.py b/setuptools/_distutils/tests/test_extension.py index 41872e04e8..5e8e768223 100644 --- a/setuptools/_distutils/tests/test_extension.py +++ b/setuptools/_distutils/tests/test_extension.py @@ -6,8 +6,7 @@ from distutils.extension import Extension, read_setup_file import pytest - -from .compat.py38 import check_warnings +from test.support.warnings_helper import check_warnings class TestExtension: @@ -63,22 +62,32 @@ def test_read_setup_file(self): def test_extension_init(self): # the first argument, which is the name, must be a string - with pytest.raises(AssertionError): + with pytest.raises(TypeError): Extension(1, []) ext = Extension('name', []) assert ext.name == 'name' # the second argument, which is the list of files, must - # be a list of strings or PathLike objects - with pytest.raises(AssertionError): + # be an iterable of strings or PathLike objects, and not a string + with pytest.raises(TypeError): Extension('name', 'file') - with pytest.raises(AssertionError): + with pytest.raises(TypeError): Extension('name', ['file', 1]) ext = Extension('name', ['file1', 'file2']) assert ext.sources == ['file1', 'file2'] ext = Extension('name', [pathlib.Path('file1'), pathlib.Path('file2')]) assert ext.sources == ['file1', 'file2'] + # any non-string iterable of strings or PathLike objects should work + ext = Extension('name', ('file1', 'file2')) # tuple + assert ext.sources == ['file1', 'file2'] + ext = Extension('name', {'file1', 'file2'}) # set + assert sorted(ext.sources) == ['file1', 'file2'] + ext = Extension('name', iter(['file1', 'file2'])) # iterator + assert ext.sources == ['file1', 'file2'] + ext = Extension('name', [pathlib.Path('file1'), 'file2']) # mixed types + assert ext.sources == ['file1', 'file2'] + # others arguments have defaults for attr in ( 'include_dirs', diff --git a/setuptools/_distutils/tests/test_file_util.py b/setuptools/_distutils/tests/test_file_util.py index 85ac2136b3..a75d4a0317 100644 --- a/setuptools/_distutils/tests/test_file_util.py +++ b/setuptools/_distutils/tests/test_file_util.py @@ -44,18 +44,19 @@ def test_move_file_verbosity(self, caplog): def test_move_file_exception_unpacking_rename(self): # see issue 22182 - with mock.patch("os.rename", side_effect=OSError("wrong", 1)), pytest.raises( - DistutilsFileError + with ( + mock.patch("os.rename", side_effect=OSError("wrong", 1)), + pytest.raises(DistutilsFileError), ): jaraco.path.build({self.source: 'spam eggs'}) move_file(self.source, self.target, verbose=False) def test_move_file_exception_unpacking_unlink(self): # see issue 22182 - with mock.patch( - "os.rename", side_effect=OSError(errno.EXDEV, "wrong") - ), mock.patch("os.unlink", side_effect=OSError("wrong", 1)), pytest.raises( - DistutilsFileError + with ( + mock.patch("os.rename", side_effect=OSError(errno.EXDEV, "wrong")), + mock.patch("os.unlink", side_effect=OSError("wrong", 1)), + pytest.raises(DistutilsFileError), ): jaraco.path.build({self.source: 'spam eggs'}) move_file(self.source, self.target, verbose=False) diff --git a/setuptools/_distutils/tests/test_filelist.py b/setuptools/_distutils/tests/test_filelist.py index ec7e5cf363..130e6fb53b 100644 --- a/setuptools/_distutils/tests/test_filelist.py +++ b/setuptools/_distutils/tests/test_filelist.py @@ -10,7 +10,7 @@ import jaraco.path import pytest -from .compat import py38 as os_helper +from .compat import py39 as os_helper MANIFEST_IN = """\ include ok diff --git a/setuptools/_distutils/tests/test_spawn.py b/setuptools/_distutils/tests/test_spawn.py index fd7b669cbf..3b9fc926f6 100644 --- a/setuptools/_distutils/tests/test_spawn.py +++ b/setuptools/_distutils/tests/test_spawn.py @@ -12,7 +12,7 @@ import pytest from test.support import unix_shell -from .compat import py38 as os_helper +from .compat import py39 as os_helper class TestSpawn(support.TempdirManager): @@ -73,9 +73,12 @@ def test_find_executable(self, tmp_path): # PATH='': no match, except in the current directory with os_helper.EnvironmentVarGuard() as env: env['PATH'] = '' - with mock.patch( - 'distutils.spawn.os.confstr', return_value=tmp_dir, create=True - ), mock.patch('distutils.spawn.os.defpath', tmp_dir): + with ( + mock.patch( + 'distutils.spawn.os.confstr', return_value=tmp_dir, create=True + ), + mock.patch('distutils.spawn.os.defpath', tmp_dir), + ): rv = find_executable(program) assert rv is None @@ -87,9 +90,10 @@ def test_find_executable(self, tmp_path): # PATH=':': explicitly looks in the current directory with os_helper.EnvironmentVarGuard() as env: env['PATH'] = os.pathsep - with mock.patch( - 'distutils.spawn.os.confstr', return_value='', create=True - ), mock.patch('distutils.spawn.os.defpath', ''): + with ( + mock.patch('distutils.spawn.os.confstr', return_value='', create=True), + mock.patch('distutils.spawn.os.defpath', ''), + ): rv = find_executable(program) assert rv is None @@ -103,16 +107,22 @@ def test_find_executable(self, tmp_path): env.pop('PATH', None) # without confstr - with mock.patch( - 'distutils.spawn.os.confstr', side_effect=ValueError, create=True - ), mock.patch('distutils.spawn.os.defpath', tmp_dir): + with ( + mock.patch( + 'distutils.spawn.os.confstr', side_effect=ValueError, create=True + ), + mock.patch('distutils.spawn.os.defpath', tmp_dir), + ): rv = find_executable(program) assert rv == filename # with confstr - with mock.patch( - 'distutils.spawn.os.confstr', return_value=tmp_dir, create=True - ), mock.patch('distutils.spawn.os.defpath', ''): + with ( + mock.patch( + 'distutils.spawn.os.confstr', return_value=tmp_dir, create=True + ), + mock.patch('distutils.spawn.os.defpath', ''), + ): rv = find_executable(program) assert rv == filename diff --git a/setuptools/_distutils/tests/test_sysconfig.py b/setuptools/_distutils/tests/test_sysconfig.py index 49274a36ae..867e7dcb39 100644 --- a/setuptools/_distutils/tests/test_sysconfig.py +++ b/setuptools/_distutils/tests/test_sysconfig.py @@ -130,9 +130,11 @@ def test_customize_compiler(self): comp = self.customize_compiler() assert comp.exes['archiver'] == 'env_ar --env-arflags' assert comp.exes['preprocessor'] == 'env_cpp --env-cppflags' - assert comp.exes['compiler'] == 'env_cc --sc-cflags --env-cflags --env-cppflags' + assert ( + comp.exes['compiler'] == 'env_cc --env-cflags --env-cflags --env-cppflags' + ) assert comp.exes['compiler_so'] == ( - 'env_cc --sc-cflags --env-cflags --env-cppflags --sc-ccshared' + 'env_cc --env-cflags --env-cflags --env-cppflags --sc-ccshared' ) assert ( comp.exes['compiler_cxx'] diff --git a/setuptools/_distutils/tests/test_unixccompiler.py b/setuptools/_distutils/tests/test_unixccompiler.py index 50b66544a8..2c2f4aaec2 100644 --- a/setuptools/_distutils/tests/test_unixccompiler.py +++ b/setuptools/_distutils/tests/test_unixccompiler.py @@ -12,7 +12,7 @@ import pytest from . import support -from .compat.py38 import EnvironmentVarGuard +from .compat.py39 import EnvironmentVarGuard @pytest.fixture(autouse=True) @@ -272,13 +272,12 @@ def gcvs(*args, _orig=sysconfig.get_config_vars): sysconfig.get_config_var = gcv sysconfig.get_config_vars = gcvs - with mock.patch.object( - self.cc, 'spawn', return_value=None - ) as mock_spawn, mock.patch.object( - self.cc, '_need_link', return_value=True - ), mock.patch.object( - self.cc, 'mkpath', return_value=None - ), EnvironmentVarGuard() as env: + with ( + mock.patch.object(self.cc, 'spawn', return_value=None) as mock_spawn, + mock.patch.object(self.cc, '_need_link', return_value=True), + mock.patch.object(self.cc, 'mkpath', return_value=None), + EnvironmentVarGuard() as env, + ): env['CC'] = 'ccache my_cc' env['CXX'] = 'my_cxx' del env['LDSHARED'] diff --git a/setuptools/_distutils/tests/test_version.py b/setuptools/_distutils/tests/test_version.py index 1508e1cc0a..b68f097724 100644 --- a/setuptools/_distutils/tests/test_version.py +++ b/setuptools/_distutils/tests/test_version.py @@ -53,9 +53,9 @@ def test_cmp_strict(self): res = StrictVersion(v1)._cmp(v2) assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = StrictVersion(v1)._cmp(object()) - assert ( - res is NotImplemented - ), f'cmp({v1}, {v2}) should be NotImplemented, got {res}' + assert res is NotImplemented, ( + f'cmp({v1}, {v2}) should be NotImplemented, got {res}' + ) def test_cmp(self): versions = ( @@ -75,6 +75,6 @@ def test_cmp(self): res = LooseVersion(v1)._cmp(v2) assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = LooseVersion(v1)._cmp(object()) - assert ( - res is NotImplemented - ), f'cmp({v1}, {v2}) should be NotImplemented, got {res}' + assert res is NotImplemented, ( + f'cmp({v1}, {v2}) should be NotImplemented, got {res}' + ) diff --git a/setuptools/_distutils/text_file.py b/setuptools/_distutils/text_file.py index fec29c73b0..89d9048d59 100644 --- a/setuptools/_distutils/text_file.py +++ b/setuptools/_distutils/text_file.py @@ -133,9 +133,9 @@ def gen_error(self, msg, line=None): line = self.current_line outmsg.append(self.filename + ", ") if isinstance(line, (list, tuple)): - outmsg.append("lines %d-%d: " % tuple(line)) + outmsg.append("lines {}-{}: ".format(*line)) else: - outmsg.append("line %d: " % line) + outmsg.append(f"line {int(line)}: ") outmsg.append(str(msg)) return "".join(outmsg) diff --git a/setuptools/_distutils/util.py b/setuptools/_distutils/util.py index 609c1a50cd..83ad39e958 100644 --- a/setuptools/_distutils/util.py +++ b/setuptools/_distutils/util.py @@ -25,7 +25,7 @@ from .spawn import spawn -def get_host_platform(): +def get_host_platform() -> str: """ Return a string that identifies the current platform. Use this function to distinguish platform-specific build directories and @@ -34,15 +34,7 @@ def get_host_platform(): # This function initially exposed platforms as defined in Python 3.9 # even with older Python versions when distutils was split out. - # Now it delegates to stdlib sysconfig, but maintains compatibility. - - if sys.version_info < (3, 9): - if os.name == "posix" and hasattr(os, 'uname'): - osname, host, release, version, machine = os.uname() - if osname[:3] == "aix": - from .compat.py38 import aix_platform - - return aix_platform(osname, version, release) + # Now it delegates to stdlib sysconfig. return sysconfig.get_platform() @@ -288,7 +280,7 @@ def split_quoted(s): elif s[end] == '"': # slurp doubly-quoted string m = _dquote_re.match(s, end) else: - raise RuntimeError("this can't happen (bad char '%c')" % s[end]) + raise RuntimeError(f"this can't happen (bad char '{s[end]}')") if m is None: raise ValueError(f"bad string (mismatched {s[end]} quotes?)") @@ -503,3 +495,8 @@ def is_mingw(): get_platform() starts with 'mingw'. """ return sys.platform == 'win32' and get_platform().startswith('mingw') + + +def is_freethreaded(): + """Return True if the Python interpreter is built with free threading support.""" + return bool(sysconfig.get_config_var('Py_GIL_DISABLED')) diff --git a/setuptools/_distutils/version.py b/setuptools/_distutils/version.py index 942b56bf94..2223ee9c8c 100644 --- a/setuptools/_distutils/version.py +++ b/setuptools/_distutils/version.py @@ -53,8 +53,7 @@ def __init__(self, vstring=None): if vstring: self.parse(vstring) warnings.warn( - "distutils Version classes are deprecated. " - "Use packaging.version instead.", + "distutils Version classes are deprecated. Use packaging.version instead.", DeprecationWarning, stacklevel=2, )