1"""Extensions to the 'distutils' for large or complex distributions"""
2# mypy: disable_error_code=override
3# Command.reinitialize_command has an extra **kw param that distutils doesn't have
4# Can't disable on the exact line because distutils doesn't exists on Python 3.12
5# and mypy isn't aware of distutils_hack, causing distutils.core.Command to be Any,
6# and a [unused-ignore] to be raised on 3.12+
7
8from __future__ import annotations
9
10import functools
11import os
12import re
13import sys
14from abc import abstractmethod
15from collections.abc import Mapping
16from typing import TYPE_CHECKING, TypeVar, overload
17
18sys.path.extend(((vendor_path := os.path.join(os.path.dirname(os.path.dirname(__file__)), 'setuptools', '_vendor')) not in sys.path) * [vendor_path]) # fmt: skip
19# workaround for #4476
20sys.modules.pop('backports', None)
21
22import _distutils_hack.override # noqa: F401
23
24from . import logging, monkey
25from .depends import Require
26from .discovery import PackageFinder, PEP420PackageFinder
27from .dist import Distribution
28from .extension import Extension
29from .version import __version__ as __version__
30from .warnings import SetuptoolsDeprecationWarning
31
32import distutils.core
33from distutils.errors import DistutilsOptionError
34
35__all__ = [
36 'setup',
37 'Distribution',
38 'Command',
39 'Extension',
40 'Require',
41 'SetuptoolsDeprecationWarning',
42 'find_packages',
43 'find_namespace_packages',
44]
45
46_CommandT = TypeVar("_CommandT", bound="_Command")
47
48bootstrap_install_from = None
49
50find_packages = PackageFinder.find
51find_namespace_packages = PEP420PackageFinder.find
52
53
54def _install_setup_requires(attrs):
55 # Note: do not use `setuptools.Distribution` directly, as
56 # our PEP 517 backend patch `distutils.core.Distribution`.
57 class MinimalDistribution(distutils.core.Distribution):
58 """
59 A minimal version of a distribution for supporting the
60 fetch_build_eggs interface.
61 """
62
63 def __init__(self, attrs: Mapping[str, object]):
64 _incl = 'dependency_links', 'setup_requires'
65 filtered = {k: attrs[k] for k in set(_incl) & set(attrs)}
66 super().__init__(filtered)
67 # Prevent accidentally triggering discovery with incomplete set of attrs
68 self.set_defaults._disable()
69
70 def _get_project_config_files(self, filenames=None):
71 """Ignore ``pyproject.toml``, they are not related to setup_requires"""
72 try:
73 cfg, toml = super()._split_standard_project_metadata(filenames)
74 except Exception:
75 return filenames, ()
76 return cfg, ()
77
78 def finalize_options(self):
79 """
80 Disable finalize_options to avoid building the working set.
81 Ref #2158.
82 """
83
84 dist = MinimalDistribution(attrs)
85
86 # Honor setup.cfg's options.
87 dist.parse_config_files(ignore_option_errors=True)
88 if dist.setup_requires:
89 _fetch_build_eggs(dist)
90
91
92def _fetch_build_eggs(dist):
93 try:
94 dist.fetch_build_eggs(dist.setup_requires)
95 except Exception as ex:
96 msg = """
97 It is possible a package already installed in your system
98 contains an version that is invalid according to PEP 440.
99 You can try `pip install --use-pep517` as a workaround for this problem,
100 or rely on a new virtual environment.
101
102 If the problem refers to a package that is not installed yet,
103 please contact that package's maintainers or distributors.
104 """
105 if "InvalidVersion" in ex.__class__.__name__:
106 if hasattr(ex, "add_note"):
107 ex.add_note(msg) # PEP 678
108 else:
109 dist.announce(f"\n{msg}\n")
110 raise
111
112
113def setup(**attrs):
114 logging.configure()
115 # Make sure we have any requirements needed to interpret 'attrs'.
116 _install_setup_requires(attrs)
117 return distutils.core.setup(**attrs)
118
119
120setup.__doc__ = distutils.core.setup.__doc__
121
122if TYPE_CHECKING:
123 from typing_extensions import TypeAlias
124
125 # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962
126 _Command: TypeAlias = distutils.core.Command
127else:
128 _Command = monkey.get_unpatched(distutils.core.Command)
129
130
131class Command(_Command):
132 """
133 Setuptools internal actions are organized using a *command design pattern*.
134 This means that each action (or group of closely related actions) executed during
135 the build should be implemented as a ``Command`` subclass.
136
137 These commands are abstractions and do not necessarily correspond to a command that
138 can (or should) be executed via a terminal, in a CLI fashion (although historically
139 they would).
140
141 When creating a new command from scratch, custom defined classes **SHOULD** inherit
142 from ``setuptools.Command`` and implement a few mandatory methods.
143 Between these mandatory methods, are listed:
144 :meth:`initialize_options`, :meth:`finalize_options` and :meth:`run`.
145
146 A useful analogy for command classes is to think of them as subroutines with local
147 variables called "options". The options are "declared" in :meth:`initialize_options`
148 and "defined" (given their final values, aka "finalized") in :meth:`finalize_options`,
149 both of which must be defined by every command class. The "body" of the subroutine,
150 (where it does all the work) is the :meth:`run` method.
151 Between :meth:`initialize_options` and :meth:`finalize_options`, ``setuptools`` may set
152 the values for options/attributes based on user's input (or circumstance),
153 which means that the implementation should be careful to not overwrite values in
154 :meth:`finalize_options` unless necessary.
155
156 Please note that other commands (or other parts of setuptools) may also overwrite
157 the values of the command's options/attributes multiple times during the build
158 process.
159 Therefore it is important to consistently implement :meth:`initialize_options` and
160 :meth:`finalize_options`. For example, all derived attributes (or attributes that
161 depend on the value of other attributes) **SHOULD** be recomputed in
162 :meth:`finalize_options`.
163
164 When overwriting existing commands, custom defined classes **MUST** abide by the
165 same APIs implemented by the original class. They also **SHOULD** inherit from the
166 original class.
167 """
168
169 command_consumes_arguments = False
170 distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
171
172 def __init__(self, dist: Distribution, **kw):
173 """
174 Construct the command for dist, updating
175 vars(self) with any keyword parameters.
176 """
177 super().__init__(dist)
178 vars(self).update(kw)
179
180 def _ensure_stringlike(self, option, what, default=None):
181 val = getattr(self, option)
182 if val is None:
183 setattr(self, option, default)
184 return default
185 elif not isinstance(val, str):
186 raise DistutilsOptionError(
187 "'%s' must be a %s (got `%s`)" % (option, what, val)
188 )
189 return val
190
191 def ensure_string_list(self, option):
192 r"""Ensure that 'option' is a list of strings. If 'option' is
193 currently a string, we split it either on /,\s*/ or /\s+/, so
194 "foo bar baz", "foo,bar,baz", and "foo, bar baz" all become
195 ["foo", "bar", "baz"].
196
197 ..
198 TODO: This method seems to be similar to the one in ``distutils.cmd``
199 Probably it is just here for backward compatibility with old Python versions?
200
201 :meta private:
202 """
203 val = getattr(self, option)
204 if val is None:
205 return
206 elif isinstance(val, str):
207 setattr(self, option, re.split(r',\s*|\s+', val))
208 else:
209 if isinstance(val, list):
210 ok = all(isinstance(v, str) for v in val)
211 else:
212 ok = False
213 if not ok:
214 raise DistutilsOptionError(
215 "'%s' must be a list of strings (got %r)" % (option, val)
216 )
217
218 @overload
219 def reinitialize_command(
220 self, command: str, reinit_subcommands: bool = False, **kw
221 ) -> _Command: ...
222 @overload
223 def reinitialize_command(
224 self, command: _CommandT, reinit_subcommands: bool = False, **kw
225 ) -> _CommandT: ...
226 def reinitialize_command(
227 self, command: str | _Command, reinit_subcommands: bool = False, **kw
228 ) -> _Command:
229 cmd = _Command.reinitialize_command(self, command, reinit_subcommands)
230 vars(cmd).update(kw)
231 return cmd
232
233 @abstractmethod
234 def initialize_options(self) -> None:
235 """
236 Set or (reset) all options/attributes/caches used by the command
237 to their default values. Note that these values may be overwritten during
238 the build.
239 """
240 raise NotImplementedError
241
242 @abstractmethod
243 def finalize_options(self) -> None:
244 """
245 Set final values for all options/attributes used by the command.
246 Most of the time, each option/attribute/cache should only be set if it does not
247 have any value yet (e.g. ``if self.attr is None: self.attr = val``).
248 """
249 raise NotImplementedError
250
251 @abstractmethod
252 def run(self) -> None:
253 """
254 Execute the actions intended by the command.
255 (Side effects **SHOULD** only take place when :meth:`run` is executed,
256 for example, creating new files or writing to the terminal output).
257 """
258 raise NotImplementedError
259
260
261def _find_all_simple(path):
262 """
263 Find all files under 'path'
264 """
265 results = (
266 os.path.join(base, file)
267 for base, dirs, files in os.walk(path, followlinks=True)
268 for file in files
269 )
270 return filter(os.path.isfile, results)
271
272
273def findall(dir=os.curdir):
274 """
275 Find all files under 'dir' and return the list of full filenames.
276 Unless dir is '.', return full filenames with dir prepended.
277 """
278 files = _find_all_simple(dir)
279 if dir == os.curdir:
280 make_rel = functools.partial(os.path.relpath, start=dir)
281 files = map(make_rel, files)
282 return list(files)
283
284
285class sic(str):
286 """Treat this string as-is (https://en.wikipedia.org/wiki/Sic)"""
287
288
289# Apply monkey patches
290monkey.patch_all()