From fe9555baaac0d818998a100abb0c98d4b0416d4f Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 21 Dec 2025 14:57:21 -0500 Subject: [PATCH 1/3] setuptools.setup Command type variance issue --- .../@tests/test_cases/check_setup.py | 23 ++++++++++ stubs/setuptools/setuptools/__init__.pyi | 45 +++++++++---------- 2 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 stubs/setuptools/@tests/test_cases/check_setup.py diff --git a/stubs/setuptools/@tests/test_cases/check_setup.py b/stubs/setuptools/@tests/test_cases/check_setup.py new file mode 100644 index 000000000000..b6bac7b01de5 --- /dev/null +++ b/stubs/setuptools/@tests/test_cases/check_setup.py @@ -0,0 +1,23 @@ +from typing_extensions import assert_type + +from setuptools import Command as setuptools_Command, Distribution as setuptools_Distribution, setup +from setuptools._distutils.cmd import Command as distutils_Command +from setuptools._distutils.dist import Distribution as distutils_Distribution + +# Ensure that any distutils-derived classes are usable w/o type variance issues +assert_type( + setup( + cmdclass=dict[str, type[distutils_Command]](), + command_obj=dict[str, distutils_Command](), + distclass=distutils_Distribution, + ), + distutils_Distribution, +) +assert_type( + setup( + cmdclass=dict[str, type[setuptools_Command]](), + command_obj=dict[str, setuptools_Command](), + distclass=setuptools_Distribution, + ), + setuptools_Distribution, +) diff --git a/stubs/setuptools/setuptools/__init__.pyi b/stubs/setuptools/setuptools/__init__.pyi index eaea1f558070..2c7273e543e9 100644 --- a/stubs/setuptools/setuptools/__init__.pyi +++ b/stubs/setuptools/setuptools/__init__.pyi @@ -1,5 +1,6 @@ from _typeshed import StrPath from abc import abstractmethod +from binascii import Incomplete from collections.abc import ItemsView, Iterable, Mapping, Sequence from typing import Any, Literal, Protocol, TypedDict, TypeVar, overload, type_check_only from typing_extensions import Never, NotRequired @@ -36,9 +37,7 @@ from .warnings import SetuptoolsDeprecationWarning as SetuptoolsDeprecationWarni _CommandT = TypeVar("_CommandT", bound=_Command) _DistributionT = TypeVar("_DistributionT", bound=_Distribution, default=Distribution) -_T = TypeVar("_T") _KT = TypeVar("_KT") -_VT = TypeVar("_VT") _VT_co = TypeVar("_VT_co", covariant=True) __all__ = [ @@ -54,22 +53,22 @@ __all__ = [ __version__: str +# We need any Command subclass to be valid +# Any: pyright would accept using covariance in __setitem__, but mypy won't let a dict be assignable to this protocol +# This is unsound, but it's a quirk of setuptools' internals @type_check_only -class _DictLike(Protocol[_KT, _VT_co]): # type: ignore[misc] # Covariant type as parameter - @overload - def get(self, key: _KT, /) -> _VT_co | None: ... - @overload - def get(self, key: _KT, default: _VT_co, /) -> _VT_co: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter - @overload - def get(self, key: _KT, default: _T, /) -> _VT_co | _T: ... +class _DictLike(Protocol[_KT, _VT_co]): + # See note about using _VT_co instead of Any + def get(self, key: _KT, default: Any | None = None, /) -> _VT_co | None: ... def items(self) -> ItemsView[_KT, _VT_co]: ... def keys(self) -> Iterable[_KT]: ... def __getitem__(self, key: _KT, /) -> _VT_co: ... - def __contains__(self, x: Any, /) -> bool: ... + def __contains__(self, x: object, /) -> bool: ... @type_check_only -class _MutableDictLike(_DictLike[_KT, _VT], Protocol): - def __setitem__(self, key: _KT, value: _VT, /) -> None: ... +class _MutableDictLike(_DictLike[_KT, _VT_co], Protocol): + # See note about using _VT_co instead of Any + def __setitem__(self, key: _KT, value: Any, /) -> None: ... @type_check_only class _BuildInfo(TypedDict): @@ -107,9 +106,9 @@ def setup( download_url: str | None = None, # Attributes from distutils.dist.Distribution.__init__ (except self.metadata) # These take priority over attributes from distutils.dist.Distribution.display_option_names - verbose=True, - dry_run=False, - help=False, + verbose: bool = True, + dry_run: bool = False, + help: bool = False, cmdclass: _MutableDictLike[str, type[_Command]] = {}, command_packages: str | list[str] | None = None, script_name: StrPath | None = ..., # default is actually set in distutils.core.setup @@ -123,7 +122,7 @@ def setup( ext_modules: Sequence[_Extension] | None = None, ext_package: str | None = None, include_dirs: list[str] | None = None, - extra_path=None, + extra_path: Never = ..., # Deprecated scripts: list[str] | None = None, data_files: list[tuple[str, Sequence[str]]] | None = None, password: str = "", @@ -149,13 +148,13 @@ def setup( setup_requires: list[str] = [], # From Distribution._DISTUTILS_UNSUPPORTED_METADATA set in Distribution._set_metadata_defaults long_description_content_type: str | None = None, - project_urls={}, - provides_extras={}, - license_expression=None, - license_file=None, - license_files=None, - install_requires=[], - extras_require={}, + project_urls: _DictLike[Incomplete, Incomplete] = {}, + provides_extras: _DictLike[Incomplete, Incomplete] = {}, + license_expression: str | None = None, + license_file: Never = ..., # Deprecated + license_files: Iterable[str] | None = None, + install_requires: str | Iterable[str] = [], + extras_require: _DictLike[Incomplete, Incomplete] = {}, # kwargs used directly in distutils.core.setup distclass: type[_DistributionT] = Distribution, # type: ignore[assignment] # noqa: Y011 # Custom Distributions could accept more params From cf2b87a847b74d3c382d2a3f35dafacfdbf84c1a Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 21 Dec 2025 17:02:28 -0500 Subject: [PATCH 2/3] Fix autocomplete typo --- stubs/setuptools/setuptools/__init__.pyi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stubs/setuptools/setuptools/__init__.pyi b/stubs/setuptools/setuptools/__init__.pyi index 2c7273e543e9..d5e9476d8964 100644 --- a/stubs/setuptools/setuptools/__init__.pyi +++ b/stubs/setuptools/setuptools/__init__.pyi @@ -1,6 +1,5 @@ -from _typeshed import StrPath +from _typeshed import Incomplete, StrPath from abc import abstractmethod -from binascii import Incomplete from collections.abc import ItemsView, Iterable, Mapping, Sequence from typing import Any, Literal, Protocol, TypedDict, TypeVar, overload, type_check_only from typing_extensions import Never, NotRequired From 5a7cb825b37607c851e3b7e7e744c9175b592555 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 21 Dec 2025 17:12:57 -0500 Subject: [PATCH 3/3] provides_extras actually uses setdefault --- stubs/setuptools/setuptools/__init__.pyi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stubs/setuptools/setuptools/__init__.pyi b/stubs/setuptools/setuptools/__init__.pyi index d5e9476d8964..45c1a79416b1 100644 --- a/stubs/setuptools/setuptools/__init__.pyi +++ b/stubs/setuptools/setuptools/__init__.pyi @@ -68,6 +68,7 @@ class _DictLike(Protocol[_KT, _VT_co]): class _MutableDictLike(_DictLike[_KT, _VT_co], Protocol): # See note about using _VT_co instead of Any def __setitem__(self, key: _KT, value: Any, /) -> None: ... + def setdefault(self, key: _KT, default: Any, /) -> _VT_co: ... @type_check_only class _BuildInfo(TypedDict): @@ -148,7 +149,7 @@ def setup( # From Distribution._DISTUTILS_UNSUPPORTED_METADATA set in Distribution._set_metadata_defaults long_description_content_type: str | None = None, project_urls: _DictLike[Incomplete, Incomplete] = {}, - provides_extras: _DictLike[Incomplete, Incomplete] = {}, + provides_extras: _MutableDictLike[Incomplete, Incomplete] = {}, license_expression: str | None = None, license_file: Never = ..., # Deprecated license_files: Iterable[str] | None = None,