jpayne@69
|
1 from __future__ import annotations
|
jpayne@69
|
2
|
jpayne@69
|
3 import io
|
jpayne@69
|
4 import itertools
|
jpayne@69
|
5 import numbers
|
jpayne@69
|
6 import os
|
jpayne@69
|
7 import re
|
jpayne@69
|
8 import sys
|
jpayne@69
|
9 from collections.abc import Iterable
|
jpayne@69
|
10 from glob import iglob
|
jpayne@69
|
11 from pathlib import Path
|
jpayne@69
|
12 from typing import (
|
jpayne@69
|
13 TYPE_CHECKING,
|
jpayne@69
|
14 Any,
|
jpayne@69
|
15 Dict,
|
jpayne@69
|
16 List,
|
jpayne@69
|
17 MutableMapping,
|
jpayne@69
|
18 Sequence,
|
jpayne@69
|
19 Tuple,
|
jpayne@69
|
20 Union,
|
jpayne@69
|
21 )
|
jpayne@69
|
22
|
jpayne@69
|
23 from more_itertools import partition, unique_everseen
|
jpayne@69
|
24 from packaging.markers import InvalidMarker, Marker
|
jpayne@69
|
25 from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
jpayne@69
|
26 from packaging.version import Version
|
jpayne@69
|
27
|
jpayne@69
|
28 from setuptools._path import StrPath
|
jpayne@69
|
29
|
jpayne@69
|
30 from . import (
|
jpayne@69
|
31 _entry_points,
|
jpayne@69
|
32 _reqs,
|
jpayne@69
|
33 command as _, # noqa: F401 # imported for side-effects
|
jpayne@69
|
34 )
|
jpayne@69
|
35 from ._importlib import metadata
|
jpayne@69
|
36 from ._reqs import _StrOrIter
|
jpayne@69
|
37 from .config import pyprojecttoml, setupcfg
|
jpayne@69
|
38 from .discovery import ConfigDiscovery
|
jpayne@69
|
39 from .monkey import get_unpatched
|
jpayne@69
|
40 from .warnings import InformationOnly, SetuptoolsDeprecationWarning
|
jpayne@69
|
41
|
jpayne@69
|
42 import distutils.cmd
|
jpayne@69
|
43 import distutils.command
|
jpayne@69
|
44 import distutils.core
|
jpayne@69
|
45 import distutils.dist
|
jpayne@69
|
46 import distutils.log
|
jpayne@69
|
47 from distutils.debug import DEBUG
|
jpayne@69
|
48 from distutils.errors import DistutilsOptionError, DistutilsSetupError
|
jpayne@69
|
49 from distutils.fancy_getopt import translate_longopt
|
jpayne@69
|
50 from distutils.util import strtobool
|
jpayne@69
|
51
|
jpayne@69
|
52 if TYPE_CHECKING:
|
jpayne@69
|
53 from typing_extensions import TypeAlias
|
jpayne@69
|
54
|
jpayne@69
|
55 __all__ = ['Distribution']
|
jpayne@69
|
56
|
jpayne@69
|
57 _sequence = tuple, list
|
jpayne@69
|
58 """
|
jpayne@69
|
59 :meta private:
|
jpayne@69
|
60
|
jpayne@69
|
61 Supported iterable types that are known to be:
|
jpayne@69
|
62 - ordered (which `set` isn't)
|
jpayne@69
|
63 - not match a str (which `Sequence[str]` does)
|
jpayne@69
|
64 - not imply a nested type (like `dict`)
|
jpayne@69
|
65 for use with `isinstance`.
|
jpayne@69
|
66 """
|
jpayne@69
|
67 _Sequence: TypeAlias = Union[Tuple[str, ...], List[str]]
|
jpayne@69
|
68 # This is how stringifying _Sequence would look in Python 3.10
|
jpayne@69
|
69 _sequence_type_repr = "tuple[str, ...] | list[str]"
|
jpayne@69
|
70 _OrderedStrSequence: TypeAlias = Union[str, Dict[str, Any], Sequence[str]]
|
jpayne@69
|
71 """
|
jpayne@69
|
72 :meta private:
|
jpayne@69
|
73 Avoid single-use iterable. Disallow sets.
|
jpayne@69
|
74 A poor approximation of an OrderedSequence (dict doesn't match a Sequence).
|
jpayne@69
|
75 """
|
jpayne@69
|
76
|
jpayne@69
|
77
|
jpayne@69
|
78 def __getattr__(name: str) -> Any: # pragma: no cover
|
jpayne@69
|
79 if name == "sequence":
|
jpayne@69
|
80 SetuptoolsDeprecationWarning.emit(
|
jpayne@69
|
81 "`setuptools.dist.sequence` is an internal implementation detail.",
|
jpayne@69
|
82 "Please define your own `sequence = tuple, list` instead.",
|
jpayne@69
|
83 due_date=(2025, 8, 28), # Originally added on 2024-08-27
|
jpayne@69
|
84 )
|
jpayne@69
|
85 return _sequence
|
jpayne@69
|
86 raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
jpayne@69
|
87
|
jpayne@69
|
88
|
jpayne@69
|
89 def check_importable(dist, attr, value):
|
jpayne@69
|
90 try:
|
jpayne@69
|
91 ep = metadata.EntryPoint(value=value, name=None, group=None)
|
jpayne@69
|
92 assert not ep.extras
|
jpayne@69
|
93 except (TypeError, ValueError, AttributeError, AssertionError) as e:
|
jpayne@69
|
94 raise DistutilsSetupError(
|
jpayne@69
|
95 "%r must be importable 'module:attrs' string (got %r)" % (attr, value)
|
jpayne@69
|
96 ) from e
|
jpayne@69
|
97
|
jpayne@69
|
98
|
jpayne@69
|
99 def assert_string_list(dist, attr: str, value: _Sequence) -> None:
|
jpayne@69
|
100 """Verify that value is a string list"""
|
jpayne@69
|
101 try:
|
jpayne@69
|
102 # verify that value is a list or tuple to exclude unordered
|
jpayne@69
|
103 # or single-use iterables
|
jpayne@69
|
104 assert isinstance(value, _sequence)
|
jpayne@69
|
105 # verify that elements of value are strings
|
jpayne@69
|
106 assert ''.join(value) != value
|
jpayne@69
|
107 except (TypeError, ValueError, AttributeError, AssertionError) as e:
|
jpayne@69
|
108 raise DistutilsSetupError(
|
jpayne@69
|
109 f"{attr!r} must be of type <{_sequence_type_repr}> (got {value!r})"
|
jpayne@69
|
110 ) from e
|
jpayne@69
|
111
|
jpayne@69
|
112
|
jpayne@69
|
113 def check_nsp(dist, attr, value):
|
jpayne@69
|
114 """Verify that namespace packages are valid"""
|
jpayne@69
|
115 ns_packages = value
|
jpayne@69
|
116 assert_string_list(dist, attr, ns_packages)
|
jpayne@69
|
117 for nsp in ns_packages:
|
jpayne@69
|
118 if not dist.has_contents_for(nsp):
|
jpayne@69
|
119 raise DistutilsSetupError(
|
jpayne@69
|
120 "Distribution contains no modules or packages for "
|
jpayne@69
|
121 + "namespace package %r" % nsp
|
jpayne@69
|
122 )
|
jpayne@69
|
123 parent, sep, child = nsp.rpartition('.')
|
jpayne@69
|
124 if parent and parent not in ns_packages:
|
jpayne@69
|
125 distutils.log.warn(
|
jpayne@69
|
126 "WARNING: %r is declared as a package namespace, but %r"
|
jpayne@69
|
127 " is not: please correct this in setup.py",
|
jpayne@69
|
128 nsp,
|
jpayne@69
|
129 parent,
|
jpayne@69
|
130 )
|
jpayne@69
|
131 SetuptoolsDeprecationWarning.emit(
|
jpayne@69
|
132 "The namespace_packages parameter is deprecated.",
|
jpayne@69
|
133 "Please replace its usage with implicit namespaces (PEP 420).",
|
jpayne@69
|
134 see_docs="references/keywords.html#keyword-namespace-packages",
|
jpayne@69
|
135 # TODO: define due_date, it may break old packages that are no longer
|
jpayne@69
|
136 # maintained (e.g. sphinxcontrib extensions) when installed from source.
|
jpayne@69
|
137 # Warning officially introduced in May 2022, however the deprecation
|
jpayne@69
|
138 # was mentioned much earlier in the docs (May 2020, see #2149).
|
jpayne@69
|
139 )
|
jpayne@69
|
140
|
jpayne@69
|
141
|
jpayne@69
|
142 def check_extras(dist, attr, value):
|
jpayne@69
|
143 """Verify that extras_require mapping is valid"""
|
jpayne@69
|
144 try:
|
jpayne@69
|
145 list(itertools.starmap(_check_extra, value.items()))
|
jpayne@69
|
146 except (TypeError, ValueError, AttributeError) as e:
|
jpayne@69
|
147 raise DistutilsSetupError(
|
jpayne@69
|
148 "'extras_require' must be a dictionary whose values are "
|
jpayne@69
|
149 "strings or lists of strings containing valid project/version "
|
jpayne@69
|
150 "requirement specifiers."
|
jpayne@69
|
151 ) from e
|
jpayne@69
|
152
|
jpayne@69
|
153
|
jpayne@69
|
154 def _check_extra(extra, reqs):
|
jpayne@69
|
155 name, sep, marker = extra.partition(':')
|
jpayne@69
|
156 try:
|
jpayne@69
|
157 _check_marker(marker)
|
jpayne@69
|
158 except InvalidMarker:
|
jpayne@69
|
159 msg = f"Invalid environment marker: {marker} ({extra!r})"
|
jpayne@69
|
160 raise DistutilsSetupError(msg) from None
|
jpayne@69
|
161 list(_reqs.parse(reqs))
|
jpayne@69
|
162
|
jpayne@69
|
163
|
jpayne@69
|
164 def _check_marker(marker):
|
jpayne@69
|
165 if not marker:
|
jpayne@69
|
166 return
|
jpayne@69
|
167 m = Marker(marker)
|
jpayne@69
|
168 m.evaluate()
|
jpayne@69
|
169
|
jpayne@69
|
170
|
jpayne@69
|
171 def assert_bool(dist, attr, value):
|
jpayne@69
|
172 """Verify that value is True, False, 0, or 1"""
|
jpayne@69
|
173 if bool(value) != value:
|
jpayne@69
|
174 raise DistutilsSetupError(f"{attr!r} must be a boolean value (got {value!r})")
|
jpayne@69
|
175
|
jpayne@69
|
176
|
jpayne@69
|
177 def invalid_unless_false(dist, attr, value):
|
jpayne@69
|
178 if not value:
|
jpayne@69
|
179 DistDeprecationWarning.emit(f"{attr} is ignored.")
|
jpayne@69
|
180 # TODO: should there be a `due_date` here?
|
jpayne@69
|
181 return
|
jpayne@69
|
182 raise DistutilsSetupError(f"{attr} is invalid.")
|
jpayne@69
|
183
|
jpayne@69
|
184
|
jpayne@69
|
185 def check_requirements(dist, attr: str, value: _OrderedStrSequence) -> None:
|
jpayne@69
|
186 """Verify that install_requires is a valid requirements list"""
|
jpayne@69
|
187 try:
|
jpayne@69
|
188 list(_reqs.parse(value))
|
jpayne@69
|
189 if isinstance(value, set):
|
jpayne@69
|
190 raise TypeError("Unordered types are not allowed")
|
jpayne@69
|
191 except (TypeError, ValueError) as error:
|
jpayne@69
|
192 msg = (
|
jpayne@69
|
193 f"{attr!r} must be a string or iterable of strings "
|
jpayne@69
|
194 f"containing valid project/version requirement specifiers; {error}"
|
jpayne@69
|
195 )
|
jpayne@69
|
196 raise DistutilsSetupError(msg) from error
|
jpayne@69
|
197
|
jpayne@69
|
198
|
jpayne@69
|
199 def check_specifier(dist, attr, value):
|
jpayne@69
|
200 """Verify that value is a valid version specifier"""
|
jpayne@69
|
201 try:
|
jpayne@69
|
202 SpecifierSet(value)
|
jpayne@69
|
203 except (InvalidSpecifier, AttributeError) as error:
|
jpayne@69
|
204 msg = f"{attr!r} must be a string containing valid version specifiers; {error}"
|
jpayne@69
|
205 raise DistutilsSetupError(msg) from error
|
jpayne@69
|
206
|
jpayne@69
|
207
|
jpayne@69
|
208 def check_entry_points(dist, attr, value):
|
jpayne@69
|
209 """Verify that entry_points map is parseable"""
|
jpayne@69
|
210 try:
|
jpayne@69
|
211 _entry_points.load(value)
|
jpayne@69
|
212 except Exception as e:
|
jpayne@69
|
213 raise DistutilsSetupError(e) from e
|
jpayne@69
|
214
|
jpayne@69
|
215
|
jpayne@69
|
216 def check_package_data(dist, attr, value):
|
jpayne@69
|
217 """Verify that value is a dictionary of package names to glob lists"""
|
jpayne@69
|
218 if not isinstance(value, dict):
|
jpayne@69
|
219 raise DistutilsSetupError(
|
jpayne@69
|
220 "{!r} must be a dictionary mapping package names to lists of "
|
jpayne@69
|
221 "string wildcard patterns".format(attr)
|
jpayne@69
|
222 )
|
jpayne@69
|
223 for k, v in value.items():
|
jpayne@69
|
224 if not isinstance(k, str):
|
jpayne@69
|
225 raise DistutilsSetupError(
|
jpayne@69
|
226 "keys of {!r} dict must be strings (got {!r})".format(attr, k)
|
jpayne@69
|
227 )
|
jpayne@69
|
228 assert_string_list(dist, 'values of {!r} dict'.format(attr), v)
|
jpayne@69
|
229
|
jpayne@69
|
230
|
jpayne@69
|
231 def check_packages(dist, attr, value):
|
jpayne@69
|
232 for pkgname in value:
|
jpayne@69
|
233 if not re.match(r'\w+(\.\w+)*', pkgname):
|
jpayne@69
|
234 distutils.log.warn(
|
jpayne@69
|
235 "WARNING: %r not a valid package name; please use only "
|
jpayne@69
|
236 ".-separated package names in setup.py",
|
jpayne@69
|
237 pkgname,
|
jpayne@69
|
238 )
|
jpayne@69
|
239
|
jpayne@69
|
240
|
jpayne@69
|
241 if TYPE_CHECKING:
|
jpayne@69
|
242 # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962
|
jpayne@69
|
243 from distutils.core import Distribution as _Distribution
|
jpayne@69
|
244 else:
|
jpayne@69
|
245 _Distribution = get_unpatched(distutils.core.Distribution)
|
jpayne@69
|
246
|
jpayne@69
|
247
|
jpayne@69
|
248 class Distribution(_Distribution):
|
jpayne@69
|
249 """Distribution with support for tests and package data
|
jpayne@69
|
250
|
jpayne@69
|
251 This is an enhanced version of 'distutils.dist.Distribution' that
|
jpayne@69
|
252 effectively adds the following new optional keyword arguments to 'setup()':
|
jpayne@69
|
253
|
jpayne@69
|
254 'install_requires' -- a string or sequence of strings specifying project
|
jpayne@69
|
255 versions that the distribution requires when installed, in the format
|
jpayne@69
|
256 used by 'pkg_resources.require()'. They will be installed
|
jpayne@69
|
257 automatically when the package is installed. If you wish to use
|
jpayne@69
|
258 packages that are not available in PyPI, or want to give your users an
|
jpayne@69
|
259 alternate download location, you can add a 'find_links' option to the
|
jpayne@69
|
260 '[easy_install]' section of your project's 'setup.cfg' file, and then
|
jpayne@69
|
261 setuptools will scan the listed web pages for links that satisfy the
|
jpayne@69
|
262 requirements.
|
jpayne@69
|
263
|
jpayne@69
|
264 'extras_require' -- a dictionary mapping names of optional "extras" to the
|
jpayne@69
|
265 additional requirement(s) that using those extras incurs. For example,
|
jpayne@69
|
266 this::
|
jpayne@69
|
267
|
jpayne@69
|
268 extras_require = dict(reST = ["docutils>=0.3", "reSTedit"])
|
jpayne@69
|
269
|
jpayne@69
|
270 indicates that the distribution can optionally provide an extra
|
jpayne@69
|
271 capability called "reST", but it can only be used if docutils and
|
jpayne@69
|
272 reSTedit are installed. If the user installs your package using
|
jpayne@69
|
273 EasyInstall and requests one of your extras, the corresponding
|
jpayne@69
|
274 additional requirements will be installed if needed.
|
jpayne@69
|
275
|
jpayne@69
|
276 'package_data' -- a dictionary mapping package names to lists of filenames
|
jpayne@69
|
277 or globs to use to find data files contained in the named packages.
|
jpayne@69
|
278 If the dictionary has filenames or globs listed under '""' (the empty
|
jpayne@69
|
279 string), those names will be searched for in every package, in addition
|
jpayne@69
|
280 to any names for the specific package. Data files found using these
|
jpayne@69
|
281 names/globs will be installed along with the package, in the same
|
jpayne@69
|
282 location as the package. Note that globs are allowed to reference
|
jpayne@69
|
283 the contents of non-package subdirectories, as long as you use '/' as
|
jpayne@69
|
284 a path separator. (Globs are automatically converted to
|
jpayne@69
|
285 platform-specific paths at runtime.)
|
jpayne@69
|
286
|
jpayne@69
|
287 In addition to these new keywords, this class also has several new methods
|
jpayne@69
|
288 for manipulating the distribution's contents. For example, the 'include()'
|
jpayne@69
|
289 and 'exclude()' methods can be thought of as in-place add and subtract
|
jpayne@69
|
290 commands that add or remove packages, modules, extensions, and so on from
|
jpayne@69
|
291 the distribution.
|
jpayne@69
|
292 """
|
jpayne@69
|
293
|
jpayne@69
|
294 _DISTUTILS_UNSUPPORTED_METADATA = {
|
jpayne@69
|
295 'long_description_content_type': lambda: None,
|
jpayne@69
|
296 'project_urls': dict,
|
jpayne@69
|
297 'provides_extras': dict, # behaves like an ordered set
|
jpayne@69
|
298 'license_file': lambda: None,
|
jpayne@69
|
299 'license_files': lambda: None,
|
jpayne@69
|
300 'install_requires': list,
|
jpayne@69
|
301 'extras_require': dict,
|
jpayne@69
|
302 }
|
jpayne@69
|
303
|
jpayne@69
|
304 # Used by build_py, editable_wheel and install_lib commands for legacy namespaces
|
jpayne@69
|
305 namespace_packages: list[str] #: :meta private: DEPRECATED
|
jpayne@69
|
306
|
jpayne@69
|
307 # Any: Dynamic assignment results in Incompatible types in assignment
|
jpayne@69
|
308 def __init__(self, attrs: MutableMapping[str, Any] | None = None) -> None:
|
jpayne@69
|
309 have_package_data = hasattr(self, "package_data")
|
jpayne@69
|
310 if not have_package_data:
|
jpayne@69
|
311 self.package_data: dict[str, list[str]] = {}
|
jpayne@69
|
312 attrs = attrs or {}
|
jpayne@69
|
313 self.dist_files: list[tuple[str, str, str]] = []
|
jpayne@69
|
314 self.include_package_data: bool | None = None
|
jpayne@69
|
315 self.exclude_package_data: dict[str, list[str]] | None = None
|
jpayne@69
|
316 # Filter-out setuptools' specific options.
|
jpayne@69
|
317 self.src_root: str | None = attrs.pop("src_root", None)
|
jpayne@69
|
318 self.dependency_links: list[str] = attrs.pop('dependency_links', [])
|
jpayne@69
|
319 self.setup_requires: list[str] = attrs.pop('setup_requires', [])
|
jpayne@69
|
320 for ep in metadata.entry_points(group='distutils.setup_keywords'):
|
jpayne@69
|
321 vars(self).setdefault(ep.name, None)
|
jpayne@69
|
322
|
jpayne@69
|
323 metadata_only = set(self._DISTUTILS_UNSUPPORTED_METADATA)
|
jpayne@69
|
324 metadata_only -= {"install_requires", "extras_require"}
|
jpayne@69
|
325 dist_attrs = {k: v for k, v in attrs.items() if k not in metadata_only}
|
jpayne@69
|
326 _Distribution.__init__(self, dist_attrs)
|
jpayne@69
|
327
|
jpayne@69
|
328 # Private API (setuptools-use only, not restricted to Distribution)
|
jpayne@69
|
329 # Stores files that are referenced by the configuration and need to be in the
|
jpayne@69
|
330 # sdist (e.g. `version = file: VERSION.txt`)
|
jpayne@69
|
331 self._referenced_files: set[str] = set()
|
jpayne@69
|
332
|
jpayne@69
|
333 self.set_defaults = ConfigDiscovery(self)
|
jpayne@69
|
334
|
jpayne@69
|
335 self._set_metadata_defaults(attrs)
|
jpayne@69
|
336
|
jpayne@69
|
337 self.metadata.version = self._normalize_version(self.metadata.version)
|
jpayne@69
|
338 self._finalize_requires()
|
jpayne@69
|
339
|
jpayne@69
|
340 def _validate_metadata(self):
|
jpayne@69
|
341 required = {"name"}
|
jpayne@69
|
342 provided = {
|
jpayne@69
|
343 key
|
jpayne@69
|
344 for key in vars(self.metadata)
|
jpayne@69
|
345 if getattr(self.metadata, key, None) is not None
|
jpayne@69
|
346 }
|
jpayne@69
|
347 missing = required - provided
|
jpayne@69
|
348
|
jpayne@69
|
349 if missing:
|
jpayne@69
|
350 msg = f"Required package metadata is missing: {missing}"
|
jpayne@69
|
351 raise DistutilsSetupError(msg)
|
jpayne@69
|
352
|
jpayne@69
|
353 def _set_metadata_defaults(self, attrs):
|
jpayne@69
|
354 """
|
jpayne@69
|
355 Fill-in missing metadata fields not supported by distutils.
|
jpayne@69
|
356 Some fields may have been set by other tools (e.g. pbr).
|
jpayne@69
|
357 Those fields (vars(self.metadata)) take precedence to
|
jpayne@69
|
358 supplied attrs.
|
jpayne@69
|
359 """
|
jpayne@69
|
360 for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items():
|
jpayne@69
|
361 vars(self.metadata).setdefault(option, attrs.get(option, default()))
|
jpayne@69
|
362
|
jpayne@69
|
363 @staticmethod
|
jpayne@69
|
364 def _normalize_version(version):
|
jpayne@69
|
365 from . import sic
|
jpayne@69
|
366
|
jpayne@69
|
367 if isinstance(version, numbers.Number):
|
jpayne@69
|
368 # Some people apparently take "version number" too literally :)
|
jpayne@69
|
369 version = str(version)
|
jpayne@69
|
370 elif isinstance(version, sic) or version is None:
|
jpayne@69
|
371 return version
|
jpayne@69
|
372
|
jpayne@69
|
373 normalized = str(Version(version))
|
jpayne@69
|
374 if version != normalized:
|
jpayne@69
|
375 InformationOnly.emit(f"Normalizing '{version}' to '{normalized}'")
|
jpayne@69
|
376 return normalized
|
jpayne@69
|
377 return version
|
jpayne@69
|
378
|
jpayne@69
|
379 def _finalize_requires(self):
|
jpayne@69
|
380 """
|
jpayne@69
|
381 Set `metadata.python_requires` and fix environment markers
|
jpayne@69
|
382 in `install_requires` and `extras_require`.
|
jpayne@69
|
383 """
|
jpayne@69
|
384 if getattr(self, 'python_requires', None):
|
jpayne@69
|
385 self.metadata.python_requires = self.python_requires
|
jpayne@69
|
386
|
jpayne@69
|
387 self._normalize_requires()
|
jpayne@69
|
388 self.metadata.install_requires = self.install_requires
|
jpayne@69
|
389 self.metadata.extras_require = self.extras_require
|
jpayne@69
|
390
|
jpayne@69
|
391 if self.extras_require:
|
jpayne@69
|
392 for extra in self.extras_require.keys():
|
jpayne@69
|
393 # Setuptools allows a weird "<name>:<env markers> syntax for extras
|
jpayne@69
|
394 extra = extra.split(':')[0]
|
jpayne@69
|
395 if extra:
|
jpayne@69
|
396 self.metadata.provides_extras.setdefault(extra)
|
jpayne@69
|
397
|
jpayne@69
|
398 def _normalize_requires(self):
|
jpayne@69
|
399 """Make sure requirement-related attributes exist and are normalized"""
|
jpayne@69
|
400 install_requires = getattr(self, "install_requires", None) or []
|
jpayne@69
|
401 extras_require = getattr(self, "extras_require", None) or {}
|
jpayne@69
|
402 self.install_requires = list(map(str, _reqs.parse(install_requires)))
|
jpayne@69
|
403 self.extras_require = {
|
jpayne@69
|
404 k: list(map(str, _reqs.parse(v or []))) for k, v in extras_require.items()
|
jpayne@69
|
405 }
|
jpayne@69
|
406
|
jpayne@69
|
407 def _finalize_license_files(self) -> None:
|
jpayne@69
|
408 """Compute names of all license files which should be included."""
|
jpayne@69
|
409 license_files: list[str] | None = self.metadata.license_files
|
jpayne@69
|
410 patterns: list[str] = license_files if license_files else []
|
jpayne@69
|
411
|
jpayne@69
|
412 license_file: str | None = self.metadata.license_file
|
jpayne@69
|
413 if license_file and license_file not in patterns:
|
jpayne@69
|
414 patterns.append(license_file)
|
jpayne@69
|
415
|
jpayne@69
|
416 if license_files is None and license_file is None:
|
jpayne@69
|
417 # Default patterns match the ones wheel uses
|
jpayne@69
|
418 # See https://wheel.readthedocs.io/en/stable/user_guide.html
|
jpayne@69
|
419 # -> 'Including license files in the generated wheel file'
|
jpayne@69
|
420 patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']
|
jpayne@69
|
421
|
jpayne@69
|
422 self.metadata.license_files = list(
|
jpayne@69
|
423 unique_everseen(self._expand_patterns(patterns))
|
jpayne@69
|
424 )
|
jpayne@69
|
425
|
jpayne@69
|
426 @staticmethod
|
jpayne@69
|
427 def _expand_patterns(patterns):
|
jpayne@69
|
428 """
|
jpayne@69
|
429 >>> list(Distribution._expand_patterns(['LICENSE']))
|
jpayne@69
|
430 ['LICENSE']
|
jpayne@69
|
431 >>> list(Distribution._expand_patterns(['pyproject.toml', 'LIC*']))
|
jpayne@69
|
432 ['pyproject.toml', 'LICENSE']
|
jpayne@69
|
433 """
|
jpayne@69
|
434 return (
|
jpayne@69
|
435 path
|
jpayne@69
|
436 for pattern in patterns
|
jpayne@69
|
437 for path in sorted(iglob(pattern))
|
jpayne@69
|
438 if not path.endswith('~') and os.path.isfile(path)
|
jpayne@69
|
439 )
|
jpayne@69
|
440
|
jpayne@69
|
441 # FIXME: 'Distribution._parse_config_files' is too complex (14)
|
jpayne@69
|
442 def _parse_config_files(self, filenames=None): # noqa: C901
|
jpayne@69
|
443 """
|
jpayne@69
|
444 Adapted from distutils.dist.Distribution.parse_config_files,
|
jpayne@69
|
445 this method provides the same functionality in subtly-improved
|
jpayne@69
|
446 ways.
|
jpayne@69
|
447 """
|
jpayne@69
|
448 from configparser import ConfigParser
|
jpayne@69
|
449
|
jpayne@69
|
450 # Ignore install directory options if we have a venv
|
jpayne@69
|
451 ignore_options = (
|
jpayne@69
|
452 []
|
jpayne@69
|
453 if sys.prefix == sys.base_prefix
|
jpayne@69
|
454 else [
|
jpayne@69
|
455 'install-base',
|
jpayne@69
|
456 'install-platbase',
|
jpayne@69
|
457 'install-lib',
|
jpayne@69
|
458 'install-platlib',
|
jpayne@69
|
459 'install-purelib',
|
jpayne@69
|
460 'install-headers',
|
jpayne@69
|
461 'install-scripts',
|
jpayne@69
|
462 'install-data',
|
jpayne@69
|
463 'prefix',
|
jpayne@69
|
464 'exec-prefix',
|
jpayne@69
|
465 'home',
|
jpayne@69
|
466 'user',
|
jpayne@69
|
467 'root',
|
jpayne@69
|
468 ]
|
jpayne@69
|
469 )
|
jpayne@69
|
470
|
jpayne@69
|
471 ignore_options = frozenset(ignore_options)
|
jpayne@69
|
472
|
jpayne@69
|
473 if filenames is None:
|
jpayne@69
|
474 filenames = self.find_config_files()
|
jpayne@69
|
475
|
jpayne@69
|
476 if DEBUG:
|
jpayne@69
|
477 self.announce("Distribution.parse_config_files():")
|
jpayne@69
|
478
|
jpayne@69
|
479 parser = ConfigParser()
|
jpayne@69
|
480 parser.optionxform = str
|
jpayne@69
|
481 for filename in filenames:
|
jpayne@69
|
482 with open(filename, encoding='utf-8') as reader:
|
jpayne@69
|
483 if DEBUG:
|
jpayne@69
|
484 self.announce(" reading {filename}".format(**locals()))
|
jpayne@69
|
485 parser.read_file(reader)
|
jpayne@69
|
486 for section in parser.sections():
|
jpayne@69
|
487 options = parser.options(section)
|
jpayne@69
|
488 opt_dict = self.get_option_dict(section)
|
jpayne@69
|
489
|
jpayne@69
|
490 for opt in options:
|
jpayne@69
|
491 if opt == '__name__' or opt in ignore_options:
|
jpayne@69
|
492 continue
|
jpayne@69
|
493
|
jpayne@69
|
494 val = parser.get(section, opt)
|
jpayne@69
|
495 opt = self.warn_dash_deprecation(opt, section)
|
jpayne@69
|
496 opt = self.make_option_lowercase(opt, section)
|
jpayne@69
|
497 opt_dict[opt] = (filename, val)
|
jpayne@69
|
498
|
jpayne@69
|
499 # Make the ConfigParser forget everything (so we retain
|
jpayne@69
|
500 # the original filenames that options come from)
|
jpayne@69
|
501 parser.__init__()
|
jpayne@69
|
502
|
jpayne@69
|
503 if 'global' not in self.command_options:
|
jpayne@69
|
504 return
|
jpayne@69
|
505
|
jpayne@69
|
506 # If there was a "global" section in the config file, use it
|
jpayne@69
|
507 # to set Distribution options.
|
jpayne@69
|
508
|
jpayne@69
|
509 for opt, (src, val) in self.command_options['global'].items():
|
jpayne@69
|
510 alias = self.negative_opt.get(opt)
|
jpayne@69
|
511 if alias:
|
jpayne@69
|
512 val = not strtobool(val)
|
jpayne@69
|
513 elif opt in ('verbose', 'dry_run'): # ugh!
|
jpayne@69
|
514 val = strtobool(val)
|
jpayne@69
|
515
|
jpayne@69
|
516 try:
|
jpayne@69
|
517 setattr(self, alias or opt, val)
|
jpayne@69
|
518 except ValueError as e:
|
jpayne@69
|
519 raise DistutilsOptionError(e) from e
|
jpayne@69
|
520
|
jpayne@69
|
521 def warn_dash_deprecation(self, opt: str, section: str):
|
jpayne@69
|
522 if section in (
|
jpayne@69
|
523 'options.extras_require',
|
jpayne@69
|
524 'options.data_files',
|
jpayne@69
|
525 ):
|
jpayne@69
|
526 return opt
|
jpayne@69
|
527
|
jpayne@69
|
528 underscore_opt = opt.replace('-', '_')
|
jpayne@69
|
529 commands = list(
|
jpayne@69
|
530 itertools.chain(
|
jpayne@69
|
531 distutils.command.__all__,
|
jpayne@69
|
532 self._setuptools_commands(),
|
jpayne@69
|
533 )
|
jpayne@69
|
534 )
|
jpayne@69
|
535 if (
|
jpayne@69
|
536 not section.startswith('options')
|
jpayne@69
|
537 and section != 'metadata'
|
jpayne@69
|
538 and section not in commands
|
jpayne@69
|
539 ):
|
jpayne@69
|
540 return underscore_opt
|
jpayne@69
|
541
|
jpayne@69
|
542 if '-' in opt:
|
jpayne@69
|
543 SetuptoolsDeprecationWarning.emit(
|
jpayne@69
|
544 "Invalid dash-separated options",
|
jpayne@69
|
545 f"""
|
jpayne@69
|
546 Usage of dash-separated {opt!r} will not be supported in future
|
jpayne@69
|
547 versions. Please use the underscore name {underscore_opt!r} instead.
|
jpayne@69
|
548 """,
|
jpayne@69
|
549 see_docs="userguide/declarative_config.html",
|
jpayne@69
|
550 due_date=(2025, 3, 3),
|
jpayne@69
|
551 # Warning initially introduced in 3 Mar 2021
|
jpayne@69
|
552 )
|
jpayne@69
|
553 return underscore_opt
|
jpayne@69
|
554
|
jpayne@69
|
555 def _setuptools_commands(self):
|
jpayne@69
|
556 try:
|
jpayne@69
|
557 entry_points = metadata.distribution('setuptools').entry_points
|
jpayne@69
|
558 return {ep.name for ep in entry_points} # Avoid newer API for compatibility
|
jpayne@69
|
559 except metadata.PackageNotFoundError:
|
jpayne@69
|
560 # during bootstrapping, distribution doesn't exist
|
jpayne@69
|
561 return []
|
jpayne@69
|
562
|
jpayne@69
|
563 def make_option_lowercase(self, opt: str, section: str):
|
jpayne@69
|
564 if section != 'metadata' or opt.islower():
|
jpayne@69
|
565 return opt
|
jpayne@69
|
566
|
jpayne@69
|
567 lowercase_opt = opt.lower()
|
jpayne@69
|
568 SetuptoolsDeprecationWarning.emit(
|
jpayne@69
|
569 "Invalid uppercase configuration",
|
jpayne@69
|
570 f"""
|
jpayne@69
|
571 Usage of uppercase key {opt!r} in {section!r} will not be supported in
|
jpayne@69
|
572 future versions. Please use lowercase {lowercase_opt!r} instead.
|
jpayne@69
|
573 """,
|
jpayne@69
|
574 see_docs="userguide/declarative_config.html",
|
jpayne@69
|
575 due_date=(2025, 3, 3),
|
jpayne@69
|
576 # Warning initially introduced in 6 Mar 2021
|
jpayne@69
|
577 )
|
jpayne@69
|
578 return lowercase_opt
|
jpayne@69
|
579
|
jpayne@69
|
580 # FIXME: 'Distribution._set_command_options' is too complex (14)
|
jpayne@69
|
581 def _set_command_options(self, command_obj, option_dict=None): # noqa: C901
|
jpayne@69
|
582 """
|
jpayne@69
|
583 Set the options for 'command_obj' from 'option_dict'. Basically
|
jpayne@69
|
584 this means copying elements of a dictionary ('option_dict') to
|
jpayne@69
|
585 attributes of an instance ('command').
|
jpayne@69
|
586
|
jpayne@69
|
587 'command_obj' must be a Command instance. If 'option_dict' is not
|
jpayne@69
|
588 supplied, uses the standard option dictionary for this command
|
jpayne@69
|
589 (from 'self.command_options').
|
jpayne@69
|
590
|
jpayne@69
|
591 (Adopted from distutils.dist.Distribution._set_command_options)
|
jpayne@69
|
592 """
|
jpayne@69
|
593 command_name = command_obj.get_command_name()
|
jpayne@69
|
594 if option_dict is None:
|
jpayne@69
|
595 option_dict = self.get_option_dict(command_name)
|
jpayne@69
|
596
|
jpayne@69
|
597 if DEBUG:
|
jpayne@69
|
598 self.announce(" setting options for '%s' command:" % command_name)
|
jpayne@69
|
599 for option, (source, value) in option_dict.items():
|
jpayne@69
|
600 if DEBUG:
|
jpayne@69
|
601 self.announce(" %s = %s (from %s)" % (option, value, source))
|
jpayne@69
|
602 try:
|
jpayne@69
|
603 bool_opts = [translate_longopt(o) for o in command_obj.boolean_options]
|
jpayne@69
|
604 except AttributeError:
|
jpayne@69
|
605 bool_opts = []
|
jpayne@69
|
606 try:
|
jpayne@69
|
607 neg_opt = command_obj.negative_opt
|
jpayne@69
|
608 except AttributeError:
|
jpayne@69
|
609 neg_opt = {}
|
jpayne@69
|
610
|
jpayne@69
|
611 try:
|
jpayne@69
|
612 is_string = isinstance(value, str)
|
jpayne@69
|
613 if option in neg_opt and is_string:
|
jpayne@69
|
614 setattr(command_obj, neg_opt[option], not strtobool(value))
|
jpayne@69
|
615 elif option in bool_opts and is_string:
|
jpayne@69
|
616 setattr(command_obj, option, strtobool(value))
|
jpayne@69
|
617 elif hasattr(command_obj, option):
|
jpayne@69
|
618 setattr(command_obj, option, value)
|
jpayne@69
|
619 else:
|
jpayne@69
|
620 raise DistutilsOptionError(
|
jpayne@69
|
621 "error in %s: command '%s' has no such option '%s'"
|
jpayne@69
|
622 % (source, command_name, option)
|
jpayne@69
|
623 )
|
jpayne@69
|
624 except ValueError as e:
|
jpayne@69
|
625 raise DistutilsOptionError(e) from e
|
jpayne@69
|
626
|
jpayne@69
|
627 def _get_project_config_files(self, filenames: Iterable[StrPath] | None):
|
jpayne@69
|
628 """Add default file and split between INI and TOML"""
|
jpayne@69
|
629 tomlfiles = []
|
jpayne@69
|
630 standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml")
|
jpayne@69
|
631 if filenames is not None:
|
jpayne@69
|
632 parts = partition(lambda f: Path(f).suffix == ".toml", filenames)
|
jpayne@69
|
633 filenames = list(parts[0]) # 1st element => predicate is False
|
jpayne@69
|
634 tomlfiles = list(parts[1]) # 2nd element => predicate is True
|
jpayne@69
|
635 elif standard_project_metadata.exists():
|
jpayne@69
|
636 tomlfiles = [standard_project_metadata]
|
jpayne@69
|
637 return filenames, tomlfiles
|
jpayne@69
|
638
|
jpayne@69
|
639 def parse_config_files(
|
jpayne@69
|
640 self,
|
jpayne@69
|
641 filenames: Iterable[StrPath] | None = None,
|
jpayne@69
|
642 ignore_option_errors: bool = False,
|
jpayne@69
|
643 ):
|
jpayne@69
|
644 """Parses configuration files from various levels
|
jpayne@69
|
645 and loads configuration.
|
jpayne@69
|
646 """
|
jpayne@69
|
647 inifiles, tomlfiles = self._get_project_config_files(filenames)
|
jpayne@69
|
648
|
jpayne@69
|
649 self._parse_config_files(filenames=inifiles)
|
jpayne@69
|
650
|
jpayne@69
|
651 setupcfg.parse_configuration(
|
jpayne@69
|
652 self, self.command_options, ignore_option_errors=ignore_option_errors
|
jpayne@69
|
653 )
|
jpayne@69
|
654 for filename in tomlfiles:
|
jpayne@69
|
655 pyprojecttoml.apply_configuration(self, filename, ignore_option_errors)
|
jpayne@69
|
656
|
jpayne@69
|
657 self._finalize_requires()
|
jpayne@69
|
658 self._finalize_license_files()
|
jpayne@69
|
659
|
jpayne@69
|
660 def fetch_build_eggs(self, requires: _StrOrIter):
|
jpayne@69
|
661 """Resolve pre-setup requirements"""
|
jpayne@69
|
662 from .installer import _fetch_build_eggs
|
jpayne@69
|
663
|
jpayne@69
|
664 return _fetch_build_eggs(self, requires)
|
jpayne@69
|
665
|
jpayne@69
|
666 def finalize_options(self):
|
jpayne@69
|
667 """
|
jpayne@69
|
668 Allow plugins to apply arbitrary operations to the
|
jpayne@69
|
669 distribution. Each hook may optionally define a 'order'
|
jpayne@69
|
670 to influence the order of execution. Smaller numbers
|
jpayne@69
|
671 go first and the default is 0.
|
jpayne@69
|
672 """
|
jpayne@69
|
673 group = 'setuptools.finalize_distribution_options'
|
jpayne@69
|
674
|
jpayne@69
|
675 def by_order(hook):
|
jpayne@69
|
676 return getattr(hook, 'order', 0)
|
jpayne@69
|
677
|
jpayne@69
|
678 defined = metadata.entry_points(group=group)
|
jpayne@69
|
679 filtered = itertools.filterfalse(self._removed, defined)
|
jpayne@69
|
680 loaded = map(lambda e: e.load(), filtered)
|
jpayne@69
|
681 for ep in sorted(loaded, key=by_order):
|
jpayne@69
|
682 ep(self)
|
jpayne@69
|
683
|
jpayne@69
|
684 @staticmethod
|
jpayne@69
|
685 def _removed(ep):
|
jpayne@69
|
686 """
|
jpayne@69
|
687 When removing an entry point, if metadata is loaded
|
jpayne@69
|
688 from an older version of Setuptools, that removed
|
jpayne@69
|
689 entry point will attempt to be loaded and will fail.
|
jpayne@69
|
690 See #2765 for more details.
|
jpayne@69
|
691 """
|
jpayne@69
|
692 removed = {
|
jpayne@69
|
693 # removed 2021-09-05
|
jpayne@69
|
694 '2to3_doctests',
|
jpayne@69
|
695 }
|
jpayne@69
|
696 return ep.name in removed
|
jpayne@69
|
697
|
jpayne@69
|
698 def _finalize_setup_keywords(self):
|
jpayne@69
|
699 for ep in metadata.entry_points(group='distutils.setup_keywords'):
|
jpayne@69
|
700 value = getattr(self, ep.name, None)
|
jpayne@69
|
701 if value is not None:
|
jpayne@69
|
702 ep.load()(self, ep.name, value)
|
jpayne@69
|
703
|
jpayne@69
|
704 def get_egg_cache_dir(self):
|
jpayne@69
|
705 from . import windows_support
|
jpayne@69
|
706
|
jpayne@69
|
707 egg_cache_dir = os.path.join(os.curdir, '.eggs')
|
jpayne@69
|
708 if not os.path.exists(egg_cache_dir):
|
jpayne@69
|
709 os.mkdir(egg_cache_dir)
|
jpayne@69
|
710 windows_support.hide_file(egg_cache_dir)
|
jpayne@69
|
711 readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt')
|
jpayne@69
|
712 with open(readme_txt_filename, 'w', encoding="utf-8") as f:
|
jpayne@69
|
713 f.write(
|
jpayne@69
|
714 'This directory contains eggs that were downloaded '
|
jpayne@69
|
715 'by setuptools to build, test, and run plug-ins.\n\n'
|
jpayne@69
|
716 )
|
jpayne@69
|
717 f.write(
|
jpayne@69
|
718 'This directory caches those eggs to prevent '
|
jpayne@69
|
719 'repeated downloads.\n\n'
|
jpayne@69
|
720 )
|
jpayne@69
|
721 f.write('However, it is safe to delete this directory.\n\n')
|
jpayne@69
|
722
|
jpayne@69
|
723 return egg_cache_dir
|
jpayne@69
|
724
|
jpayne@69
|
725 def fetch_build_egg(self, req):
|
jpayne@69
|
726 """Fetch an egg needed for building"""
|
jpayne@69
|
727 from .installer import fetch_build_egg
|
jpayne@69
|
728
|
jpayne@69
|
729 return fetch_build_egg(self, req)
|
jpayne@69
|
730
|
jpayne@69
|
731 def get_command_class(self, command: str):
|
jpayne@69
|
732 """Pluggable version of get_command_class()"""
|
jpayne@69
|
733 if command in self.cmdclass:
|
jpayne@69
|
734 return self.cmdclass[command]
|
jpayne@69
|
735
|
jpayne@69
|
736 # Special case bdist_wheel so it's never loaded from "wheel"
|
jpayne@69
|
737 if command == 'bdist_wheel':
|
jpayne@69
|
738 from .command.bdist_wheel import bdist_wheel
|
jpayne@69
|
739
|
jpayne@69
|
740 return bdist_wheel
|
jpayne@69
|
741
|
jpayne@69
|
742 eps = metadata.entry_points(group='distutils.commands', name=command)
|
jpayne@69
|
743 for ep in eps:
|
jpayne@69
|
744 self.cmdclass[command] = cmdclass = ep.load()
|
jpayne@69
|
745 return cmdclass
|
jpayne@69
|
746 else:
|
jpayne@69
|
747 return _Distribution.get_command_class(self, command)
|
jpayne@69
|
748
|
jpayne@69
|
749 def print_commands(self):
|
jpayne@69
|
750 for ep in metadata.entry_points(group='distutils.commands'):
|
jpayne@69
|
751 if ep.name not in self.cmdclass:
|
jpayne@69
|
752 cmdclass = ep.load()
|
jpayne@69
|
753 self.cmdclass[ep.name] = cmdclass
|
jpayne@69
|
754 return _Distribution.print_commands(self)
|
jpayne@69
|
755
|
jpayne@69
|
756 def get_command_list(self):
|
jpayne@69
|
757 for ep in metadata.entry_points(group='distutils.commands'):
|
jpayne@69
|
758 if ep.name not in self.cmdclass:
|
jpayne@69
|
759 cmdclass = ep.load()
|
jpayne@69
|
760 self.cmdclass[ep.name] = cmdclass
|
jpayne@69
|
761 return _Distribution.get_command_list(self)
|
jpayne@69
|
762
|
jpayne@69
|
763 def include(self, **attrs):
|
jpayne@69
|
764 """Add items to distribution that are named in keyword arguments
|
jpayne@69
|
765
|
jpayne@69
|
766 For example, 'dist.include(py_modules=["x"])' would add 'x' to
|
jpayne@69
|
767 the distribution's 'py_modules' attribute, if it was not already
|
jpayne@69
|
768 there.
|
jpayne@69
|
769
|
jpayne@69
|
770 Currently, this method only supports inclusion for attributes that are
|
jpayne@69
|
771 lists or tuples. If you need to add support for adding to other
|
jpayne@69
|
772 attributes in this or a subclass, you can add an '_include_X' method,
|
jpayne@69
|
773 where 'X' is the name of the attribute. The method will be called with
|
jpayne@69
|
774 the value passed to 'include()'. So, 'dist.include(foo={"bar":"baz"})'
|
jpayne@69
|
775 will try to call 'dist._include_foo({"bar":"baz"})', which can then
|
jpayne@69
|
776 handle whatever special inclusion logic is needed.
|
jpayne@69
|
777 """
|
jpayne@69
|
778 for k, v in attrs.items():
|
jpayne@69
|
779 include = getattr(self, '_include_' + k, None)
|
jpayne@69
|
780 if include:
|
jpayne@69
|
781 include(v)
|
jpayne@69
|
782 else:
|
jpayne@69
|
783 self._include_misc(k, v)
|
jpayne@69
|
784
|
jpayne@69
|
785 def exclude_package(self, package: str):
|
jpayne@69
|
786 """Remove packages, modules, and extensions in named package"""
|
jpayne@69
|
787
|
jpayne@69
|
788 pfx = package + '.'
|
jpayne@69
|
789 if self.packages:
|
jpayne@69
|
790 self.packages = [
|
jpayne@69
|
791 p for p in self.packages if p != package and not p.startswith(pfx)
|
jpayne@69
|
792 ]
|
jpayne@69
|
793
|
jpayne@69
|
794 if self.py_modules:
|
jpayne@69
|
795 self.py_modules = [
|
jpayne@69
|
796 p for p in self.py_modules if p != package and not p.startswith(pfx)
|
jpayne@69
|
797 ]
|
jpayne@69
|
798
|
jpayne@69
|
799 if self.ext_modules:
|
jpayne@69
|
800 self.ext_modules = [
|
jpayne@69
|
801 p
|
jpayne@69
|
802 for p in self.ext_modules
|
jpayne@69
|
803 if p.name != package and not p.name.startswith(pfx)
|
jpayne@69
|
804 ]
|
jpayne@69
|
805
|
jpayne@69
|
806 def has_contents_for(self, package: str):
|
jpayne@69
|
807 """Return true if 'exclude_package(package)' would do something"""
|
jpayne@69
|
808
|
jpayne@69
|
809 pfx = package + '.'
|
jpayne@69
|
810
|
jpayne@69
|
811 for p in self.iter_distribution_names():
|
jpayne@69
|
812 if p == package or p.startswith(pfx):
|
jpayne@69
|
813 return True
|
jpayne@69
|
814
|
jpayne@69
|
815 return False
|
jpayne@69
|
816
|
jpayne@69
|
817 def _exclude_misc(self, name: str, value: _Sequence) -> None:
|
jpayne@69
|
818 """Handle 'exclude()' for list/tuple attrs without a special handler"""
|
jpayne@69
|
819 if not isinstance(value, _sequence):
|
jpayne@69
|
820 raise DistutilsSetupError(
|
jpayne@69
|
821 f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})"
|
jpayne@69
|
822 )
|
jpayne@69
|
823 try:
|
jpayne@69
|
824 old = getattr(self, name)
|
jpayne@69
|
825 except AttributeError as e:
|
jpayne@69
|
826 raise DistutilsSetupError("%s: No such distribution setting" % name) from e
|
jpayne@69
|
827 if old is not None and not isinstance(old, _sequence):
|
jpayne@69
|
828 raise DistutilsSetupError(
|
jpayne@69
|
829 name + ": this setting cannot be changed via include/exclude"
|
jpayne@69
|
830 )
|
jpayne@69
|
831 elif old:
|
jpayne@69
|
832 setattr(self, name, [item for item in old if item not in value])
|
jpayne@69
|
833
|
jpayne@69
|
834 def _include_misc(self, name: str, value: _Sequence) -> None:
|
jpayne@69
|
835 """Handle 'include()' for list/tuple attrs without a special handler"""
|
jpayne@69
|
836
|
jpayne@69
|
837 if not isinstance(value, _sequence):
|
jpayne@69
|
838 raise DistutilsSetupError(
|
jpayne@69
|
839 f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})"
|
jpayne@69
|
840 )
|
jpayne@69
|
841 try:
|
jpayne@69
|
842 old = getattr(self, name)
|
jpayne@69
|
843 except AttributeError as e:
|
jpayne@69
|
844 raise DistutilsSetupError("%s: No such distribution setting" % name) from e
|
jpayne@69
|
845 if old is None:
|
jpayne@69
|
846 setattr(self, name, value)
|
jpayne@69
|
847 elif not isinstance(old, _sequence):
|
jpayne@69
|
848 raise DistutilsSetupError(
|
jpayne@69
|
849 name + ": this setting cannot be changed via include/exclude"
|
jpayne@69
|
850 )
|
jpayne@69
|
851 else:
|
jpayne@69
|
852 new = [item for item in value if item not in old]
|
jpayne@69
|
853 setattr(self, name, list(old) + new)
|
jpayne@69
|
854
|
jpayne@69
|
855 def exclude(self, **attrs):
|
jpayne@69
|
856 """Remove items from distribution that are named in keyword arguments
|
jpayne@69
|
857
|
jpayne@69
|
858 For example, 'dist.exclude(py_modules=["x"])' would remove 'x' from
|
jpayne@69
|
859 the distribution's 'py_modules' attribute. Excluding packages uses
|
jpayne@69
|
860 the 'exclude_package()' method, so all of the package's contained
|
jpayne@69
|
861 packages, modules, and extensions are also excluded.
|
jpayne@69
|
862
|
jpayne@69
|
863 Currently, this method only supports exclusion from attributes that are
|
jpayne@69
|
864 lists or tuples. If you need to add support for excluding from other
|
jpayne@69
|
865 attributes in this or a subclass, you can add an '_exclude_X' method,
|
jpayne@69
|
866 where 'X' is the name of the attribute. The method will be called with
|
jpayne@69
|
867 the value passed to 'exclude()'. So, 'dist.exclude(foo={"bar":"baz"})'
|
jpayne@69
|
868 will try to call 'dist._exclude_foo({"bar":"baz"})', which can then
|
jpayne@69
|
869 handle whatever special exclusion logic is needed.
|
jpayne@69
|
870 """
|
jpayne@69
|
871 for k, v in attrs.items():
|
jpayne@69
|
872 exclude = getattr(self, '_exclude_' + k, None)
|
jpayne@69
|
873 if exclude:
|
jpayne@69
|
874 exclude(v)
|
jpayne@69
|
875 else:
|
jpayne@69
|
876 self._exclude_misc(k, v)
|
jpayne@69
|
877
|
jpayne@69
|
878 def _exclude_packages(self, packages: _Sequence) -> None:
|
jpayne@69
|
879 if not isinstance(packages, _sequence):
|
jpayne@69
|
880 raise DistutilsSetupError(
|
jpayne@69
|
881 f"packages: setting must be of type <{_sequence_type_repr}> (got {packages!r})"
|
jpayne@69
|
882 )
|
jpayne@69
|
883 list(map(self.exclude_package, packages))
|
jpayne@69
|
884
|
jpayne@69
|
885 def _parse_command_opts(self, parser, args):
|
jpayne@69
|
886 # Remove --with-X/--without-X options when processing command args
|
jpayne@69
|
887 self.global_options = self.__class__.global_options
|
jpayne@69
|
888 self.negative_opt = self.__class__.negative_opt
|
jpayne@69
|
889
|
jpayne@69
|
890 # First, expand any aliases
|
jpayne@69
|
891 command = args[0]
|
jpayne@69
|
892 aliases = self.get_option_dict('aliases')
|
jpayne@69
|
893 while command in aliases:
|
jpayne@69
|
894 src, alias = aliases[command]
|
jpayne@69
|
895 del aliases[command] # ensure each alias can expand only once!
|
jpayne@69
|
896 import shlex
|
jpayne@69
|
897
|
jpayne@69
|
898 args[:1] = shlex.split(alias, True)
|
jpayne@69
|
899 command = args[0]
|
jpayne@69
|
900
|
jpayne@69
|
901 nargs = _Distribution._parse_command_opts(self, parser, args)
|
jpayne@69
|
902
|
jpayne@69
|
903 # Handle commands that want to consume all remaining arguments
|
jpayne@69
|
904 cmd_class = self.get_command_class(command)
|
jpayne@69
|
905 if getattr(cmd_class, 'command_consumes_arguments', None):
|
jpayne@69
|
906 self.get_option_dict(command)['args'] = ("command line", nargs)
|
jpayne@69
|
907 if nargs is not None:
|
jpayne@69
|
908 return []
|
jpayne@69
|
909
|
jpayne@69
|
910 return nargs
|
jpayne@69
|
911
|
jpayne@69
|
912 def get_cmdline_options(self):
|
jpayne@69
|
913 """Return a '{cmd: {opt:val}}' map of all command-line options
|
jpayne@69
|
914
|
jpayne@69
|
915 Option names are all long, but do not include the leading '--', and
|
jpayne@69
|
916 contain dashes rather than underscores. If the option doesn't take
|
jpayne@69
|
917 an argument (e.g. '--quiet'), the 'val' is 'None'.
|
jpayne@69
|
918
|
jpayne@69
|
919 Note that options provided by config files are intentionally excluded.
|
jpayne@69
|
920 """
|
jpayne@69
|
921
|
jpayne@69
|
922 d = {}
|
jpayne@69
|
923
|
jpayne@69
|
924 for cmd, opts in self.command_options.items():
|
jpayne@69
|
925 for opt, (src, val) in opts.items():
|
jpayne@69
|
926 if src != "command line":
|
jpayne@69
|
927 continue
|
jpayne@69
|
928
|
jpayne@69
|
929 opt = opt.replace('_', '-')
|
jpayne@69
|
930
|
jpayne@69
|
931 if val == 0:
|
jpayne@69
|
932 cmdobj = self.get_command_obj(cmd)
|
jpayne@69
|
933 neg_opt = self.negative_opt.copy()
|
jpayne@69
|
934 neg_opt.update(getattr(cmdobj, 'negative_opt', {}))
|
jpayne@69
|
935 for neg, pos in neg_opt.items():
|
jpayne@69
|
936 if pos == opt:
|
jpayne@69
|
937 opt = neg
|
jpayne@69
|
938 val = None
|
jpayne@69
|
939 break
|
jpayne@69
|
940 else:
|
jpayne@69
|
941 raise AssertionError("Shouldn't be able to get here")
|
jpayne@69
|
942
|
jpayne@69
|
943 elif val == 1:
|
jpayne@69
|
944 val = None
|
jpayne@69
|
945
|
jpayne@69
|
946 d.setdefault(cmd, {})[opt] = val
|
jpayne@69
|
947
|
jpayne@69
|
948 return d
|
jpayne@69
|
949
|
jpayne@69
|
950 def iter_distribution_names(self):
|
jpayne@69
|
951 """Yield all packages, modules, and extension names in distribution"""
|
jpayne@69
|
952
|
jpayne@69
|
953 yield from self.packages or ()
|
jpayne@69
|
954
|
jpayne@69
|
955 yield from self.py_modules or ()
|
jpayne@69
|
956
|
jpayne@69
|
957 for ext in self.ext_modules or ():
|
jpayne@69
|
958 if isinstance(ext, tuple):
|
jpayne@69
|
959 name, buildinfo = ext
|
jpayne@69
|
960 else:
|
jpayne@69
|
961 name = ext.name
|
jpayne@69
|
962 if name.endswith('module'):
|
jpayne@69
|
963 name = name[:-6]
|
jpayne@69
|
964 yield name
|
jpayne@69
|
965
|
jpayne@69
|
966 def handle_display_options(self, option_order):
|
jpayne@69
|
967 """If there were any non-global "display-only" options
|
jpayne@69
|
968 (--help-commands or the metadata display options) on the command
|
jpayne@69
|
969 line, display the requested info and return true; else return
|
jpayne@69
|
970 false.
|
jpayne@69
|
971 """
|
jpayne@69
|
972 import sys
|
jpayne@69
|
973
|
jpayne@69
|
974 if self.help_commands:
|
jpayne@69
|
975 return _Distribution.handle_display_options(self, option_order)
|
jpayne@69
|
976
|
jpayne@69
|
977 # Stdout may be StringIO (e.g. in tests)
|
jpayne@69
|
978 if not isinstance(sys.stdout, io.TextIOWrapper):
|
jpayne@69
|
979 return _Distribution.handle_display_options(self, option_order)
|
jpayne@69
|
980
|
jpayne@69
|
981 # Don't wrap stdout if utf-8 is already the encoding. Provides
|
jpayne@69
|
982 # workaround for #334.
|
jpayne@69
|
983 if sys.stdout.encoding.lower() in ('utf-8', 'utf8'):
|
jpayne@69
|
984 return _Distribution.handle_display_options(self, option_order)
|
jpayne@69
|
985
|
jpayne@69
|
986 # Print metadata in UTF-8 no matter the platform
|
jpayne@69
|
987 encoding = sys.stdout.encoding
|
jpayne@69
|
988 sys.stdout.reconfigure(encoding='utf-8')
|
jpayne@69
|
989 try:
|
jpayne@69
|
990 return _Distribution.handle_display_options(self, option_order)
|
jpayne@69
|
991 finally:
|
jpayne@69
|
992 sys.stdout.reconfigure(encoding=encoding)
|
jpayne@69
|
993
|
jpayne@69
|
994 def run_command(self, command):
|
jpayne@69
|
995 self.set_defaults()
|
jpayne@69
|
996 # Postpone defaults until all explicit configuration is considered
|
jpayne@69
|
997 # (setup() args, config files, command line and plugins)
|
jpayne@69
|
998
|
jpayne@69
|
999 super().run_command(command)
|
jpayne@69
|
1000
|
jpayne@69
|
1001
|
jpayne@69
|
1002 class DistDeprecationWarning(SetuptoolsDeprecationWarning):
|
jpayne@69
|
1003 """Class for warning about deprecations in dist in
|
jpayne@69
|
1004 setuptools. Not ignored by default, unlike DeprecationWarning."""
|