diff --git a/pyproject.toml b/pyproject.toml index a7d1f3e99c..e11dd6c527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,9 @@ testing = [ # for tools/finalize.py 'jaraco.develop >= 7.21; python_version >= "3.9" and sys_platform != "cygwin"', "pytest-home >= 0.5", - "mypy==1.10.0", # pin mypy version so a new version doesn't suddenly cause the CI to fail + # pin type-checkers so a new version doesn't suddenly cause the CI to fail + "pyright == 1.1.364", + "mypy == 1.10.0", # No Python 3.11 dependencies require tomli, but needed for type-checking since we import it directly "tomli", # No Python 3.12 dependencies require importlib_metadata, but needed for type-checking since we import it directly diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000000..160c66458c --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/vscode-pyright/schemas/pyrightconfig.schema.json", + "exclude": [ + "build", + ".tox", + ".eggs", + "**/extern", // Vendored + "**/_vendor", // Vendored + "setuptools/_distutils", // Vendored + "**/tests", // Disabled as long as analyzeUnannotatedFunctions=false to reduce log spam + "**/_*", // Disabled as long as analyzeUnannotatedFunctions=false to reduce log spam + ], + // Our testing setup doesn't allow passing CLI arguments, so local devs have to set this manually. + // "pythonVersion": "3.8", + // For now we don't mind if mypy's `type: ignore` comments accidentally suppresses pyright issues + "enableTypeIgnoreComments": true, + "typeCheckingMode": "basic", + // Avoid raising issues when importing from "extern" modules, as those are added to path dynamically. + // https://github.com/pypa/setuptools/pull/3979#discussion_r1367968993 + "reportMissingImports": "none", + // Too many issues caused by vendoring and dynamic patching, still worth fixing when we can + "reportAttributeAccessIssue": "warning", + // FIXME: A handful of reportOperatorIssue spread throughout the codebase + "reportOperatorIssue": "warning", + // Deferred 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/setuptools/command/build.py b/setuptools/command/build.py index bc765a17ae..5bca4dd667 100644 --- a/setuptools/command/build.py +++ b/setuptools/command/build.py @@ -82,12 +82,15 @@ 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)""" + ... def get_source_files(self) -> list[str]: """ @@ -99,6 +102,7 @@ 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. """ + ... def get_outputs(self) -> list[str]: """ @@ -112,6 +116,7 @@ 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. """ + ... def get_output_mapping(self) -> dict[str, str]: """ @@ -122,3 +127,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 508704f3c0..2b5dceff99 100644 --- a/setuptools/command/build_ext.py +++ b/setuptools/command/build_ext.py @@ -153,20 +153,18 @@ def _get_output_mapping(self) -> Iterator[tuple[str, str]]: yield (output_cache, inplace_cache) def get_ext_filename(self, fullname): - so_ext = os.getenv('SETUPTOOLS_EXT_SUFFIX') + so_ext = os.getenv('SETUPTOOLS_EXT_SUFFIX', '') if so_ext: filename = os.path.join(*fullname.split('.')) + so_ext else: filename = _build_ext.get_ext_filename(self, fullname) - so_ext = get_config_var('EXT_SUFFIX') + so_ext = str(get_config_var('EXT_SUFFIX')) if fullname in self.ext_map: ext = self.ext_map[fullname] - use_abi3 = ext.py_limited_api and get_abi3_suffix() - if use_abi3: - filename = filename[: -len(so_ext)] - so_ext = get_abi3_suffix() - filename = filename + so_ext + abi3_suffix = get_abi3_suffix() + if ext.py_limited_api and abi3_suffix: # Use abi3 + filename = filename[: -len(so_ext)] + abi3_suffix if isinstance(ext, Library): fn, ext = os.path.splitext(filename) return self.shlib_compiler.library_filename(fn, libtype) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index eb6ba1025f..180f34fbea 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -1792,7 +1792,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[reportOptionalSubscript, reportIndexIssue] def update_dist_caches(dist_path, fix_zipimporter_caches): @@ -2018,7 +2018,7 @@ def is_python_script(script_text, filename): try: - from os import chmod as _chmod + from os import chmod as _chmod # pyright: ignore[reportAssignmentType] # Loosing type-safety w/ pyright, but that's ok except ImportError: # Jython compatibility def _chmod(*args: object, **kwargs: object) -> None: # type: ignore[misc] # Mypy re-uses the imported definition anyway diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index 55d477eebf..5392f2d516 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -57,6 +57,7 @@ from .install_scripts import install_scripts as install_scripts_cls if TYPE_CHECKING: + from typing_extensions import Self from .._vendor.wheel.wheelfile import WheelFile _P = TypeVar("_P", bound=StrPath) @@ -396,7 +397,7 @@ def _select_strategy( class EditableStrategy(Protocol): def __call__(self, wheel: WheelFile, files: list[str], mapping: dict[str, str]): ... - def __enter__(self): ... + def __enter__(self) -> Self: ... def __exit__(self, _exc_type, _exc_value, _traceback): ... diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index c8dae5f751..dd98d134d7 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -303,7 +303,10 @@ def _obtain(self, dist: Distribution, field: str, package_dir: Mapping[str, str] 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) -> dict[str, str] | None: @@ -313,9 +316,10 @@ def _obtain_readme(self, dist: Distribution) -> dict[str, str] | 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 diff --git a/setuptools/monkey.py b/setuptools/monkey.py index e513f95245..6a78cd52db 100644 --- a/setuptools/monkey.py +++ b/setuptools/monkey.py @@ -10,12 +10,14 @@ import sys import types from importlib import import_module -from typing import TypeVar +from typing import Type, TypeVar, cast, overload import distutils.filelist _T = TypeVar("_T") +_UnpatchT = TypeVar("_UnpatchT", type, types.FunctionType) + __all__: list[str] = [] """ @@ -38,25 +40,30 @@ def _get_mro(cls): return inspect.getmro(cls) -def get_unpatched(item: _T) -> _T: - lookup = ( - get_unpatched_class - if isinstance(item, type) - else get_unpatched_function - if isinstance(item, types.FunctionType) - else lambda item: None - ) - return lookup(item) +@overload +def get_unpatched(item: _UnpatchT) -> _UnpatchT: ... # type: ignore[overload-overlap] +@overload +def get_unpatched(item: object) -> None: ... +def get_unpatched( + item: type | types.FunctionType | object, +) -> type | types.FunctionType | None: + if isinstance(item, type): + return get_unpatched_class(item) + if isinstance(item, types.FunctionType): + return get_unpatched_function(item) + return None -def get_unpatched_class(cls): +def get_unpatched_class(cls: type[_T]) -> type[_T]: """Protect against re-patching the distutils if reloaded Also ensures that no other distutils extension monkeypatched the distutils first. """ external_bases = ( - cls for cls in _get_mro(cls) if not cls.__module__.startswith('setuptools') + cast(Type[_T], cls) + for cls in _get_mro(cls) + if not cls.__module__.startswith('setuptools') ) base = next(external_bases) if not base.__module__.startswith('distutils'): diff --git a/setuptools/package_index.py b/setuptools/package_index.py index c8789e279f..9c7add6824 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -1071,7 +1071,7 @@ def open_with_auth(url, opener=urllib.request.urlopen): if scheme in ('http', 'https'): auth, address = _splituser(netloc) else: - auth = None + auth, address = (None, None) if not auth: cred = PyPIConfig().find_credential(url) diff --git a/setuptools/sandbox.py b/setuptools/sandbox.py index 147b26749e..ffa82d777c 100644 --- a/setuptools/sandbox.py +++ b/setuptools/sandbox.py @@ -447,7 +447,8 @@ def _violation(self, operation, *args, **kw): 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) + # Self-referential, can't be None + return _file(path, mode, *args, **kw) # pyright: ignore[reportOptionalCall] 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/tox.ini b/tox.ini index ecfe01cd18..910296ce2b 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ setenv = PYTHONWARNDEFAULTENCODING = 1 SETUPTOOLS_ENFORCE_DEPRECATION = {env:SETUPTOOLS_ENFORCE_DEPRECATION:1} commands = + pyright . pytest {posargs} usedevelop = True extras =