Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
keewenaw
GitHub Repository: keewenaw/ethereum-wallet-cracker
Path: blob/main/test/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py
4799 views
1
"""Load setuptools configuration from ``pyproject.toml`` files"""
2
import logging
3
import os
4
import warnings
5
from contextlib import contextmanager
6
from functools import partial
7
from typing import TYPE_CHECKING, Callable, Dict, Optional, Mapping, Union
8
9
from setuptools.errors import FileError, OptionError
10
11
from . import expand as _expand
12
from ._apply_pyprojecttoml import apply as _apply
13
from ._apply_pyprojecttoml import _PREVIOUSLY_DEFINED, _WouldIgnoreField
14
15
if TYPE_CHECKING:
16
from setuptools.dist import Distribution # noqa
17
18
_Path = Union[str, os.PathLike]
19
_logger = logging.getLogger(__name__)
20
21
22
def load_file(filepath: _Path) -> dict:
23
from setuptools.extern import tomli # type: ignore
24
25
with open(filepath, "rb") as file:
26
return tomli.load(file)
27
28
29
def validate(config: dict, filepath: _Path) -> bool:
30
from . import _validate_pyproject as validator
31
32
trove_classifier = validator.FORMAT_FUNCTIONS.get("trove-classifier")
33
if hasattr(trove_classifier, "_disable_download"):
34
# Improve reproducibility by default. See issue 31 for validate-pyproject.
35
trove_classifier._disable_download() # type: ignore
36
37
try:
38
return validator.validate(config)
39
except validator.ValidationError as ex:
40
_logger.error(f"configuration error: {ex.summary}") # type: ignore
41
_logger.debug(ex.details) # type: ignore
42
error = ValueError(f"invalid pyproject.toml config: {ex.name}") # type: ignore
43
raise error from None
44
45
46
def apply_configuration(
47
dist: "Distribution",
48
filepath: _Path,
49
ignore_option_errors=False,
50
) -> "Distribution":
51
"""Apply the configuration from a ``pyproject.toml`` file into an existing
52
distribution object.
53
"""
54
config = read_configuration(filepath, True, ignore_option_errors, dist)
55
return _apply(dist, config, filepath)
56
57
58
def read_configuration(
59
filepath: _Path,
60
expand=True,
61
ignore_option_errors=False,
62
dist: Optional["Distribution"] = None,
63
):
64
"""Read given configuration file and returns options from it as a dict.
65
66
:param str|unicode filepath: Path to configuration file in the ``pyproject.toml``
67
format.
68
69
:param bool expand: Whether to expand directives and other computed values
70
(i.e. post-process the given configuration)
71
72
:param bool ignore_option_errors: Whether to silently ignore
73
options, values of which could not be resolved (e.g. due to exceptions
74
in directives such as file:, attr:, etc.).
75
If False exceptions are propagated as expected.
76
77
:param Distribution|None: Distribution object to which the configuration refers.
78
If not given a dummy object will be created and discarded after the
79
configuration is read. This is used for auto-discovery of packages in the case
80
a dynamic configuration (e.g. ``attr`` or ``cmdclass``) is expanded.
81
When ``expand=False`` this object is simply ignored.
82
83
:rtype: dict
84
"""
85
filepath = os.path.abspath(filepath)
86
87
if not os.path.isfile(filepath):
88
raise FileError(f"Configuration file {filepath!r} does not exist.")
89
90
asdict = load_file(filepath) or {}
91
project_table = asdict.get("project", {})
92
tool_table = asdict.get("tool", {})
93
setuptools_table = tool_table.get("setuptools", {})
94
if not asdict or not (project_table or setuptools_table):
95
return {} # User is not using pyproject to configure setuptools
96
97
# TODO: Remove the following once the feature stabilizes:
98
msg = (
99
"Support for project metadata in `pyproject.toml` is still experimental "
100
"and may be removed (or change) in future releases."
101
)
102
warnings.warn(msg, _ExperimentalProjectMetadata)
103
104
# There is an overall sense in the community that making include_package_data=True
105
# the default would be an improvement.
106
# `ini2toml` backfills include_package_data=False when nothing is explicitly given,
107
# therefore setting a default here is backwards compatible.
108
orig_setuptools_table = setuptools_table.copy()
109
if dist and getattr(dist, "include_package_data") is not None:
110
setuptools_table.setdefault("include-package-data", dist.include_package_data)
111
else:
112
setuptools_table.setdefault("include-package-data", True)
113
# Persist changes:
114
asdict["tool"] = tool_table
115
tool_table["setuptools"] = setuptools_table
116
117
try:
118
# Don't complain about unrelated errors (e.g. tools not using the "tool" table)
119
subset = {"project": project_table, "tool": {"setuptools": setuptools_table}}
120
validate(subset, filepath)
121
except Exception as ex:
122
# TODO: Remove the following once the feature stabilizes:
123
if _skip_bad_config(project_table, orig_setuptools_table, dist):
124
return {}
125
# TODO: After the previous statement is removed the try/except can be replaced
126
# by the _ignore_errors context manager.
127
if ignore_option_errors:
128
_logger.debug(f"ignored error: {ex.__class__.__name__} - {ex}")
129
else:
130
raise # re-raise exception
131
132
if expand:
133
root_dir = os.path.dirname(filepath)
134
return expand_configuration(asdict, root_dir, ignore_option_errors, dist)
135
136
return asdict
137
138
139
def _skip_bad_config(
140
project_cfg: dict, setuptools_cfg: dict, dist: Optional["Distribution"]
141
) -> bool:
142
"""Be temporarily forgiving with invalid ``pyproject.toml``"""
143
# See pypa/setuptools#3199 and pypa/cibuildwheel#1064
144
145
if dist is None or (
146
dist.metadata.name is None
147
and dist.metadata.version is None
148
and dist.install_requires is None
149
):
150
# It seems that the build is not getting any configuration from other places
151
return False
152
153
if setuptools_cfg:
154
# If `[tool.setuptools]` is set, then `pyproject.toml` config is intentional
155
return False
156
157
given_config = set(project_cfg.keys())
158
popular_subset = {"name", "version", "python_requires", "requires-python"}
159
if given_config <= popular_subset:
160
# It seems that the docs in cibuildtool has been inadvertently encouraging users
161
# to create `pyproject.toml` files that are not compliant with the standards.
162
# Let's be forgiving for the time being.
163
warnings.warn(_InvalidFile.message(), _InvalidFile, stacklevel=2)
164
return True
165
166
return False
167
168
169
def expand_configuration(
170
config: dict,
171
root_dir: Optional[_Path] = None,
172
ignore_option_errors: bool = False,
173
dist: Optional["Distribution"] = None,
174
) -> dict:
175
"""Given a configuration with unresolved fields (e.g. dynamic, cmdclass, ...)
176
find their final values.
177
178
:param dict config: Dict containing the configuration for the distribution
179
:param str root_dir: Top-level directory for the distribution/project
180
(the same directory where ``pyproject.toml`` is place)
181
:param bool ignore_option_errors: see :func:`read_configuration`
182
:param Distribution|None: Distribution object to which the configuration refers.
183
If not given a dummy object will be created and discarded after the
184
configuration is read. Used in the case a dynamic configuration
185
(e.g. ``attr`` or ``cmdclass``).
186
187
:rtype: dict
188
"""
189
return _ConfigExpander(config, root_dir, ignore_option_errors, dist).expand()
190
191
192
class _ConfigExpander:
193
def __init__(
194
self,
195
config: dict,
196
root_dir: Optional[_Path] = None,
197
ignore_option_errors: bool = False,
198
dist: Optional["Distribution"] = None,
199
):
200
self.config = config
201
self.root_dir = root_dir or os.getcwd()
202
self.project_cfg = config.get("project", {})
203
self.dynamic = self.project_cfg.get("dynamic", [])
204
self.setuptools_cfg = config.get("tool", {}).get("setuptools", {})
205
self.dynamic_cfg = self.setuptools_cfg.get("dynamic", {})
206
self.ignore_option_errors = ignore_option_errors
207
self._dist = dist
208
209
def _ensure_dist(self) -> "Distribution":
210
from setuptools.dist import Distribution
211
212
attrs = {"src_root": self.root_dir, "name": self.project_cfg.get("name", None)}
213
return self._dist or Distribution(attrs)
214
215
def _process_field(self, container: dict, field: str, fn: Callable):
216
if field in container:
217
with _ignore_errors(self.ignore_option_errors):
218
container[field] = fn(container[field])
219
220
def _canonic_package_data(self, field="package-data"):
221
package_data = self.setuptools_cfg.get(field, {})
222
return _expand.canonic_package_data(package_data)
223
224
def expand(self):
225
self._expand_packages()
226
self._canonic_package_data()
227
self._canonic_package_data("exclude-package-data")
228
229
# A distribution object is required for discovering the correct package_dir
230
dist = self._ensure_dist()
231
232
with _EnsurePackagesDiscovered(dist, self.setuptools_cfg) as ensure_discovered:
233
package_dir = ensure_discovered.package_dir
234
self._expand_data_files()
235
self._expand_cmdclass(package_dir)
236
self._expand_all_dynamic(dist, package_dir)
237
238
return self.config
239
240
def _expand_packages(self):
241
packages = self.setuptools_cfg.get("packages")
242
if packages is None or isinstance(packages, (list, tuple)):
243
return
244
245
find = packages.get("find")
246
if isinstance(find, dict):
247
find["root_dir"] = self.root_dir
248
find["fill_package_dir"] = self.setuptools_cfg.setdefault("package-dir", {})
249
with _ignore_errors(self.ignore_option_errors):
250
self.setuptools_cfg["packages"] = _expand.find_packages(**find)
251
252
def _expand_data_files(self):
253
data_files = partial(_expand.canonic_data_files, root_dir=self.root_dir)
254
self._process_field(self.setuptools_cfg, "data-files", data_files)
255
256
def _expand_cmdclass(self, package_dir: Mapping[str, str]):
257
root_dir = self.root_dir
258
cmdclass = partial(_expand.cmdclass, package_dir=package_dir, root_dir=root_dir)
259
self._process_field(self.setuptools_cfg, "cmdclass", cmdclass)
260
261
def _expand_all_dynamic(self, dist: "Distribution", package_dir: Mapping[str, str]):
262
special = ( # need special handling
263
"version",
264
"readme",
265
"entry-points",
266
"scripts",
267
"gui-scripts",
268
"classifiers",
269
)
270
# `_obtain` functions are assumed to raise appropriate exceptions/warnings.
271
obtained_dynamic = {
272
field: self._obtain(dist, field, package_dir)
273
for field in self.dynamic
274
if field not in special
275
}
276
obtained_dynamic.update(
277
self._obtain_entry_points(dist, package_dir) or {},
278
version=self._obtain_version(dist, package_dir),
279
readme=self._obtain_readme(dist),
280
classifiers=self._obtain_classifiers(dist),
281
)
282
# `None` indicates there is nothing in `tool.setuptools.dynamic` but the value
283
# might have already been set by setup.py/extensions, so avoid overwriting.
284
updates = {k: v for k, v in obtained_dynamic.items() if v is not None}
285
self.project_cfg.update(updates)
286
287
def _ensure_previously_set(self, dist: "Distribution", field: str):
288
previous = _PREVIOUSLY_DEFINED[field](dist)
289
if previous is None and not self.ignore_option_errors:
290
msg = (
291
f"No configuration found for dynamic {field!r}.\n"
292
"Some dynamic fields need to be specified via `tool.setuptools.dynamic`"
293
"\nothers must be specified via the equivalent attribute in `setup.py`."
294
)
295
raise OptionError(msg)
296
297
def _obtain(self, dist: "Distribution", field: str, package_dir: Mapping[str, str]):
298
if field in self.dynamic_cfg:
299
directive = self.dynamic_cfg[field]
300
with _ignore_errors(self.ignore_option_errors):
301
root_dir = self.root_dir
302
if "file" in directive:
303
return _expand.read_files(directive["file"], root_dir)
304
if "attr" in directive:
305
return _expand.read_attr(directive["attr"], package_dir, root_dir)
306
msg = f"invalid `tool.setuptools.dynamic.{field}`: {directive!r}"
307
raise ValueError(msg)
308
return None
309
self._ensure_previously_set(dist, field)
310
return None
311
312
def _obtain_version(self, dist: "Distribution", package_dir: Mapping[str, str]):
313
# Since plugins can set version, let's silently skip if it cannot be obtained
314
if "version" in self.dynamic and "version" in self.dynamic_cfg:
315
return _expand.version(self._obtain(dist, "version", package_dir))
316
return None
317
318
def _obtain_readme(self, dist: "Distribution") -> Optional[Dict[str, str]]:
319
if "readme" not in self.dynamic:
320
return None
321
322
dynamic_cfg = self.dynamic_cfg
323
if "readme" in dynamic_cfg:
324
return {
325
"text": self._obtain(dist, "readme", {}),
326
"content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"),
327
}
328
329
self._ensure_previously_set(dist, "readme")
330
return None
331
332
def _obtain_entry_points(
333
self, dist: "Distribution", package_dir: Mapping[str, str]
334
) -> Optional[Dict[str, dict]]:
335
fields = ("entry-points", "scripts", "gui-scripts")
336
if not any(field in self.dynamic for field in fields):
337
return None
338
339
text = self._obtain(dist, "entry-points", package_dir)
340
if text is None:
341
return None
342
343
groups = _expand.entry_points(text)
344
expanded = {"entry-points": groups}
345
346
def _set_scripts(field: str, group: str):
347
if group in groups:
348
value = groups.pop(group)
349
if field not in self.dynamic:
350
msg = _WouldIgnoreField.message(field, value)
351
warnings.warn(msg, _WouldIgnoreField)
352
# TODO: Don't set field when support for pyproject.toml stabilizes
353
# instead raise an error as specified in PEP 621
354
expanded[field] = value
355
356
_set_scripts("scripts", "console_scripts")
357
_set_scripts("gui-scripts", "gui_scripts")
358
359
return expanded
360
361
def _obtain_classifiers(self, dist: "Distribution"):
362
if "classifiers" in self.dynamic:
363
value = self._obtain(dist, "classifiers", {})
364
if value:
365
return value.splitlines()
366
return None
367
368
369
@contextmanager
370
def _ignore_errors(ignore_option_errors: bool):
371
if not ignore_option_errors:
372
yield
373
return
374
375
try:
376
yield
377
except Exception as ex:
378
_logger.debug(f"ignored error: {ex.__class__.__name__} - {ex}")
379
380
381
class _EnsurePackagesDiscovered(_expand.EnsurePackagesDiscovered):
382
def __init__(self, distribution: "Distribution", setuptools_cfg: dict):
383
super().__init__(distribution)
384
self._setuptools_cfg = setuptools_cfg
385
386
def __enter__(self):
387
"""When entering the context, the values of ``packages``, ``py_modules`` and
388
``package_dir`` that are missing in ``dist`` are copied from ``setuptools_cfg``.
389
"""
390
dist, cfg = self._dist, self._setuptools_cfg
391
package_dir: Dict[str, str] = cfg.setdefault("package-dir", {})
392
package_dir.update(dist.package_dir or {})
393
dist.package_dir = package_dir # needs to be the same object
394
395
dist.set_defaults._ignore_ext_modules() # pyproject.toml-specific behaviour
396
397
# Set `py_modules` and `packages` in dist to short-circuit auto-discovery,
398
# but avoid overwriting empty lists purposefully set by users.
399
if dist.py_modules is None:
400
dist.py_modules = cfg.get("py-modules")
401
if dist.packages is None:
402
dist.packages = cfg.get("packages")
403
404
return super().__enter__()
405
406
def __exit__(self, exc_type, exc_value, traceback):
407
"""When exiting the context, if values of ``packages``, ``py_modules`` and
408
``package_dir`` are missing in ``setuptools_cfg``, copy from ``dist``.
409
"""
410
# If anything was discovered set them back, so they count in the final config.
411
self._setuptools_cfg.setdefault("packages", self._dist.packages)
412
self._setuptools_cfg.setdefault("py-modules", self._dist.py_modules)
413
return super().__exit__(exc_type, exc_value, traceback)
414
415
416
class _ExperimentalProjectMetadata(UserWarning):
417
"""Explicitly inform users that `pyproject.toml` configuration is experimental"""
418
419
420
class _InvalidFile(UserWarning):
421
"""The given `pyproject.toml` file is invalid and would be ignored.
422
!!\n\n
423
############################
424
# Invalid `pyproject.toml` #
425
############################
426
427
Any configurations in `pyproject.toml` will be ignored.
428
Please note that future releases of setuptools will halt the build process
429
if an invalid file is given.
430
431
To prevent setuptools from considering `pyproject.toml` please
432
DO NOT include the `[project]` or `[tool.setuptools]` tables in your file.
433
\n\n!!
434
"""
435
436
@classmethod
437
def message(cls):
438
from inspect import cleandoc
439
return cleandoc(cls.__doc__)
440
441