diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e841bde57c..cbfd594433 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,6 +34,24 @@ env: jobs: + pyright: + strategy: + matrix: + python: + - "3.8" + - "3.11" + - "3.12" + platform: + - ubuntu-latest + - macos-latest + - windows-latest + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + - uses: jakebailey/pyright-action@v2 + with: + python-version: ${{ matrix.python }} + test: strategy: matrix: diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index c9d1e24790..68441354fb 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -54,6 +54,7 @@ def ensure_local_distutils(): # check that submodules load as expected core = importlib.import_module('distutils.core') + assert core.__file__ is not None, core.__file__ assert '_distutils' in core.__file__, core.__file__ assert 'setuptools._distutils.log' not in sys.modules diff --git a/docs/conf.py b/docs/conf.py index 0a82ff2fe2..26e6375dbe 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,6 @@ +from typing import Dict, Tuple + + extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', @@ -93,7 +96,7 @@ # Include Python intersphinx mapping to prevent failures # jaraco/skeleton#51 extensions += ['sphinx.ext.intersphinx'] -intersphinx_mapping = { +intersphinx_mapping: Dict[str, Tuple[str, None]] = { 'python': ('https://docs.python.org/3', None), } diff --git a/mypy.ini b/mypy.ini index b6f972769e..b007c27bf2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,21 @@ [mypy] +# CI should test for all versions, local development gets hints for oldest supported +python_version = 3.8 +strict = False +warn_unused_ignores = True +# TODO: Not all dependencies are typed. setuptools itself should be typed too +# TODO: Test environment is not yet properly configured to install all imported packages ignore_missing_imports = True -# required to support namespace packages -# https://github.com/python/mypy/issues/14057 +# required to support namespace packages: https://github.com/python/mypy/issues/14057 explicit_package_bases = True +exclude = (?x)( + ^build/ + | ^.tox/ + | ^pkg_resources/tests/data/my-test-package-source/setup.py$ # Duplicate module name + | ^.+?/(_vendor|extern)/ # Vendored + | ^setuptools/_distutils/ # Vendored + ) + +# https://github.com/pypa/setuptools/pull/3979#discussion_r1367968993 +[mypy-pkg_resources.extern.*,setuptools.extern.*] +ignore_missing_imports = True diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index d6847448eb..5ba791ab1c 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -27,7 +27,7 @@ import time import re import types -from typing import Protocol +from typing import Any, Protocol, cast import zipfile import zipimport import warnings @@ -71,8 +71,8 @@ join_continuation, ) -from pkg_resources.extern import platformdirs -from pkg_resources.extern import packaging +from pkg_resources.extern import platformdirs # type: ignore[attr-defined] +from pkg_resources.extern import packaging # type: ignore[attr-defined] __import__('pkg_resources.extern.packaging.version') __import__('pkg_resources.extern.packaging.specifiers') @@ -80,25 +80,25 @@ __import__('pkg_resources.extern.packaging.markers') __import__('pkg_resources.extern.packaging.utils') -# declare some globals that will be defined later to -# satisfy the linters. -require = None -working_set = None -add_activation_listener = None -resources_stream = None -cleanup_resources = None -resource_dir = None -resource_stream = None -set_extraction_path = None -resource_isdir = None -resource_string = None -iter_entry_points = None -resource_listdir = None -resource_filename = None -resource_exists = None -_distribution_finders = None -_namespace_handlers = None -_namespace_packages = None +def _dummy(*args: object, **kwargs: object) -> Any: + pass + +# declare some globals that will be defined later to satisfy linters. +require = _dummy +working_set = cast("WorkingSet", None) +add_activation_listener = _dummy +cleanup_resources = _dummy +resource_stream = _dummy +set_extraction_path = _dummy +resource_isdir = _dummy +resource_string = _dummy +iter_entry_points = _dummy +resource_listdir = _dummy +resource_filename = _dummy +resource_exists = _dummy +_distribution_finders: dict = {} +_namespace_handlers: dict = {} +_namespace_packages: dict = {} warnings.warn( @@ -3210,6 +3210,7 @@ def _find_adapter(registry, ob): for t in types: if t in registry: return registry[t] + raise ValueError("Adapter not found") def ensure_directory(path): diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py index 0883642080..bfbf619c85 100644 --- a/pkg_resources/tests/test_pkg_resources.py +++ b/pkg_resources/tests/test_pkg_resources.py @@ -9,6 +9,7 @@ import stat import distutils.dist import distutils.command.install_egg_info +from typing import List from unittest import mock @@ -32,7 +33,7 @@ def __call__(self): class TestZipProvider: - finalizers = [] + finalizers: List[EggRemover] = [] ref_time = datetime.datetime(2013, 5, 12, 13, 25, 0) "A reference time for a file modification" diff --git a/pkg_resources/tests/test_resources.py b/pkg_resources/tests/test_resources.py index 5b2308aea7..ea296a56b4 100644 --- a/pkg_resources/tests/test_resources.py +++ b/pkg_resources/tests/test_resources.py @@ -5,7 +5,7 @@ import itertools import pytest -from pkg_resources.extern import packaging +from pkg_resources.extern import packaging # type: ignore[attr-defined] import pkg_resources from pkg_resources import ( diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000000..ea08e3f487 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/vscode-pyright/schemas/pyrightconfig.schema.json", + "exclude": [ + "build", + ".tox", + "**/extern", // Vendored + "**/_vendor", // Vendored + "setuptools/_distutils", // Vendored + ], + // CI should test for all versions, local development gets hints for oldest supported + "pythonVersion": "3.8", + // For now we don't mind if mypy's `type: ignore` comments accidentally suppresses pyright issues + "enableTypeIgnoreComments": true, + "typeCheckingMode": "basic", + // TODO: Test environment is not yet properly configured to install all imported packages + "reportMissingImports": "none", + // Too many issues caused by vendoring and dynamic patching, still worth fixing when we can + "reportAttributeAccessIssue": "warning", + // Defered initialization (initialize_options/finalize_options) causes many "potentially None" issues + // TODO: Fix with type-guards or by changing how it's initialized + "reportCallIssue": "warning", + "reportArgumentType": "warning", + "reportOptionalIterable": "warning", + "reportOptionalMemberAccess": "warning", + "reportGeneralTypeIssues": "warning", + "reportOptionalOperand": "warning", +} diff --git a/setup.py b/setup.py index 075d7c405f..1a6074766a 100755 --- a/setup.py +++ b/setup.py @@ -88,5 +88,6 @@ def _restore_install_lib(self): if __name__ == '__main__': # allow setup.py to run from another directory - here and os.chdir(here) + # TODO: Use a proper conditonal statement here + here and os.chdir(here) # type: ignore[func-returns-value] dist = setuptools.setup(**setup_params) diff --git a/setuptools/__init__.py b/setuptools/__init__.py index 563ca1c4ba..6f3a95bc20 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -1,8 +1,10 @@ """Extensions to the 'distutils' for large or complex distributions""" +from collections.abc import Mapping import functools import os import re +from typing import Any import _distutils_hack.override # noqa: F401 import distutils.core @@ -46,7 +48,7 @@ class MinimalDistribution(distutils.core.Distribution): fetch_build_eggs interface. """ - def __init__(self, attrs): + def __init__(self, attrs: Mapping[str, Any]): _incl = 'dependency_links', 'setup_requires' filtered = {k: attrs[k] for k in set(_incl) & set(attrs)} super().__init__(filtered) @@ -109,7 +111,7 @@ def setup(**attrs): _Command = monkey.get_unpatched(distutils.core.Command) -class Command(_Command): +class Command(_Command): # type: ignore[valid-type, misc] # https://github.com/python/mypy/issues/14458 """ Setuptools internal actions are organized using a *command design pattern*. This means that each action (or group of closely related actions) executed during diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py index 4bf3c7c947..9ca127d425 100644 --- a/setuptools/_core_metadata.py +++ b/setuptools/_core_metadata.py @@ -62,7 +62,8 @@ def _read_list_from_msg(msg: Message, field: str) -> Optional[List[str]]: def _read_payload_from_msg(msg: Message) -> Optional[str]: - value = msg.get_payload().strip() + payload = msg.get_payload() + value = payload.strip() if isinstance(payload, str) else "" if value == 'UNKNOWN' or not value: return None return value diff --git a/setuptools/_entry_points.py b/setuptools/_entry_points.py index 747a69067e..103700d9b3 100644 --- a/setuptools/_entry_points.py +++ b/setuptools/_entry_points.py @@ -8,6 +8,7 @@ from ._importlib import metadata from ._itertools import ensure_unique from .extern.more_itertools import consume +metadata_EntryPoints = metadata.EntryPoints def ensure_valid(ep): @@ -33,14 +34,14 @@ def load_group(value, group): # normalize to a single sequence of lines lines = yield_lines(value) text = f'[{group}]\n' + '\n'.join(lines) - return metadata.EntryPoints._from_text(text) + return metadata_EntryPoints._from_text(text) def by_group_and_name(ep): return ep.group, ep.name -def validate(eps: metadata.EntryPoints): +def validate(eps: metadata_EntryPoints): """ Ensure entry points are unique by group and name and validate each. """ @@ -56,7 +57,7 @@ def load(eps): groups = itertools.chain.from_iterable( load_group(value, group) for group, value in eps.items() ) - return validate(metadata.EntryPoints(groups)) + return validate(metadata_EntryPoints(groups)) @load.register(str) @@ -70,14 +71,14 @@ def _(eps): >>> ep.value 'bar' """ - return validate(metadata.EntryPoints(metadata.EntryPoints._from_text(eps))) + return validate(metadata_EntryPoints(metadata_EntryPoints._from_text(eps))) load.register(type(None), lambda x: x) @pass_none -def render(eps: metadata.EntryPoints): +def render(eps: metadata_EntryPoints): by_group = operator.attrgetter('group') groups = itertools.groupby(sorted(eps, key=by_group), by_group) diff --git a/setuptools/_importlib.py b/setuptools/_importlib.py index bd2b01e2b5..73ec89453b 100644 --- a/setuptools/_importlib.py +++ b/setuptools/_importlib.py @@ -38,14 +38,14 @@ def disable_importlib_metadata_finder(metadata): if sys.version_info < (3, 10): - from setuptools.extern import importlib_metadata as metadata + from setuptools.extern import importlib_metadata as metadata # type: ignore[attr-defined] disable_importlib_metadata_finder(metadata) else: import importlib.metadata as metadata # noqa: F401 - +metadata=metadata if sys.version_info < (3, 9): - from setuptools.extern import importlib_resources as resources + from setuptools.extern import importlib_resources as resources # type: ignore[attr-defined] else: import importlib.resources as resources # noqa: F401 diff --git a/setuptools/_normalization.py b/setuptools/_normalization.py index 8d4731eb60..5435562bd1 100644 --- a/setuptools/_normalization.py +++ b/setuptools/_normalization.py @@ -4,12 +4,9 @@ """ import re -from pathlib import Path -from typing import Union -from .extern import packaging +from .extern import packaging # type: ignore[attr-defined] -_Path = Union[str, Path] # https://packaging.python.org/en/latest/specifications/core-metadata/#name _VALID_NAME = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.I) diff --git a/setuptools/_reqs.py b/setuptools/_reqs.py index 9f83437033..41b10a5fc8 100644 --- a/setuptools/_reqs.py +++ b/setuptools/_reqs.py @@ -24,13 +24,9 @@ def parse_strings(strs: _StrOrIter) -> Iterator[str]: @overload -def parse(strs: _StrOrIter) -> Iterator[Requirement]: ... - - +def parse(strs: _StrOrIter) -> "map[Requirement]": ... @overload -def parse(strs: _StrOrIter, parser: Callable[[str], _T]) -> Iterator[_T]: ... - - +def parse(strs: _StrOrIter, parser: Callable[[str], _T]) -> "map[_T]": ... def parse(strs, parser=parse_req): """ Replacement for ``pkg_resources.parse_requirements`` that uses ``packaging``. diff --git a/setuptools/command/__init__.py b/setuptools/command/__init__.py index 5acd7687d6..4688b34aac 100644 --- a/setuptools/command/__init__.py +++ b/setuptools/command/__init__.py @@ -3,7 +3,7 @@ if 'egg' not in bdist.format_commands: try: - bdist.format_commands['egg'] = ('bdist_egg', "Python .egg file") + bdist.format_commands['egg'] = ('bdist_egg', "Python .egg file") # pyright: ignore[reportCallIssue, reportArgumentType] # for backwards compatibility except TypeError: # For backward compatibility with older distutils (stdlib) bdist.format_command['egg'] = ('bdist_egg', "Python .egg file") diff --git a/setuptools/command/_requirestxt.py b/setuptools/command/_requirestxt.py index 7b732b11ab..b0c2d7059a 100644 --- a/setuptools/command/_requirestxt.py +++ b/setuptools/command/_requirestxt.py @@ -35,7 +35,7 @@ def _prepare( def _convert_extras_requirements( - extras_require: _StrOrIter, + extras_require: Mapping[str, _StrOrIter], ) -> Mapping[str, _Ordered[Requirement]]: """ Convert requirements in `extras_require` of the form diff --git a/setuptools/command/build.py b/setuptools/command/build.py index afda7e3be9..197e84956f 100644 --- a/setuptools/command/build.py +++ b/setuptools/command/build.py @@ -1,3 +1,4 @@ +from abc import abstractmethod from typing import Dict, List, Protocol from distutils.command.build import build as _build @@ -98,13 +99,17 @@ def finalize_options(self): def initialize_options(self): """(Required by the original :class:`setuptools.Command` interface)""" + ... def finalize_options(self): """(Required by the original :class:`setuptools.Command` interface)""" + ... def run(self): """(Required by the original :class:`setuptools.Command` interface)""" + ... + @abstractmethod def get_source_files(self) -> List[str]: """ Return a list of all files that are used by the command to create the expected @@ -115,7 +120,9 @@ def get_source_files(self) -> List[str]: with all the files necessary to build the distribution. All files should be strings relative to the project root directory. """ + ... + @abstractmethod def get_outputs(self) -> List[str]: """ Return a list of files intended for distribution as they would have been @@ -128,7 +135,9 @@ def get_outputs(self) -> List[str]: in ``get_output_mapping()`` plus files that are generated during the build and don't correspond to any source file already present in the project. """ + ... + @abstractmethod def get_output_mapping(self) -> Dict[str, str]: """ Return a mapping between destination files as they would be produced by the @@ -138,3 +147,4 @@ def get_output_mapping(self) -> Dict[str, str]: Destination files should be strings in the form of ``"{build_lib}/destination/file/path"``. """ + ... diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py index ef2a4da84d..51bc38e60a 100644 --- a/setuptools/command/build_ext.py +++ b/setuptools/command/build_ext.py @@ -3,10 +3,9 @@ import itertools from importlib.machinery import EXTENSION_SUFFIXES from importlib.util import cache_from_source as _compiled_file_name -from typing import Dict, Iterator, List, Tuple +from typing import TYPE_CHECKING, Dict, Iterator, List, Tuple from pathlib import Path -from distutils.command.build_ext import build_ext as _du_build_ext from distutils.ccompiler import new_compiler from distutils.sysconfig import customize_compiler, get_config_var from distutils import log @@ -14,19 +13,23 @@ from setuptools.errors import BaseError from setuptools.extension import Extension, Library -try: - # Attempt to use Cython for building extensions, if available - from Cython.Distutils.build_ext import build_ext as _build_ext +if TYPE_CHECKING: + from setuptools.dist import Distribution + from distutils.command.build_ext import build_ext as _build_ext +else: + try: + # Attempt to use Cython for building extensions, if available + from Cython.Distutils.build_ext import build_ext as _build_ext - # Additionally, assert that the compiler module will load - # also. Ref #1229. - __import__('Cython.Compiler.Main') -except ImportError: - _build_ext = _du_build_ext + # Additionally, assert that the compiler module will load + # also. Ref #1229. + __import__('Cython.Compiler.Main') + except ImportError: + from distutils.command.build_ext import build_ext as _build_ext # make sure _config_vars is initialized get_config_var("LDSHARED") -from distutils.sysconfig import _config_vars as _CONFIG_VARS # noqa +from distutils.sysconfig import _config_vars as _CONFIG_VARS # type: ignore # noqa # Not publicly exposed in distutils stubs def _customize_compiler_for_shlib(compiler): @@ -76,11 +79,13 @@ def get_abi3_suffix(): return suffix elif suffix == '.pyd': # Windows return suffix + raise ValueError("suffix not found") class build_ext(_build_ext): editable_mode: bool = False inplace: bool = False + distribution: "Distribution" def run(self): """Build extensions in build directory, then copy if --inplace""" @@ -97,7 +102,8 @@ def _get_inplace_equivalent(self, build_py, ext: Extension) -> Tuple[str, str]: package = '.'.join(modpath[:-1]) package_dir = build_py.get_package_dir(package) inplace_file = os.path.join(package_dir, os.path.basename(filename)) - regular_file = os.path.join(self.build_lib, filename) + # FIXME: How is `self.build_lib` anything else than None? + regular_file = os.path.join(self.build_lib, filename) # pyright: ignore[reportCallIssue, reportArgumentType] return (inplace_file, regular_file) def copy_extensions_to_source(self): @@ -127,7 +133,7 @@ def _get_output_mapping(self) -> Iterator[Tuple[str, str]]: return build_py = self.get_finalized_command('build_py') - opt = self.get_finalized_command('install_lib').optimize or "" + opt = self.get_finalized_command('install_lib').optimize or "" # type: ignore[attr-defined] # TODO: Fix in distutils stubs for ext in self.extensions: inplace_file, regular_file = self._get_inplace_equivalent(build_py, ext) @@ -147,7 +153,7 @@ def _get_output_mapping(self) -> Iterator[Tuple[str, str]]: output_cache = _compiled_file_name(regular_stub, optimization=opt) yield (output_cache, inplace_cache) - def get_ext_filename(self, fullname): + def get_ext_filename(self, fullname: str): so_ext = os.getenv('SETUPTOOLS_EXT_SUFFIX') if so_ext: filename = os.path.join(*fullname.split('.')) + so_ext @@ -338,7 +344,7 @@ def _write_stub_file(self, stub_file: str, ext: Extension, compile=False): log.info("writing stub loader for %s to %s", ext._full_name, stub_file) if compile and os.path.exists(stub_file): raise BaseError(stub_file + " already exists! Please delete.") - if not self.dry_run: + if not self.dry_run: # type: ignore[attr-defined] # TODO: Fix in distutils stubs f = open(stub_file, 'w') f.write( '\n'.join([ @@ -373,13 +379,13 @@ def _write_stub_file(self, stub_file: str, ext: Extension, compile=False): def _compile_and_remove_stub(self, stub_file: str): from distutils.util import byte_compile - byte_compile([stub_file], optimize=0, force=True, dry_run=self.dry_run) - optimize = self.get_finalized_command('install_lib').optimize + byte_compile([stub_file], optimize=0, force=True, dry_run=self.dry_run) # type: ignore[attr-defined] # TODO: Fix in distutils stubs + optimize = self.get_finalized_command('install_lib').optimize # type: ignore[attr-defined] # TODO: Fix in distutils stubs if optimize > 0: byte_compile( - [stub_file], optimize=optimize, force=True, dry_run=self.dry_run + [stub_file], optimize=optimize, force=True, dry_run=self.dry_run # type: ignore[attr-defined] # TODO: Fix in distutils stubs ) - if os.path.exists(stub_file) and not self.dry_run: + if os.path.exists(stub_file) and not self.dry_run: # type: ignore[attr-defined] # TODO: Fix in distutils stubs os.unlink(stub_file) diff --git a/setuptools/command/build_py.py b/setuptools/command/build_py.py index 3f40b060b3..4861a9a366 100644 --- a/setuptools/command/build_py.py +++ b/setuptools/command/build_py.py @@ -218,7 +218,7 @@ def _filter_build_files(self, files: Iterable[str], egg_info: str) -> Iterator[s This function should filter this case of invalid files out. """ build = self.get_finalized_command("build") - build_dirs = (egg_info, self.build_lib, build.build_temp, build.build_base) + build_dirs = (egg_info, self.build_lib, build.build_temp, build.build_base) # type: ignore[attr-defined] # TODO: Fix in distutils stubs norm_dirs = [os.path.normpath(p) for p in build_dirs if p] for file in files: diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index f73d857f08..1127548490 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -11,6 +11,7 @@ """ from glob import glob +from typing import TYPE_CHECKING, Optional, Union from distutils.util import get_platform from distutils.util import convert_path, subst_vars from distutils.errors import ( @@ -25,6 +26,7 @@ from distutils.command import install import sys import os +from typing import TYPE_CHECKING, Dict, List, Optional, Union import zipimport import shutil import tempfile @@ -78,6 +80,8 @@ from .._path import ensure_directory from ..extern.jaraco.text import yield_lines +if TYPE_CHECKING: + _FileDescriptorOrPath = Union[int, str, bytes, os.PathLike[str], os.PathLike[bytes]] # Turn on PEP440Warnings warnings.filterwarnings("default", category=pkg_resources.PEP440Warning) @@ -1645,7 +1649,7 @@ def _load_raw(self): while paths and not paths[-1].strip(): paths.pop() dirty = True - return paths, dirty or (paths and saw_import) + return paths, dirty or bool(paths and saw_import) def _load(self): if os.path.isfile(self.filename): @@ -1766,7 +1770,7 @@ def _wrap_lines(cls, lines): if os.environ.get('SETUPTOOLS_SYS_PATH_TECHNIQUE', 'raw') == 'rewrite': - PthDistributions = RewritePthDistributions + PthDistributions = RewritePthDistributions # type: ignore[misc] # Overwriting type def _first_line_re(): @@ -1787,7 +1791,7 @@ def auto_chmod(func, arg, exc): return func(arg) et, ev, _ = sys.exc_info() # TODO: This code doesn't make sense. What is it trying to do? - raise (ev[0], ev[1] + (" %s %s" % (func, arg))) + raise (ev[0], ev[1] + (" %s %s" % (func, arg))) # pyright: ignore[reportGeneralTypeIssues, reportIndexIssue, reportOptionalSubscript] def update_dist_caches(dist_path, fix_zipimporter_caches): @@ -2016,7 +2020,13 @@ def is_python_script(script_text, filename): from os import chmod as _chmod except ImportError: # Jython compatibility - def _chmod(*args): + def _chmod( + path: "_FileDescriptorOrPath", + mode: int, + *, + dir_fd: Optional[int] = None, + follow_symlinks: bool = True, + ) -> None: pass @@ -2034,8 +2044,8 @@ class CommandSpec(list): those passed to Popen. """ - options = [] - split_args = dict() + options: List[str] = [] + split_args: Dict[str, bool] = dict() @classmethod def best(cls): diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index 8a4ae7928f..8b2582f23b 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -1,3 +1,5 @@ +# pyright: reportOptionalMemberAccess=false +# TODO: Fix get_command_obj in distutils stubs (which should create by default) """ Create a wheel that, when installed, will make the source package 'editable' (add it to the interpreter's path, including metadata) per PEP 660. Replaces @@ -266,7 +268,7 @@ def _run_build_commands( self._run_install("data") return files, mapping - def _run_build_subcommands(self): + def _run_build_subcommands(self) -> None: """ Issue #3501 indicates that some plugins/customizations might rely on: @@ -280,7 +282,7 @@ def _run_build_subcommands(self): # TODO: Once plugins/customisations had the chance to catch up, replace # `self._run_build_subcommands()` with `self.run_command("build")`. # Also remove _safely_run, TestCustomBuildPy. Suggested date: Aug/2023. - build: Command = self.get_finalized_command("build") + build = self.get_finalized_command("build") for name in build.get_sub_commands(): cmd = self.get_finalized_command(name) if name == "build_py" and type(cmd) != build_py_cls: @@ -355,7 +357,7 @@ def _select_strategy( name: str, tag: str, build_lib: _Path, - ) -> "EditableStrategy": + ) -> "_LinkTree | _StaticPth | _TopLevelFinder": """Decides which strategy to use to implement an editable installation.""" build_name = f"__editable__.{name}-{tag}" project_dir = Path(self.project_dir) @@ -600,7 +602,7 @@ def _simple_layout( layout = {pkg: find_package_path(pkg, package_dir, project_dir) for pkg in packages} if not layout: return set(package_dir) in ({}, {""}) - parent = os.path.commonpath(starmap(_parent_path, layout.items())) + parent = os.path.commonpath(starmap(_parent_path, layout.items())) # type: ignore[call-overload] # FIXME upstream return all( _path.same_path(Path(parent, *key.split('.')), value) for key, value in layout.items() diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 62d2feea9b..996950e158 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -27,7 +27,7 @@ import setuptools.unicode_utils as unicode_utils from setuptools.glob import glob -from setuptools.extern import packaging +from setuptools.extern import packaging # type: ignore[attr-defined] from ..warnings import SetuptoolsDeprecationWarning @@ -559,7 +559,7 @@ def run(self): def _manifest_normalize(self, path): path = unicode_utils.filesys_decode(path) - return path.replace(os.sep, '/') + return path.replace(os.sep, '/') # pyright: ignore[reportOptionalMemberAccess] # Raise error on None def write_manifest(self): """ diff --git a/setuptools/command/install.py b/setuptools/command/install.py index 606cce9d89..ff5a7b8c49 100644 --- a/setuptools/command/install.py +++ b/setuptools/command/install.py @@ -130,7 +130,9 @@ def do_egg_install(self): cmd.package_index.scan(glob.glob('*.egg')) self.run_command('bdist_egg') - args = [self.distribution.get_command_obj('bdist_egg').egg_output] + + # TODO: Fix get_command_obj in distutils stubs (which should create by default) + args = [self.distribution.get_command_obj('bdist_egg').egg_output] # pyright: ignore[reportOptionalMemberAccess] if setuptools.bootstrap_install_from: # Bootstrap self-installation of setuptools diff --git a/setuptools/command/rotate.py b/setuptools/command/rotate.py index cfb78ce52d..6f73721c70 100644 --- a/setuptools/command/rotate.py +++ b/setuptools/command/rotate.py @@ -3,6 +3,7 @@ from distutils.errors import DistutilsOptionError import os import shutil +from typing import List from setuptools import Command @@ -17,7 +18,7 @@ class rotate(Command): ('keep=', 'k', "number of matching distributions to keep"), ] - boolean_options = [] + boolean_options: List[str] = [] def initialize_options(self): self.match = None diff --git a/setuptools/command/test.py b/setuptools/command/test.py index 0a128f2a7a..4e609b779c 100644 --- a/setuptools/command/test.py +++ b/setuptools/command/test.py @@ -1,8 +1,11 @@ +from collections.abc import Callable import os import operator import sys import contextlib import itertools +from typing import Generic, List, TypeVar, overload +from typing_extensions import Self import unittest from distutils.errors import DistutilsError, DistutilsOptionError from distutils import log @@ -61,12 +64,16 @@ def loadTestsFromModule(self, module, pattern=None): else: return tests[0] # don't create a nested suite for only one return - +_T = TypeVar("_T") # adapted from jaraco.classes.properties:NonDataProperty -class NonDataProperty: - def __init__(self, fget): +class NonDataProperty(Generic[_T]): + def __init__(self, fget: Callable[..., _T]): self.fget = fget + @overload + def __get__(self, obj: None, objtype: object=None) -> Self: ... + @overload + def __get__(self, obj: object, objtype=None) -> _T: ... def __get__(self, obj, objtype=None): if obj is None: return self @@ -113,7 +120,7 @@ def finalize_options(self): self.test_runner = getattr(self.distribution, 'test_runner', None) @NonDataProperty - def test_args(self): + def test_args(self) -> List[str]: return list(self._test_args()) def _test_args(self): @@ -168,8 +175,7 @@ def paths_on_pythonpath(paths): Do this in a context that restores the value on exit. """ - nothing = object() - orig_pythonpath = os.environ.get('PYTHONPATH', nothing) + orig_pythonpath = os.environ.get('PYTHONPATH') current_pythonpath = os.environ.get('PYTHONPATH', '') try: prefix = os.pathsep.join(unique_everseen(paths)) @@ -179,7 +185,7 @@ def paths_on_pythonpath(paths): os.environ['PYTHONPATH'] = new_path yield finally: - if orig_pythonpath is nothing: + if orig_pythonpath is None: os.environ.pop('PYTHONPATH', None) else: os.environ['PYTHONPATH'] = orig_pythonpath diff --git a/setuptools/command/upload_docs.py b/setuptools/command/upload_docs.py index 32c9abd796..6da37692d2 100644 --- a/setuptools/command/upload_docs.py +++ b/setuptools/command/upload_docs.py @@ -50,7 +50,7 @@ def has_sphinx(self): and metadata.entry_points(group='distutils.commands', name='build_sphinx') ) - sub_commands = [('build_sphinx', has_sphinx)] + sub_commands = [('build_sphinx', has_sphinx)] # type: ignore[list-item] # distutils stubs issue w/ Python 3.12 def initialize_options(self): upload.initialize_options(self) @@ -76,6 +76,8 @@ def finalize_options(self): self.announce('Using upload directory %s' % self.target_dir) def create_zipfile(self, filename): + if self.target_dir is None: + raise ValueError("self.target_dir cannot be None") zip_file = zipfile.ZipFile(filename, "w") try: self.mkpath(self.target_dir) # just in case diff --git a/setuptools/config/__init__.py b/setuptools/config/__init__.py index fcc7d008d6..7d7719df2d 100644 --- a/setuptools/config/__init__.py +++ b/setuptools/config/__init__.py @@ -3,19 +3,22 @@ """ from functools import wraps -from typing import Callable, TypeVar, cast +from typing import Callable, TypeVar + +from typing_extensions import ParamSpec from ..warnings import SetuptoolsDeprecationWarning from . import setupcfg -Fn = TypeVar("Fn", bound=Callable) +_R = TypeVar("_R") +_P = ParamSpec("_P") -__all__ = ('parse_configuration', 'read_configuration') +__all__ = ("parse_configuration", "read_configuration") -def _deprecation_notice(fn: Fn) -> Fn: +def _deprecation_notice(fn: Callable[_P, _R]) -> Callable[_P, _R]: @wraps(fn) - def _wrapper(*args, **kwargs): + def _wrapper(*args: _P.args, **kwargs: _P.kwargs): SetuptoolsDeprecationWarning.emit( "Deprecated API usage.", f""" @@ -36,7 +39,7 @@ def _wrapper(*args, **kwargs): ) return fn(*args, **kwargs) - return cast(Fn, _wrapper) + return _wrapper read_configuration = _deprecation_notice(setupcfg.read_configuration) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 4261f3e218..b412b9c25c 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -27,7 +27,6 @@ Tuple, Type, Union, - cast, ) from ..errors import RemovedConfigError @@ -36,10 +35,11 @@ if TYPE_CHECKING: from setuptools._importlib import metadata # noqa from setuptools.dist import Distribution # noqa + metadata_EntryPoint = metadata.EntryPoint EMPTY: Mapping = MappingProxyType({}) # Immutable dict-like _Path = Union[os.PathLike, str] -_DictOrStr = Union[dict, str] +_DictOrStr = Union[Dict[str, str], str] _CorrespFn = Callable[["Distribution", Any, _Path], None] _Correspondence = Union[str, _CorrespFn] @@ -157,11 +157,11 @@ def _long_description(dist: "Distribution", val: _DictOrStr, root_dir: _Path): from setuptools.config import expand if isinstance(val, str): - file: Union[str, list] = val + file = val text = expand.read_files(file, root_dir) ctype = _guess_content_type(val) else: - file = val.get("file") or [] + file = val.get("file") or () text = val.get("text") or expand.read_files(file, root_dir) ctype = val["content-type"] @@ -171,7 +171,7 @@ def _long_description(dist: "Distribution", val: _DictOrStr, root_dir: _Path): _set_config(dist, "long_description_content_type", ctype) if file: - dist._referenced_files.add(cast(str, file)) + dist._referenced_files.add(file) def _license(dist: "Distribution", val: dict, root_dir: _Path): @@ -280,7 +280,7 @@ def _valid_command_options(cmdclass: Mapping = EMPTY) -> Dict[str, Set[str]]: return valid_options -def _load_ep(ep: "metadata.EntryPoint") -> Optional[Tuple[str, Type]]: +def _load_ep(ep: "metadata_EntryPoint") -> Optional[Tuple[str, Type]]: # Ignore all the errors try: return (ep.name, ep.load()) diff --git a/setuptools/config/_validate_pyproject/__init__.py b/setuptools/config/_validate_pyproject/__init__.py index dbe6cb4ca4..cddc1599ac 100644 --- a/setuptools/config/_validate_pyproject/__init__.py +++ b/setuptools/config/_validate_pyproject/__init__.py @@ -5,7 +5,7 @@ from .error_reporting import detailed_errors, ValidationError from .extra_validations import EXTRA_VALIDATIONS from .fastjsonschema_exceptions import JsonSchemaException, JsonSchemaValueException -from .fastjsonschema_validations import validate as _validate +from .fastjsonschema_validations import validate as _validate # type: ignore[attr-defined] # mypy false-positive. Pyright is fine here __all__ = [ "validate", diff --git a/setuptools/config/_validate_pyproject/error_reporting.py b/setuptools/config/_validate_pyproject/error_reporting.py index d44e290e36..be8971bc34 100644 --- a/setuptools/config/_validate_pyproject/error_reporting.py +++ b/setuptools/config/_validate_pyproject/error_reporting.py @@ -5,7 +5,7 @@ import re from contextlib import contextmanager from textwrap import indent, wrap -from typing import Any, Dict, Iterator, List, Optional, Sequence, Union, cast +from typing import Any, Dict, Iterator, List, Optional, Sequence, Union from .fastjsonschema_exceptions import JsonSchemaValueException @@ -297,7 +297,7 @@ def _value(self, value: Any, path: Sequence[str]) -> str: if path[-1] == "type" and not self._is_property(path): type_ = self._jargon(value) return ( - f"[{', '.join(type_)}]" if isinstance(value, list) else cast(str, type_) + f"[{', '.join(type_)}]" if isinstance(type_, list) else type_ ) return repr(value) diff --git a/setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py b/setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py index d2dddd6a10..bb9eee24ff 100644 --- a/setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py +++ b/setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py @@ -26,7 +26,7 @@ class JsonSchemaValueException(JsonSchemaException): Added all extra properties. """ - def __init__(self, message, value=None, name=None, definition=None, rule=None): + def __init__(self, message, value=None, name="", definition={}, rule=None): super().__init__(message) self.message = message self.value = value diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py index b48fc1187e..8d8be2eae5 100644 --- a/setuptools/config/expand.py +++ b/setuptools/config/expand.py @@ -19,7 +19,7 @@ """ import ast -import importlib +import importlib.util import os import pathlib import sys @@ -39,7 +39,6 @@ Tuple, TypeVar, Union, - cast, ) from pathlib import Path from types import ModuleType @@ -64,7 +63,7 @@ class StaticModule: """Proxy to a module object that avoids executing arbitrary code.""" def __init__(self, name: str, spec: ModuleSpec): - module = ast.parse(pathlib.Path(spec.origin).read_bytes()) + module = ast.parse(pathlib.Path(spec.origin).read_bytes()) # type: ignore[arg-type] # Let it raise an error on None vars(self).update(locals()) del self.self @@ -341,17 +340,17 @@ def version(value: Union[Callable, Iterable[Union[str, int]], str]) -> str: it should be normalised to string. """ if callable(value): - value = value() - - value = cast(Iterable[Union[str, int]], value) + _value = value() + else: + _value = value - if not isinstance(value, str): - if hasattr(value, '__iter__'): - value = '.'.join(map(str, value)) + if not isinstance(_value, str): + if hasattr(_value, '__iter__'): + _value = '.'.join(map(str, _value)) else: - value = '%s' % value + _value = '%s' % _value - return value + return _value def canonic_package_data(package_data: dict) -> dict: @@ -385,7 +384,7 @@ def entry_points(text: str, text_source="entry-points") -> Dict[str, dict]: (that correspond to the entry-point value). """ parser = ConfigParser(default_section=None, delimiters=("=",)) # type: ignore - parser.optionxform = str # case sensitive + parser.optionxform = lambda optionstr: str(optionstr) # case sensitive, support kwarg parser.read_string(text, text_source) groups = {k: dict(v.items()) for k, v in parser.items()} groups.pop(parser.default_section, None) diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index 321e106e40..88e3931181 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -23,6 +23,7 @@ if TYPE_CHECKING: from setuptools.dist import Distribution # noqa + from typing_extensions import Self _Path = Union[str, os.PathLike] _logger = logging.getLogger(__name__) @@ -271,7 +272,7 @@ def _ensure_previously_set(self, dist: "Distribution", field: str): def _expand_directive( self, specifier: str, directive, package_dir: Mapping[str, str] ): - from setuptools.extern.more_itertools import always_iterable # type: ignore + from setuptools.extern.more_itertools import always_iterable with _ignore_errors(self.ignore_option_errors): root_dir = self.root_dir @@ -296,19 +297,23 @@ def _obtain(self, dist: "Distribution", field: str, package_dir: Mapping[str, st def _obtain_version(self, dist: "Distribution", package_dir: Mapping[str, str]): # Since plugins can set version, let's silently skip if it cannot be obtained if "version" in self.dynamic and "version" in self.dynamic_cfg: - return _expand.version(self._obtain(dist, "version", package_dir)) + return _expand.version( + # We already do an early check for the presence of "version" + self._obtain(dist, "version", package_dir) # pyright: ignore[reportArgumentType] + ) return None - def _obtain_readme(self, dist: "Distribution") -> Optional[Dict[str, str]]: + def _obtain_readme(self, dist: "Distribution") -> Optional[Dict[str, Optional[str]]]: if "readme" not in self.dynamic: return None dynamic_cfg = self.dynamic_cfg if "readme" in dynamic_cfg: return { + # We already do an early check for the presence of "readme" "text": self._obtain(dist, "readme", {}), "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"), - } + } # pyright: ignore[reportReturnType] self._ensure_previously_set(dist, "readme") return None @@ -401,7 +406,7 @@ def __init__( self._project_cfg = project_cfg self._setuptools_cfg = setuptools_cfg - def __enter__(self): + def __enter__(self) -> "Self": """When entering the context, the values of ``packages``, ``py_modules`` and ``package_dir`` that are missing in ``dist`` are copied from ``setuptools_cfg``. """ diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py index a7f02714cb..20ce71308d 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -108,7 +108,7 @@ def _apply( filenames = [*other_files, filepath] try: - _Distribution.parse_config_files(dist, filenames=filenames) + _Distribution.parse_config_files(dist, filenames=filenames) # type: ignore[arg-type] # TODO: fix in disutils stubs handlers = parse_configuration( dist, dist.command_options, ignore_option_errors=ignore_option_errors ) @@ -475,7 +475,7 @@ def parse_section(self, section_options): # Keep silent for a new option may appear anytime. self[name] = value - def parse(self): + def parse(self) -> None: """Parses configuration file items from one or more related sections. diff --git a/setuptools/dist.py b/setuptools/dist.py index 0d35583dbc..2d0328719f 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -10,7 +10,7 @@ from contextlib import suppress from glob import iglob from pathlib import Path -from typing import List, Optional, Set +from typing import Dict, List, MutableMapping, Optional, Set, Tuple import distutils.cmd import distutils.command @@ -205,7 +205,7 @@ def check_packages(dist, attr, value): _Distribution = get_unpatched(distutils.core.Distribution) -class Distribution(_Distribution): +class Distribution(_Distribution): # type: ignore[valid-type, misc] # https://github.com/python/mypy/issues/14458 """Distribution with support for tests and package data This is an enhanced version of 'distutils.dist.Distribution' that @@ -257,6 +257,10 @@ class Distribution(_Distribution): the distribution. """ + packages: Optional[List] + py_modules: Optional[List] + ext_modules: Optional[List] + _DISTUTILS_UNSUPPORTED_METADATA = { 'long_description_content_type': lambda: None, 'project_urls': dict, @@ -283,24 +287,24 @@ def patch_missing_pkg_info(self, attrs): dist._version = _normalization.safe_version(str(attrs['version'])) self._patched_dist = dist - def __init__(self, attrs=None): + def __init__(self, attrs: Optional[MutableMapping] = None) -> None: have_package_data = hasattr(self, "package_data") if not have_package_data: - self.package_data = {} + self.package_data: Dict[str, List[str]] = {} attrs = attrs or {} - self.dist_files = [] + self.dist_files: List[Tuple[str, str, str]] = [] # Filter-out setuptools' specific options. self.src_root = attrs.pop("src_root", None) self.patch_missing_pkg_info(attrs) self.dependency_links = attrs.pop('dependency_links', []) self.setup_requires = attrs.pop('setup_requires', []) for ep in metadata.entry_points(group='distutils.setup_keywords'): - vars(self).setdefault(ep.name, None) + vars(self).setdefault(ep.name, None) # type: ignore[attr-defined] # https://github.com/python/mypy/issues/14458 metadata_only = set(self._DISTUTILS_UNSUPPORTED_METADATA) metadata_only -= {"install_requires", "extras_require"} dist_attrs = {k: v for k, v in attrs.items() if k not in metadata_only} - _Distribution.__init__(self, dist_attrs) + super().__init__(dist_attrs) # Private API (setuptools-use only, not restricted to Distribution) # Stores files that are referenced by the configuration and need to be in the @@ -381,7 +385,7 @@ def _normalize_requires(self): k: list(map(str, _reqs.parse(v or []))) for k, v in extras_require.items() } - def _finalize_license_files(self): + def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" license_files: Optional[List[str]] = self.metadata.license_files patterns: List[str] = license_files if license_files else [] @@ -394,7 +398,7 @@ def _finalize_license_files(self): # Default patterns match the ones wheel uses # See https://wheel.readthedocs.io/en/stable/user_guide.html # -> 'Including license files in the generated wheel file' - patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') + patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*'] self.metadata.license_files = list( unique_everseen(self._expand_patterns(patterns)) @@ -812,7 +816,7 @@ def _include_misc(self, name, value): ) else: new = [item for item in value if item not in old] - setattr(self, name, old + new) + setattr(self, name, list(old) + new) def exclude(self, **attrs): """Remove items from distribution that are named in keyword arguments diff --git a/setuptools/extension.py b/setuptools/extension.py index 58c023f6b4..9eea0d6930 100644 --- a/setuptools/extension.py +++ b/setuptools/extension.py @@ -27,7 +27,7 @@ def _have_cython(): _Extension = get_unpatched(distutils.core.Extension) -class Extension(_Extension): +class Extension(_Extension): # type: ignore[valid-type, misc] # https://github.com/python/mypy/issues/14458 """ Describes a single extension module. diff --git a/setuptools/monkey.py b/setuptools/monkey.py index da0993506c..715fb92dfc 100644 --- a/setuptools/monkey.py +++ b/setuptools/monkey.py @@ -8,18 +8,21 @@ import sys import types from importlib import import_module +from typing import List, Optional, TypeVar, Union, overload import distutils.filelist -__all__ = [] +_UnpatchedT = TypeVar("_UnpatchedT", type, types.FunctionType) + +__all__: List[str] = [] """ Everything is private. Contact the project team if you think you need this functionality. """ -def _get_mro(cls): +def _get_mro(cls: type): """ Returns the bases classes for cls sorted by the MRO. @@ -32,8 +35,11 @@ def _get_mro(cls): return (cls,) + cls.__bases__ return inspect.getmro(cls) - -def get_unpatched(item): +@overload +def get_unpatched(item: _UnpatchedT) -> _UnpatchedT:... +@overload +def get_unpatched(item: object) -> None:... +def get_unpatched(item: Union[type, types.FunctionType, object]) -> Optional[Union[type, types.FunctionType]]: lookup = ( get_unpatched_class if isinstance(item, type) @@ -114,7 +120,7 @@ def patch_func(replacement, target_mod, func_name): setattr(target_mod, func_name, replacement) -def get_unpatched_function(candidate): +def get_unpatched_function(candidate) -> types.FunctionType: return candidate.unpatched diff --git a/setuptools/msvc.py b/setuptools/msvc.py index aa69db5810..2bc9b08fa7 100644 --- a/setuptools/msvc.py +++ b/setuptools/msvc.py @@ -19,10 +19,13 @@ import platform import itertools import subprocess +from typing import TYPE_CHECKING import distutils.errors +from typing import Dict, TYPE_CHECKING from setuptools.extern.more_itertools import unique_everseen -if platform.system() == 'Windows': +# https://github.com/python/mypy/issues/8166 +if not TYPE_CHECKING and platform.system() == 'Windows': import winreg from os import environ else: @@ -34,7 +37,7 @@ class winreg: HKEY_LOCAL_MACHINE = None HKEY_CLASSES_ROOT = None - environ = dict() + environ: Dict[str, str] = dict() def _msvc14_find_vc2015(): diff --git a/setuptools/py311compat.py b/setuptools/py311compat.py index 9231cbb290..36a63da6e1 100644 --- a/setuptools/py311compat.py +++ b/setuptools/py311compat.py @@ -4,4 +4,5 @@ if sys.version_info >= (3, 11): import tomllib else: # pragma: no cover - from setuptools.extern import tomli as tomllib + from setuptools.extern import tomli as tomllib # type: ignore[attr-defined] +tomllib = tomllib diff --git a/setuptools/py312compat.py b/setuptools/py312compat.py index 28175b1f75..d7a3ef701e 100644 --- a/setuptools/py312compat.py +++ b/setuptools/py312compat.py @@ -1,8 +1,10 @@ import sys import shutil +def _do_nothing(*args: object) -> None: + pass -def shutil_rmtree(path, ignore_errors=False, onexc=None): +def shutil_rmtree(path, ignore_errors=False, onexc=_do_nothing): if sys.version_info >= (3, 12): return shutil.rmtree(path, ignore_errors, onexc=onexc) diff --git a/setuptools/sandbox.py b/setuptools/sandbox.py index 757074166a..39e49fdc9b 100644 --- a/setuptools/sandbox.py +++ b/setuptools/sandbox.py @@ -9,6 +9,7 @@ import pickle import textwrap import builtins +from typing import Union, List import pkg_resources from distutils.errors import DistutilsError @@ -19,7 +20,7 @@ else: _os = sys.modules[os.name] try: - _file = file + _file = file # type: ignore[name-defined] # Check for global variable except NameError: _file = None _open = open @@ -298,7 +299,7 @@ def run(self, func): with self: return func() - def _mk_dual_path_wrapper(name): + def _mk_dual_path_wrapper(name: str): # type: ignore[misc] # TODO: Extract or make static original = getattr(_os, name) def wrap(self, src, dst, *args, **kw): @@ -312,7 +313,7 @@ def wrap(self, src, dst, *args, **kw): if hasattr(_os, name): locals()[name] = _mk_dual_path_wrapper(name) - def _mk_single_path_wrapper(name, original=None): + def _mk_single_path_wrapper(name: str, original=None): # type: ignore[misc] # TODO: Extract or make static original = original or getattr(_os, name) def wrap(self, path, *args, **kw): @@ -349,7 +350,7 @@ def wrap(self, path, *args, **kw): if hasattr(_os, name): locals()[name] = _mk_single_path_wrapper(name) - def _mk_single_with_return(name): + def _mk_single_with_return(name: str): # type: ignore[misc] # TODO: Extract or make static original = getattr(_os, name) def wrap(self, path, *args, **kw): @@ -364,7 +365,7 @@ def wrap(self, path, *args, **kw): if hasattr(_os, name): locals()[name] = _mk_single_with_return(name) - def _mk_query(name): + def _mk_query(name: str): # type: ignore[misc] # TODO: Extract or make static original = getattr(_os, name) def wrap(self, *args, **kw): @@ -424,7 +425,7 @@ class DirectorySandbox(AbstractSandbox): "tempnam", ]) - _exception_patterns = [] + _exception_patterns: List[Union[str, re.Pattern]] = [] "exempt writing to paths that match the pattern" def __init__(self, sandbox, exceptions=_EXCEPTIONS): @@ -441,11 +442,10 @@ def _violation(self, operation, *args, **kw): raise SandboxViolation(operation, args, kw) if _file: - def _file(self, path, mode='r', *args, **kw): if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path): self._violation("file", path, mode, *args, **kw) - return _file(path, mode, *args, **kw) + return _file(path, mode, *args, **kw) # pyright: ignore[reportOptionalCall] # Unsafe, but we never re-assign _file def _open(self, path, mode='r', *args, **kw): if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path): diff --git a/setuptools/tests/_packaging_compat.py b/setuptools/tests/_packaging_compat.py index 5bdcc554d5..a34b261d72 100644 --- a/setuptools/tests/_packaging_compat.py +++ b/setuptools/tests/_packaging_compat.py @@ -1,6 +1,8 @@ +from typing import TYPE_CHECKING + from packaging import __version__ as packaging_version -if tuple(packaging_version.split(".")) >= ("23", "2"): +if TYPE_CHECKING or tuple(packaging_version.split(".")) >= ("23", "2"): from packaging.metadata import Metadata else: # Just pretend it exists while waiting for release... diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 6935523987..17f8c96ed0 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -9,6 +9,7 @@ import tarfile from inspect import cleandoc from pathlib import Path +from typing import Tuple from unittest.mock import Mock from zipfile import ZipFile @@ -451,7 +452,7 @@ def core_metadata(dist) -> str: # Make sure core metadata is valid Metadata.from_email(pkg_file_txt, validate=True) # can raise exceptions - skip_prefixes = () + skip_prefixes: Tuple[str, ...] = () skip_lines = set() # ---- DIFF NORMALISATION ---- # PEP 621 is very particular about author/maintainer metadata conversion, so skip diff --git a/setuptools/tests/integration/test_pip_install_sdist.py b/setuptools/tests/integration/test_pip_install_sdist.py index 3467a5ec07..f5db400609 100644 --- a/setuptools/tests/integration/test_pip_install_sdist.py +++ b/setuptools/tests/integration/test_pip_install_sdist.py @@ -1,3 +1,5 @@ +# https://github.com/python/mypy/issues/8009#issuecomment-558335186 +# mypy: disable-error-code="has-type" """Integration tests for setuptools that focus on building packages via pip. The idea behind these tests is not to exhaustively check all the possible @@ -25,10 +27,10 @@ from .helpers import Archive, run - pytestmark = pytest.mark.integration -(LATEST,) = Enum("v", "LATEST") + +(LATEST,) = Enum("v", "LATEST") # type: ignore[misc] # https://github.com/python/mypy/issues/8009#issuecomment-558335186 """Default version to be checked""" # There are positive and negative aspects of checking the latest version of the # packages. diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index 85cb09730c..d193c06416 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -381,6 +381,7 @@ def test_skip_discovery_with_setupcfg_metadata(self, tmp_path): assert dist.get_version() == "42" assert dist.py_modules is None assert dist.packages is None + assert dist.ext_modules is not None assert len(dist.ext_modules) == 1 assert dist.ext_modules[0].name == "proj" diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py index 987c2fd67c..ca0f007486 100644 --- a/setuptools/tests/test_editable_install.py +++ b/setuptools/tests/test_editable_install.py @@ -120,7 +120,7 @@ def editable_opts(request): @pytest.mark.parametrize( "files", [ - {**EXAMPLE, "setup.py": SETUP_SCRIPT_STUB}, # type: ignore + {**EXAMPLE, "setup.py": SETUP_SCRIPT_STUB}, EXAMPLE, # No setup.py script ], ) diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index af7d2f8295..ef8536bf4e 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -79,7 +79,8 @@ def run(): @staticmethod def _extract_mv_version(pkg_info_lines: List[str]) -> Tuple[int, int]: version_str = pkg_info_lines[0].split(' ')[1] - return tuple(map(int, version_str.split('.')[:2])) + major, minor, *_ = map(int, version_str.split('.')) + return major, minor def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env): """ diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py index fbd21b1976..16fa2c2460 100644 --- a/setuptools/tests/test_manifest.py +++ b/setuptools/tests/test_manifest.py @@ -10,6 +10,7 @@ import logging from distutils import log from distutils.errors import DistutilsTemplateError +from typing import List, Tuple from setuptools.command.egg_info import FileList, egg_info, translate_pattern from setuptools.dist import Distribution @@ -75,7 +76,7 @@ def touch(filename): ) -translate_specs = [ +translate_specs: List[Tuple[str, List[str], List[str]]] = [ ('foo', ['foo'], ['bar', 'foobar']), ('foo/bar', ['foo/bar'], ['foo/bar/baz', './foo/bar', 'foo']), # Glob matching diff --git a/setuptools/tests/test_packageindex.py b/setuptools/tests/test_packageindex.py index f27a5c63e9..c3024a05b5 100644 --- a/setuptools/tests/test_packageindex.py +++ b/setuptools/tests/test_packageindex.py @@ -1,4 +1,3 @@ -import sys import distutils.errors import urllib.request import urllib.error @@ -126,7 +125,7 @@ def test_local_index(self, tmpdir): f.write('
content
') url = 'file:' + urllib.request.pathname2url(str(tmpdir)) + '/' res = setuptools.package_index.local_open(url) - assert 'content' in res.read() + assert 'content' in res.read() # pyright: ignore[reportOperatorIssue] # TODO: This may be an upstream issue? To validate def test_egg_fragment(self): """ diff --git a/setuptools/tests/test_setuptools.py b/setuptools/tests/test_setuptools.py index 0dc4769b93..d318e4ea36 100644 --- a/setuptools/tests/test_setuptools.py +++ b/setuptools/tests/test_setuptools.py @@ -53,7 +53,7 @@ def testExtractConst(self): def f1(): global x, y, z x = "test" - y = z + y = z # pyright: ignore[reportUnboundVariable] # Testing unassigned variables fc = f1.__code__ diff --git a/setuptools/tests/test_wheel.py b/setuptools/tests/test_wheel.py index 03fb05d2f4..c54cb5b1b8 100644 --- a/setuptools/tests/test_wheel.py +++ b/setuptools/tests/test_wheel.py @@ -12,6 +12,7 @@ import shutil import subprocess import sys +from typing_extensions import Required, TypedDict import zipfile import pytest @@ -176,9 +177,17 @@ def __init__(self, id, **kwargs): def __repr__(self): return '%s(**%r)' % (self._id, self._fields) +class WheelInstallTest(TypedDict, total=False): + id: Required[str] + file_defs: dict + setup_kwargs: dict + install_tree: set + install_requires: str + extras_require: dict + requires_txt: str WHEEL_INSTALL_TESTS = ( - dict( + WheelInstallTest( id='basic', file_defs={'foo': {'__init__.py': ''}}, setup_kwargs=dict( @@ -191,13 +200,13 @@ def __repr__(self): } }), ), - dict( + WheelInstallTest( id='utf-8', setup_kwargs=dict( description='Description accentuée', ), ), - dict( + WheelInstallTest( id='data', file_defs={ 'data.txt': DALS( @@ -216,7 +225,7 @@ def __repr__(self): } }), ), - dict( + WheelInstallTest( id='extension', file_defs={ 'extension.c': DALS( @@ -284,7 +293,7 @@ def __repr__(self): ] }), ), - dict( + WheelInstallTest( id='header', file_defs={ 'header.h': DALS( @@ -309,7 +318,7 @@ def __repr__(self): ] }), ), - dict( + WheelInstallTest( id='script', file_defs={ 'script.py': DALS( @@ -340,7 +349,7 @@ def __repr__(self): } }), ), - dict( + WheelInstallTest( id='requires1', install_requires='foobar==2.0', install_tree=flatten_tree({ @@ -360,7 +369,7 @@ def __repr__(self): """ ), ), - dict( + WheelInstallTest( id='requires2', install_requires=""" bar @@ -374,14 +383,14 @@ def __repr__(self): """ ), ), - dict( + WheelInstallTest( id='requires3', install_requires=""" bar; %r != sys_platform """ % sys.platform, ), - dict( + WheelInstallTest( id='requires4', install_requires=""" foo @@ -398,7 +407,7 @@ def __repr__(self): """ ), ), - dict( + WheelInstallTest( id='requires5', extras_require={ 'extra': 'foobar; %r != sys_platform' % sys.platform, @@ -409,7 +418,7 @@ def __repr__(self): """ ), ), - dict( + WheelInstallTest( id='requires_ensure_order', install_requires=""" foo @@ -440,7 +449,7 @@ def __repr__(self): """ ), ), - dict( + WheelInstallTest( id='namespace_package', file_defs={ 'foo': { @@ -472,7 +481,7 @@ def __repr__(self): ] }), ), - dict( + WheelInstallTest( id='empty_namespace_package', file_defs={ 'foobar': { @@ -505,7 +514,7 @@ def __repr__(self): ] }), ), - dict( + WheelInstallTest( id='data_in_package', file_defs={ 'foo': { diff --git a/tools/finalize.py b/tools/finalize.py index f79f5b3b45..38a01bf8b2 100644 --- a/tools/finalize.py +++ b/tools/finalize.py @@ -24,7 +24,10 @@ def get_version(): cmd = bump_version_command + ['--dry-run', '--verbose'] out = subprocess.check_output(cmd, text=True) - return re.search('^new_version=(.*)', out, re.MULTILINE).group(1) + match = re.search('^new_version=(.*)', out, re.MULTILINE) + if not match: + return None + return match.group(1) def update_changelog():