Skip to content

Commit d457d0e

Browse files
committed
Type sequence checks in setuptools/dist.py
1 parent b828db4 commit d457d0e

File tree

4 files changed

+40
-18
lines changed

4 files changed

+40
-18
lines changed

newsfragments/4578.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a `TypeError` when a ``Distribution``'s old included attribute was a `tuple` -- by :user:`Avasam`

newsfragments/4578.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Made errors when parsing ``Distribution`` data more explicit about the expected type (``tuple[str, ...] | list[str]``) -- by :user:`Avasam`

setuptools/dist.py

+34-14
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import sys
99
from glob import iglob
1010
from pathlib import Path
11-
from typing import TYPE_CHECKING, MutableMapping
11+
from typing import TYPE_CHECKING, List, MutableMapping, NoReturn, Tuple, Union, overload
1212

1313
from more_itertools import partition, unique_everseen
1414
from packaging.markers import InvalidMarker, Marker
@@ -21,6 +21,7 @@
2121
command as _, # noqa: F401 # imported for side-effects
2222
)
2323
from ._importlib import metadata
24+
from ._reqs import _StrOrIter
2425
from .config import pyprojecttoml, setupcfg
2526
from .discovery import ConfigDiscovery
2627
from .monkey import get_unpatched
@@ -36,9 +37,22 @@
3637
from distutils.fancy_getopt import translate_longopt
3738
from distutils.util import strtobool
3839

40+
if TYPE_CHECKING:
41+
from typing_extensions import TypeAlias
42+
3943
__all__ = ['Distribution']
4044

4145
sequence = tuple, list
46+
"""
47+
Supported iterable types that are known to be:
48+
- ordered (which `set` isn't)
49+
- not match a str (which `Sequence[str]` does)
50+
- not imply a nested type (like `dict`)
51+
for use with `isinstance`.
52+
"""
53+
_Sequence: TypeAlias = Union[Tuple[str, ...], List[str]]
54+
# This is how stringifying _Sequence would look in Python 3.10
55+
_requence_type_repr = "tuple[str, ...] | list[str]"
4256

4357

4458
def check_importable(dist, attr, value):
@@ -51,7 +65,7 @@ def check_importable(dist, attr, value):
5165
) from e
5266

5367

54-
def assert_string_list(dist, attr, value):
68+
def assert_string_list(dist, attr: str, value: _Sequence) -> None:
5569
"""Verify that value is a string list"""
5670
try:
5771
# verify that value is a list or tuple to exclude unordered
@@ -61,7 +75,7 @@ def assert_string_list(dist, attr, value):
6175
assert ''.join(value) != value
6276
except (TypeError, ValueError, AttributeError, AssertionError) as e:
6377
raise DistutilsSetupError(
64-
"%r must be a list of strings (got %r)" % (attr, value)
78+
f"{attr!r} must be of type <{_requence_type_repr}> (got {value!r})"
6579
) from e
6680

6781

@@ -138,18 +152,22 @@ def invalid_unless_false(dist, attr, value):
138152
raise DistutilsSetupError(f"{attr} is invalid.")
139153

140154

141-
def check_requirements(dist, attr, value):
155+
@overload
156+
def check_requirements(dist, attr: str, value: set | dict) -> NoReturn: ...
157+
@overload
158+
def check_requirements(dist, attr: str, value: _StrOrIter) -> None: ...
159+
def check_requirements(dist, attr: str, value: _StrOrIter) -> None:
142160
"""Verify that install_requires is a valid requirements list"""
143161
try:
144162
list(_reqs.parse(value))
145163
if isinstance(value, (dict, set)):
146164
raise TypeError("Unordered types are not allowed")
147165
except (TypeError, ValueError) as error:
148166
tmpl = (
149-
"{attr!r} must be a string or list of strings "
150-
"containing valid project/version requirement specifiers; {error}"
167+
f"{attr!r} must be a string or iterable of strings "
168+
f"containing valid project/version requirement specifiers; {error}"
151169
)
152-
raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error
170+
raise DistutilsSetupError(tmpl) from error
153171

154172

155173
def check_specifier(dist, attr, value):
@@ -767,11 +785,11 @@ def has_contents_for(self, package):
767785

768786
return False
769787

770-
def _exclude_misc(self, name, value):
788+
def _exclude_misc(self, name: str, value: _Sequence) -> None:
771789
"""Handle 'exclude()' for list/tuple attrs without a special handler"""
772790
if not isinstance(value, sequence):
773791
raise DistutilsSetupError(
774-
"%s: setting must be a list or tuple (%r)" % (name, value)
792+
f"{name}: setting must be of type <{_requence_type_repr}> (got {value!r})"
775793
)
776794
try:
777795
old = getattr(self, name)
@@ -784,11 +802,13 @@ def _exclude_misc(self, name, value):
784802
elif old:
785803
setattr(self, name, [item for item in old if item not in value])
786804

787-
def _include_misc(self, name, value):
805+
def _include_misc(self, name: str, value: _Sequence) -> None:
788806
"""Handle 'include()' for list/tuple attrs without a special handler"""
789807

790808
if not isinstance(value, sequence):
791-
raise DistutilsSetupError("%s: setting must be a list (%r)" % (name, value))
809+
raise DistutilsSetupError(
810+
f"{name}: setting must be of type <{_requence_type_repr}> (got {value!r})"
811+
)
792812
try:
793813
old = getattr(self, name)
794814
except AttributeError as e:
@@ -801,7 +821,7 @@ def _include_misc(self, name, value):
801821
)
802822
else:
803823
new = [item for item in value if item not in old]
804-
setattr(self, name, old + new)
824+
setattr(self, name, list(old) + new)
805825

806826
def exclude(self, **attrs):
807827
"""Remove items from distribution that are named in keyword arguments
@@ -826,10 +846,10 @@ def exclude(self, **attrs):
826846
else:
827847
self._exclude_misc(k, v)
828848

829-
def _exclude_packages(self, packages):
849+
def _exclude_packages(self, packages: _Sequence) -> None:
830850
if not isinstance(packages, sequence):
831851
raise DistutilsSetupError(
832-
"packages: setting must be a list or tuple (%r)" % (packages,)
852+
f"packages: setting must be of type <{_requence_type_repr}> (got {packages!r})"
833853
)
834854
list(map(self.exclude_package, packages))
835855

setuptools/tests/test_dist.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ def test_provides_extras_deterministic_order():
118118
'hello': '*.msg',
119119
},
120120
(
121-
"\"values of 'package_data' dict\" "
122-
"must be a list of strings (got '*.msg')"
121+
"\"values of 'package_data' dict\" must be of type <tuple[str, ...] | list[str]>"
122+
" (got '*.msg')"
123123
),
124124
),
125125
# Invalid value type (generators are single use)
@@ -128,8 +128,8 @@ def test_provides_extras_deterministic_order():
128128
'hello': (x for x in "generator"),
129129
},
130130
(
131-
"\"values of 'package_data' dict\" must be a list of strings "
132-
"(got <generator object"
131+
"\"values of 'package_data' dict\" must be of type <tuple[str, ...] | list[str]>"
132+
" (got <generator object"
133133
),
134134
),
135135
)

0 commit comments

Comments
 (0)