Skip to content

Commit 9af0877

Browse files
authored
Type sequence checks in setuptools/dist.py (#4578)
2 parents b828db4 + 000a413 commit 9af0877

File tree

4 files changed

+73
-30
lines changed

4 files changed

+73
-30
lines changed

Diff for: 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`

Diff for: 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`

Diff for: setuptools/dist.py

+67-26
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@
88
import sys
99
from glob import iglob
1010
from pathlib import Path
11-
from typing import TYPE_CHECKING, MutableMapping
11+
from typing import (
12+
TYPE_CHECKING,
13+
Any,
14+
List,
15+
MutableMapping,
16+
NoReturn,
17+
Tuple,
18+
Union,
19+
overload,
20+
)
1221

1322
from more_itertools import partition, unique_everseen
1423
from packaging.markers import InvalidMarker, Marker
@@ -21,6 +30,7 @@
2130
command as _, # noqa: F401 # imported for side-effects
2231
)
2332
from ._importlib import metadata
33+
from ._reqs import _StrOrIter
2434
from .config import pyprojecttoml, setupcfg
2535
from .discovery import ConfigDiscovery
2636
from .monkey import get_unpatched
@@ -36,9 +46,35 @@
3646
from distutils.fancy_getopt import translate_longopt
3747
from distutils.util import strtobool
3848

49+
if TYPE_CHECKING:
50+
from typing_extensions import TypeAlias
51+
3952
__all__ = ['Distribution']
4053

41-
sequence = tuple, list
54+
_sequence = tuple, list
55+
"""
56+
:meta private:
57+
58+
Supported iterable types that are known to be:
59+
- ordered (which `set` isn't)
60+
- not match a str (which `Sequence[str]` does)
61+
- not imply a nested type (like `dict`)
62+
for use with `isinstance`.
63+
"""
64+
_Sequence: TypeAlias = Union[Tuple[str, ...], List[str]]
65+
# This is how stringifying _Sequence would look in Python 3.10
66+
_requence_type_repr = "tuple[str, ...] | list[str]"
67+
68+
69+
def __getattr__(name: str) -> Any: # pragma: no cover
70+
if name == "sequence":
71+
SetuptoolsDeprecationWarning.emit(
72+
"`setuptools.dist.sequence` is an internal implementation detail.",
73+
"Please define your own `sequence = tuple, list` instead.",
74+
due_date=(2025, 8, 28), # Originally added on 2024-08-27
75+
)
76+
return _sequence
77+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
4278

4379

4480
def check_importable(dist, attr, value):
@@ -51,17 +87,17 @@ def check_importable(dist, attr, value):
5187
) from e
5288

5389

54-
def assert_string_list(dist, attr, value):
90+
def assert_string_list(dist, attr: str, value: _Sequence) -> None:
5591
"""Verify that value is a string list"""
5692
try:
5793
# verify that value is a list or tuple to exclude unordered
5894
# or single-use iterables
59-
assert isinstance(value, sequence)
95+
assert isinstance(value, _sequence)
6096
# verify that elements of value are strings
6197
assert ''.join(value) != value
6298
except (TypeError, ValueError, AttributeError, AssertionError) as e:
6399
raise DistutilsSetupError(
64-
"%r must be a list of strings (got %r)" % (attr, value)
100+
f"{attr!r} must be of type <{_requence_type_repr}> (got {value!r})"
65101
) from e
66102

67103

@@ -126,8 +162,7 @@ def _check_marker(marker):
126162
def assert_bool(dist, attr, value):
127163
"""Verify that value is True, False, 0, or 1"""
128164
if bool(value) != value:
129-
tmpl = "{attr!r} must be a boolean value (got {value!r})"
130-
raise DistutilsSetupError(tmpl.format(attr=attr, value=value))
165+
raise DistutilsSetupError(f"{attr!r} must be a boolean value (got {value!r})")
131166

132167

133168
def invalid_unless_false(dist, attr, value):
@@ -138,27 +173,31 @@ def invalid_unless_false(dist, attr, value):
138173
raise DistutilsSetupError(f"{attr} is invalid.")
139174

140175

