__init__.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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. from __future__ import annotations
  8. import functools
  9. import os
  10. import sys
  11. from abc import abstractmethod
  12. from collections.abc import Mapping
  13. from typing import TYPE_CHECKING, TypeVar, overload
  14. sys.path.extend(((vendor_path := os.path.join(os.path.dirname(os.path.dirname(__file__)), 'setuptools', '_vendor')) not in sys.path) * [vendor_path]) # fmt: skip
  15. # workaround for #4476
  16. sys.modules.pop('backports', None)
  17. import _distutils_hack.override # noqa: F401
  18. from . import logging, monkey
  19. from .depends import Require
  20. from .discovery import PackageFinder, PEP420PackageFinder
  21. from .dist import Distribution
  22. from .extension import Extension
  23. from .version import __version__ as __version__
  24. from .warnings import SetuptoolsDeprecationWarning
  25. import distutils.core
  26. __all__ = [
  27. 'setup',
  28. 'Distribution',
  29. 'Command',
  30. 'Extension',
  31. 'Require',
  32. 'SetuptoolsDeprecationWarning',
  33. 'find_packages',
  34. 'find_namespace_packages',
  35. ]
  36. _CommandT = TypeVar("_CommandT", bound="_Command")
  37. bootstrap_install_from = None
  38. find_packages = PackageFinder.find
  39. find_namespace_packages = PEP420PackageFinder.find
  40. def _install_setup_requires(attrs):
  41. # Note: do not use `setuptools.Distribution` directly, as
  42. # our PEP 517 backend patch `distutils.core.Distribution`.
  43. class MinimalDistribution(distutils.core.Distribution):
  44. """
  45. A minimal version of a distribution for supporting the
  46. fetch_build_eggs interface.
  47. """
  48. def __init__(self, attrs: Mapping[str, object]) -> None:
  49. _incl = 'dependency_links', 'setup_requires'
  50. filtered = {k: attrs[k] for k in set(_incl) & set(attrs)}
  51. super().__init__(filtered)
  52. # Prevent accidentally triggering discovery with incomplete set of attrs
  53. self.set_defaults._disable()
  54. def _get_project_config_files(self, filenames=None):
  55. """Ignore ``pyproject.toml``, they are not related to setup_requires"""
  56. try:
  57. cfg, _toml = super()._split_standard_project_metadata(filenames)
  58. except Exception:
  59. return filenames, ()
  60. return cfg, ()
  61. def finalize_options(self):
  62. """
  63. Disable finalize_options to avoid building the working set.
  64. Ref #2158.
  65. """
  66. dist = MinimalDistribution(attrs)
  67. # Honor setup.cfg's options.
  68. dist.parse_config_files(ignore_option_errors=True)
  69. if dist.setup_requires:
  70. _fetch_build_eggs(dist)
  71. def _fetch_build_eggs(dist: Distribution):
  72. try:
  73. dist.fetch_build_eggs(dist.setup_requires)
  74. except Exception as ex:
  75. msg = """
  76. It is possible a package already installed in your system
  77. contains an version that is invalid according to PEP 440.
  78. You can try `pip install --use-pep517` as a workaround for this problem,
  79. or rely on a new virtual environment.
  80. If the problem refers to a package that is not installed yet,
  81. please contact that package's maintainers or distributors.
  82. """
  83. if "InvalidVersion" in ex.__class__.__name__:
  84. if hasattr(ex, "add_note"):
  85. ex.add_note(msg) # PEP 678
  86. else:
  87. dist.announce(f"\n{msg}\n")
  88. raise
  89. def setup(**attrs) -> Distribution:
  90. logging.configure()
  91. # Make sure we have any requirements needed to interpret 'attrs'.
  92. _install_setup_requires(attrs)
  93. # Override return type of distutils.core.Distribution with setuptools.dist.Distribution
  94. # (implicitly implemented via `setuptools.monkey.patch_all`).
  95. return distutils.core.setup(**attrs) # type: ignore[return-value]
  96. setup.__doc__ = distutils.core.setup.__doc__
  97. if TYPE_CHECKING:
  98. # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962
  99. from distutils.core import Command as _Command
  100. else:
  101. _Command = monkey.get_unpatched(distutils.core.Command)
  102. class Command(_Command):
  103. """
  104. Setuptools internal actions are organized using a *command design pattern*.
  105. This means that each action (or group of closely related actions) executed during
  106. the build should be implemented as a ``Command`` subclass.
  107. These commands are abstractions and do not necessarily correspond to a command that
  108. can (or should) be executed via a terminal, in a CLI fashion (although historically
  109. they would).
  110. When creating a new command from scratch, custom defined classes **SHOULD** inherit
  111. from ``setuptools.Command`` and implement a few mandatory methods.
  112. Between these mandatory methods, are listed:
  113. :meth:`initialize_options`, :meth:`finalize_options` and :meth:`run`.
  114. A useful analogy for command classes is to think of them as subroutines with local
  115. variables called "options". The options are "declared" in :meth:`initialize_options`
  116. and "defined" (given their final values, aka "finalized") in :meth:`finalize_options`,
  117. both of which must be defined by every command class. The "body" of the subroutine,
  118. (where it does all the work) is the :meth:`run` method.
  119. Between :meth:`initialize_options` and :meth:`finalize_options`, ``setuptools`` may set
  120. the values for options/attributes based on user's input (or circumstance),
  121. which means that the implementation should be careful to not overwrite values in
  122. :meth:`finalize_options` unless necessary.
  123. Please note that other commands (or other parts of setuptools) may also overwrite
  124. the values of the command's options/attributes multiple times during the build
  125. process.
  126. Therefore it is important to consistently implement :meth:`initialize_options` and
  127. :meth:`finalize_options`. For example, all derived attributes (or attributes that
  128. depend on the value of other attributes) **SHOULD** be recomputed in
  129. :meth:`finalize_options`.
  130. When overwriting existing commands, custom defined classes **MUST** abide by the
  131. same APIs implemented by the original class. They also **SHOULD** inherit from the
  132. original class.
  133. """
  134. command_consumes_arguments = False
  135. distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
  136. dry_run = False # type: ignore[assignment] # pyright: ignore[reportAssignmentType] (until #4689; see #4872)
  137. """
  138. For compatibility with vendored bdist_wheel.
  139. https://github.com/pypa/setuptools/pull/4872/files#r1986395142
  140. """
  141. def __init__(self, dist: Distribution, **kw) -> None:
  142. """
  143. Construct the command for dist, updating
  144. vars(self) with any keyword parameters.
  145. """
  146. super().__init__(dist)
  147. vars(self).update(kw)
  148. @overload
  149. def reinitialize_command(
  150. self, command: str, reinit_subcommands: bool = False, **kw
  151. ) -> Command: ... # override distutils.cmd.Command with setuptools.Command
  152. @overload
  153. def reinitialize_command(
  154. self, command: _CommandT, reinit_subcommands: bool = False, **kw
  155. ) -> _CommandT: ...
  156. def reinitialize_command(
  157. self, command: str | _Command, reinit_subcommands: bool = False, **kw
  158. ) -> Command | _Command:
  159. cmd = _Command.reinitialize_command(self, command, reinit_subcommands)
  160. vars(cmd).update(kw)
  161. return cmd # pyright: ignore[reportReturnType] # pypa/distutils#307
  162. @abstractmethod
  163. def initialize_options(self) -> None:
  164. """
  165. Set or (reset) all options/attributes/caches used by the command
  166. to their default values. Note that these values may be overwritten during
  167. the build.
  168. """
  169. raise NotImplementedError
  170. @abstractmethod
  171. def finalize_options(self) -> None:
  172. """
  173. Set final values for all options/attributes used by the command.
  174. Most of the time, each option/attribute/cache should only be set if it does not
  175. have any value yet (e.g. ``if self.attr is None: self.attr = val``).
  176. """
  177. raise NotImplementedError
  178. @abstractmethod
  179. def run(self) -> None:
  180. """
  181. Execute the actions intended by the command.
  182. (Side effects **SHOULD** only take place when :meth:`run` is executed,
  183. for example, creating new files or writing to the terminal output).
  184. """
  185. raise NotImplementedError
  186. def _find_all_simple(path):
  187. """
  188. Find all files under 'path'
  189. """
  190. results = (
  191. os.path.join(base, file)
  192. for base, dirs, files in os.walk(path, followlinks=True)
  193. for file in files
  194. )
  195. return filter(os.path.isfile, results)
  196. def findall(dir=os.curdir):
  197. """
  198. Find all files under 'dir' and return the list of full filenames.
  199. Unless dir is '.', return full filenames with dir prepended.
  200. """
  201. files = _find_all_simple(dir)
  202. if dir == os.curdir:
  203. make_rel = functools.partial(os.path.relpath, start=dir)
  204. files = map(make_rel, files)
  205. return list(files)
  206. class sic(str):
  207. """Treat this string as-is (https://en.wikipedia.org/wiki/Sic)"""
  208. # Apply monkey patches
  209. monkey.patch_all()