From 6519c8d1f60462287f8703ff0ad0d3676d25e45d Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 6 Mar 2024 15:56:03 -0500 Subject: [PATCH] Basic pyright configs --- mypy.ini | 8 ++++--- pyrightconfig.json | 33 ++++++++++++++++++++++++++++ setup.cfg | 1 + setuptools/command/build.py | 6 +++++ setuptools/command/easy_install.py | 2 +- setuptools/command/editable_wheel.py | 2 +- setuptools/config/pyprojecttoml.py | 8 +++++-- setuptools/monkey.py | 31 ++++++++++++++++---------- tox.ini | 1 + 9 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 pyrightconfig.json diff --git a/mypy.ini b/mypy.ini index 42ade6537ee..772141b7c88 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,6 @@ [mypy] -# CI should test for all versions, local development gets hints for oldest supported -python_version = 3.8 +# Our testing setup doesn't allow passing CLI arguments, so local devs have to set this manually. +# python_version = 3.8 strict = False warn_unused_ignores = True # required to support namespace packages: https://github.com/python/mypy/issues/14057 @@ -8,6 +8,7 @@ explicit_package_bases = True exclude = (?x)( ^build/ | ^.tox/ + | ^.eggs/ | ^pkg_resources/tests/data/my-test-package-source/setup.py$ # Duplicate module name | ^.+?/(_vendor|extern)/ # Vendored | ^setuptools/_distutils/ # Vendored @@ -21,9 +22,10 @@ disable_error_code = attr-defined # - Avoid raising issues when importing from "extern" modules, as those are added to path dynamically. # https://github.com/pypa/setuptools/pull/3979#discussion_r1367968993 +# - distutils doesn't exists on Python 3.12 (stdlib) and mypy won't see the distutils hack. # - distutils._modified has different errors on Python 3.8 [import-untyped], on Python 3.9+ [import-not-found] # - All jaraco modules are still untyped -[mypy-pkg_resources.extern.*,setuptools.extern.*,distutils._modified,jaraco.*] +[mypy-pkg_resources.extern.*,setuptools.extern.*,distutils.*,distutils._modified,jaraco.*] ignore_missing_imports = True # - pkg_resources tests create modules that won't exists statically before the test is run. diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 00000000000..210173c25ca --- /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", + // For now, align with mypy's default of skipping unannotated functions, only care about public API which should be annotated + "analyzeUnannotatedFunctions": false, + // 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", + // 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/setup.cfg b/setup.cfg index 4d1155e884d..9211a506420 100644 --- a/setup.cfg +++ b/setup.cfg @@ -73,6 +73,7 @@ testing = # for tools/finalize.py jaraco.develop >= 7.21; python_version >= "3.9" and sys_platform != "cygwin" pytest-home >= 0.5 + pyright # 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/setuptools/command/build.py b/setuptools/command/build.py index afda7e3be92..2f9f568e542 100644 --- a/setuptools/command/build.py +++ b/setuptools/command/build.py @@ -98,12 +98,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]: """ @@ -115,6 +118,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]: """ @@ -128,6 +132,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]: """ @@ -138,3 +143,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/easy_install.py b/setuptools/command/easy_install.py index 402355bd816..a1e3d998f8c 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -2012,7 +2012,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 9d319398c91..3f74a8c71d4 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -59,7 +59,7 @@ from .install_scripts import install_scripts as install_scripts_cls if TYPE_CHECKING: - from wheel.wheelfile import WheelFile # type:ignore[import-untyped] # noqa + from wheel.wheelfile import WheelFile # type: ignore[import-untyped] # noqa _P = TypeVar("_P", bound=StrPath) _logger = logging.getLogger(__name__) diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index ff97679895f..8440d0d783e 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -297,7 +297,10 @@ 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]]: @@ -307,9 +310,10 @@ def _obtain_readme(self, dist: "Distribution") -> Optional[Dict[str, str]]: 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 fd07d91dece..0ed970a71d1 100644 --- a/setuptools/monkey.py +++ b/setuptools/monkey.py @@ -8,12 +8,14 @@ import sys import types from importlib import import_module -from typing import List, TypeVar +from typing import List, Optional, Type, TypeVar, Union, cast, overload import distutils.filelist _T = TypeVar("_T") +_UnpatchT = TypeVar("_UnpatchT", type, types.FunctionType) + __all__: List[str] = [] """ @@ -36,25 +38,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: Union[type, types.FunctionType, object], +) -> Optional[Union[type, types.FunctionType]]: + 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/tox.ini b/tox.ini index 2cbff924903..31eb10dabc2 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ setenv = SETUPTOOLS_ENFORCE_DEPRECATION = {env:SETUPTOOLS_ENFORCE_DEPRECATION:0} # ^-- Temporarily disable enforcement so CI don't fail on due dates commands = + pyright . pytest {posargs} usedevelop = True extras =