141-
def check_requirements(dist, attr, value):
176+
@overload
177+
def check_requirements(dist, attr: str, value: set | dict) -> NoReturn: ...
178+
@overload
179+
def check_requirements(dist, attr: str, value: _StrOrIter) -> None: ...
180+
def check_requirements(dist, attr: str, value: _StrOrIter) -> None:
142181
"""Verify that install_requires is a valid requirements list"""
143182
try:
144183
list(_reqs.parse(value))
145184
if isinstance(value, (dict, set)):
146185
raise TypeError("Unordered types are not allowed")
147186
except (TypeError, ValueError) as error:
148-
tmpl = (
149-
"{attr!r} must be a string or list of strings "
150-
"containing valid project/version requirement specifiers; {error}"
187+
msg = (
188+
f"{attr!r} must be a string or iterable of strings "
189+
f"containing valid project/version requirement specifiers; {error}"
151190
)
152-
raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error
191+
raise DistutilsSetupError(msg) from error
153192

154193

155194
def check_specifier(dist, attr, value):
156195
"""Verify that value is a valid version specifier"""
157196
try:
158197
SpecifierSet(value)
159198
except (InvalidSpecifier, AttributeError) as error:
160-
tmpl = "{attr!r} must be a string containing valid version specifiers; {error}"
161-
raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error
199+
msg = f"{attr!r} must be a string containing valid version specifiers; {error}"
200+
raise DistutilsSetupError(msg) from error
162201

163202

164203
def check_entry_points(dist, attr, value):
@@ -767,41 +806,43 @@ def has_contents_for(self, package):
767806

768807
return False
769808

770-
def _exclude_misc(self, name, value):
809+
def _exclude_misc(self, name: str, value: _Sequence) -> None:
771810
"""Handle 'exclude()' for list/tuple attrs without a special handler"""
772-
if not isinstance(value, sequence):
811+
if not isinstance(value, _sequence):
773812
raise DistutilsSetupError(
774-
"%s: setting must be a list or tuple (%r)" % (name, value)
813+
f"{name}: setting must be of type <{_requence_type_repr}> (got {value!r})"
775814
)
776815
try:
777816
old = getattr(self, name)
778817
except AttributeError as e:
779818
raise DistutilsSetupError("%s: No such distribution setting" % name) from e
780-
if old is not None and not isinstance(old, sequence):
819+
if old is not None and not isinstance(old, _sequence):
781820
raise DistutilsSetupError(
782821
name + ": this setting cannot be changed via include/exclude"
783822
)
784823
elif old:
785824
setattr(self, name, [item for item in old if item not in value])
786825

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

790-
if not isinstance(value, sequence):
791-
raise DistutilsSetupError("%s: setting must be a list (%r)" % (name, value))
829+
if not isinstance(value, _sequence):
830+
raise DistutilsSetupError(
831+
f"{name}: setting must be of type <{_requence_type_repr}> (got {value!r})"
832+
)
792833
try:
793834
old = getattr(self, name)
794835
except AttributeError as e:
795836
raise DistutilsSetupError("%s: No such distribution setting" % name) from e
796837
if old is None:
797838
setattr(self, name, value)
798-
elif not isinstance(old, sequence):
839+
elif not isinstance(old, _sequence):
799840
raise DistutilsSetupError(
800841
name + ": this setting cannot be changed via include/exclude"
801842
)
802843
else:
803844
new = [item for item in value if item not in old]
804-
setattr(self, name, old + new)
845+
setattr(self, name, list(old) + new)
805846

806847
def exclude(self, **attrs):
807848
"""Remove items from distribution that are named in keyword arguments
@@ -826,10 +867,10 @@ def exclude(self, **attrs):
826867
else:
827868
self._exclude_misc(k, v)
828869

829-
def _exclude_packages(self, packages):
830-
if not isinstance(packages, sequence):
870+
def _exclude_packages(self, packages: _Sequence) -> None:
871+
if not isinstance(packages, _sequence):
831872
raise DistutilsSetupError(
832-
"packages: setting must be a list or tuple (%r)" % (packages,)
873+
f"packages: setting must be of type <{_requence_type_repr}> (got {packages!r})"
833874
)
834875
list(map(self.exclude_package, packages))
835876

Diff for: 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)