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/_apply_pyprojecttoml.py
4799 views
1
"""Translation layer between pyproject config and setuptools distribution and
2
metadata objects.
3
4
The distribution and metadata objects are modeled after (an old version of)
5
core metadata, therefore configs in the format specified for ``pyproject.toml``
6
need to be processed before being applied.
7
"""
8
import logging
9
import os
10
import warnings
11
from collections.abc import Mapping
12
from email.headerregistry import Address
13
from functools import partial, reduce
14
from itertools import chain
15
from types import MappingProxyType
16
from typing import (TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple,
17
Type, Union)
18
19
from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
20
21
if TYPE_CHECKING:
22
from setuptools._importlib import metadata # noqa
23
from setuptools.dist import Distribution # noqa
24
25
EMPTY: Mapping = MappingProxyType({}) # Immutable dict-like
26
_Path = Union[os.PathLike, str]
27
_DictOrStr = Union[dict, str]
28
_CorrespFn = Callable[["Distribution", Any, _Path], None]
29
_Correspondence = Union[str, _CorrespFn]
30
31
_logger = logging.getLogger(__name__)
32
33
34
def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution":
35
"""Apply configuration dict read with :func:`read_configuration`"""
36
37
if not config:
38
return dist # short-circuit unrelated pyproject.toml file
39
40
root_dir = os.path.dirname(filename) or "."
41
42
_apply_project_table(dist, config, root_dir)
43
_apply_tool_table(dist, config, filename)
44
45
current_directory = os.getcwd()
46
os.chdir(root_dir)
47
try:
48
dist._finalize_requires()
49
dist._finalize_license_files()
50
finally:
51
os.chdir(current_directory)
52
53
return dist
54
55
56
def _apply_project_table(dist: "Distribution", config: dict, root_dir: _Path):
57
project_table = config.get("project", {}).copy()
58
if not project_table:
59
return # short-circuit
60
61
_handle_missing_dynamic(dist, project_table)
62
_unify_entry_points(project_table)
63
64
for field, value in project_table.items():
65
norm_key = json_compatible_key(field)
66
corresp = PYPROJECT_CORRESPONDENCE.get(norm_key, norm_key)
67
if callable(corresp):
68
corresp(dist, value, root_dir)
69
else:
70
_set_config(dist, corresp, value)
71
72
73
def _apply_tool_table(dist: "Distribution", config: dict, filename: _Path):
74
tool_table = config.get("tool", {}).get("setuptools", {})
75
if not tool_table:
76
return # short-circuit
77
78
for field, value in tool_table.items():
79
norm_key = json_compatible_key(field)
80
81
if norm_key in TOOL_TABLE_DEPRECATIONS:
82
suggestion = TOOL_TABLE_DEPRECATIONS[norm_key]
83
msg = f"The parameter `{norm_key}` is deprecated, {suggestion}"
84
warnings.warn(msg, SetuptoolsDeprecationWarning)
85
86
norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key)
87
_set_config(dist, norm_key, value)
88
89
_copy_command_options(config, dist, filename)
90
91
92
def _handle_missing_dynamic(dist: "Distribution", project_table: dict):
93
"""Be temporarily forgiving with ``dynamic`` fields not listed in ``dynamic``"""
94
# TODO: Set fields back to `None` once the feature stabilizes
95
dynamic = set(project_table.get("dynamic", []))
96
for field, getter in _PREVIOUSLY_DEFINED.items():
97
if not (field in project_table or field in dynamic):
98
value = getter(dist)
99
if value:
100
msg = _WouldIgnoreField.message(field, value)
101
warnings.warn(msg, _WouldIgnoreField)
102
103
104
def json_compatible_key(key: str) -> str:
105
"""As defined in :pep:`566#json-compatible-metadata`"""
106
return key.lower().replace("-", "_")
107
108
109
def _set_config(dist: "Distribution", field: str, value: Any):
110
setter = getattr(dist.metadata, f"set_{field}", None)
111
if setter:
112
setter(value)
113
elif hasattr(dist.metadata, field) or field in SETUPTOOLS_PATCHES:
114
setattr(dist.metadata, field, value)
115
else:
116
setattr(dist, field, value)
117
118
119
_CONTENT_TYPES = {
120
".md": "text/markdown",
121
".rst": "text/x-rst",
122
".txt": "text/plain",
123
}
124
125
126
def _guess_content_type(file: str) -> Optional[str]:
127
_, ext = os.path.splitext(file.lower())
128
if not ext:
129
return None
130
131
if ext in _CONTENT_TYPES:
132
return _CONTENT_TYPES[ext]
133
134
valid = ", ".join(f"{k} ({v})" for k, v in _CONTENT_TYPES.items())
135
msg = f"only the following file extensions are recognized: {valid}."
136
raise ValueError(f"Undefined content type for {file}, {msg}")
137
138
139
def _long_description(dist: "Distribution", val: _DictOrStr, root_dir: _Path):
140
from setuptools.config import expand
141
142
if isinstance(val, str):
143
text = expand.read_files(val, root_dir)
144
ctype = _guess_content_type(val)
145
else:
146
text = val.get("text") or expand.read_files(val.get("file", []), root_dir)
147
ctype = val["content-type"]
148
149
_set_config(dist, "long_description", text)
150
if ctype:
151
_set_config(dist, "long_description_content_type", ctype)
152
153
154
def _license(dist: "Distribution", val: dict, root_dir: _Path):
155
from setuptools.config import expand
156
157
if "file" in val:
158
_set_config(dist, "license", expand.read_files([val["file"]], root_dir))
159
else:
160
_set_config(dist, "license", val["text"])
161
162
163
def _people(dist: "Distribution", val: List[dict], _root_dir: _Path, kind: str):
164
field = []
165
email_field = []
166
for person in val:
167
if "name" not in person:
168
email_field.append(person["email"])
169
elif "email" not in person:
170
field.append(person["name"])
171
else:
172
addr = Address(display_name=person["name"], addr_spec=person["email"])
173
email_field.append(str(addr))
174
175
if field:
176
_set_config(dist, kind, ", ".join(field))
177
if email_field:
178
_set_config(dist, f"{kind}_email", ", ".join(email_field))
179
180
181
def _project_urls(dist: "Distribution", val: dict, _root_dir):
182
_set_config(dist, "project_urls", val)
183
184
185
def _python_requires(dist: "Distribution", val: dict, _root_dir):
186
from setuptools.extern.packaging.specifiers import SpecifierSet
187
188
_set_config(dist, "python_requires", SpecifierSet(val))
189
190
191
def _dependencies(dist: "Distribution", val: list, _root_dir):
192
if getattr(dist, "install_requires", []):
193
msg = "`install_requires` overwritten in `pyproject.toml` (dependencies)"
194
warnings.warn(msg)
195
_set_config(dist, "install_requires", val)
196
197
198
def _optional_dependencies(dist: "Distribution", val: dict, _root_dir):
199
existing = getattr(dist, "extras_require", {})
200
_set_config(dist, "extras_require", {**existing, **val})
201
202
203
def _unify_entry_points(project_table: dict):
204
project = project_table
205
entry_points = project.pop("entry-points", project.pop("entry_points", {}))
206
renaming = {"scripts": "console_scripts", "gui_scripts": "gui_scripts"}
207
for key, value in list(project.items()): # eager to allow modifications
208
norm_key = json_compatible_key(key)
209
if norm_key in renaming and value:
210
entry_points[renaming[norm_key]] = project.pop(key)
211
212
if entry_points:
213
project["entry-points"] = {
214
name: [f"{k} = {v}" for k, v in group.items()]
215
for name, group in entry_points.items()
216
}
217
218
219
def _copy_command_options(pyproject: dict, dist: "Distribution", filename: _Path):
220
tool_table = pyproject.get("tool", {})
221
cmdclass = tool_table.get("setuptools", {}).get("cmdclass", {})
222
valid_options = _valid_command_options(cmdclass)
223
224
cmd_opts = dist.command_options
225
for cmd, config in pyproject.get("tool", {}).get("distutils", {}).items():
226
cmd = json_compatible_key(cmd)
227
valid = valid_options.get(cmd, set())
228
cmd_opts.setdefault(cmd, {})
229
for key, value in config.items():
230
key = json_compatible_key(key)
231
cmd_opts[cmd][key] = (str(filename), value)
232
if key not in valid:
233
# To avoid removing options that are specified dynamically we
234
# just log a warn...
235
_logger.warning(f"Command option {cmd}.{key} is not defined")
236
237
238
def _valid_command_options(cmdclass: Mapping = EMPTY) -> Dict[str, Set[str]]:
239
from .._importlib import metadata
240
from setuptools.dist import Distribution
241
242
valid_options = {"global": _normalise_cmd_options(Distribution.global_options)}
243
244
unloaded_entry_points = metadata.entry_points(group='distutils.commands')
245
loaded_entry_points = (_load_ep(ep) for ep in unloaded_entry_points)
246
entry_points = (ep for ep in loaded_entry_points if ep)
247
for cmd, cmd_class in chain(entry_points, cmdclass.items()):
248
opts = valid_options.get(cmd, set())
249
opts = opts | _normalise_cmd_options(getattr(cmd_class, "user_options", []))
250
valid_options[cmd] = opts
251
252
return valid_options
253
254
255
def _load_ep(ep: "metadata.EntryPoint") -> Optional[Tuple[str, Type]]:
256
# Ignore all the errors
257
try:
258
return (ep.name, ep.load())
259
except Exception as ex:
260
msg = f"{ex.__class__.__name__} while trying to load entry-point {ep.name}"
261
_logger.warning(f"{msg}: {ex}")
262
return None
263
264
265
def _normalise_cmd_option_key(name: str) -> str:
266
return json_compatible_key(name).strip("_=")
267
268
269
def _normalise_cmd_options(desc: List[Tuple[str, Optional[str], str]]) -> Set[str]:
270
return {_normalise_cmd_option_key(fancy_option[0]) for fancy_option in desc}
271
272
273
def _attrgetter(attr):
274
"""
275
Similar to ``operator.attrgetter`` but returns None if ``attr`` is not found
276
>>> from types import SimpleNamespace
277
>>> obj = SimpleNamespace(a=42, b=SimpleNamespace(c=13))
278
>>> _attrgetter("a")(obj)
279
42
280
>>> _attrgetter("b.c")(obj)
281
13
282
>>> _attrgetter("d")(obj) is None
283
True
284
"""
285
return partial(reduce, lambda acc, x: getattr(acc, x, None), attr.split("."))
286
287
288
def _some_attrgetter(*items):
289
"""
290
Return the first "truth-y" attribute or None
291
>>> from types import SimpleNamespace
292
>>> obj = SimpleNamespace(a=42, b=SimpleNamespace(c=13))
293
>>> _some_attrgetter("d", "a", "b.c")(obj)
294
42
295
>>> _some_attrgetter("d", "e", "b.c", "a")(obj)
296
13
297
>>> _some_attrgetter("d", "e", "f")(obj) is None
298
True
299
"""
300
def _acessor(obj):
301
values = (_attrgetter(i)(obj) for i in items)
302
return next((i for i in values if i is not None), None)
303
return _acessor
304
305
306
PYPROJECT_CORRESPONDENCE: Dict[str, _Correspondence] = {
307
"readme": _long_description,
308
"license": _license,
309
"authors": partial(_people, kind="author"),
310
"maintainers": partial(_people, kind="maintainer"),
311
"urls": _project_urls,
312
"dependencies": _dependencies,
313
"optional_dependencies": _optional_dependencies,
314
"requires_python": _python_requires,
315
}
316
317
TOOL_TABLE_RENAMES = {"script_files": "scripts"}
318
TOOL_TABLE_DEPRECATIONS = {
319
"namespace_packages": "consider using implicit namespaces instead (PEP 420)."
320
}
321
322
SETUPTOOLS_PATCHES = {"long_description_content_type", "project_urls",
323
"provides_extras", "license_file", "license_files"}
324
325
_PREVIOUSLY_DEFINED = {
326
"name": _attrgetter("metadata.name"),
327
"version": _attrgetter("metadata.version"),
328
"description": _attrgetter("metadata.description"),
329
"readme": _attrgetter("metadata.long_description"),
330
"requires-python": _some_attrgetter("python_requires", "metadata.python_requires"),
331
"license": _attrgetter("metadata.license"),
332
"authors": _some_attrgetter("metadata.author", "metadata.author_email"),
333
"maintainers": _some_attrgetter("metadata.maintainer", "metadata.maintainer_email"),
334
"keywords": _attrgetter("metadata.keywords"),
335
"classifiers": _attrgetter("metadata.classifiers"),
336
"urls": _attrgetter("metadata.project_urls"),
337
"entry-points": _attrgetter("entry_points"),
338
"dependencies": _some_attrgetter("_orig_install_requires", "install_requires"),
339
"optional-dependencies": _some_attrgetter("_orig_extras_require", "extras_require"),
340
}
341
342
343
class _WouldIgnoreField(UserWarning):
344
"""Inform users that ``pyproject.toml`` would overwrite previous metadata."""
345
346
MESSAGE = """\
347
{field!r} defined outside of `pyproject.toml` would be ignored.
348
!!\n\n
349
##########################################################################
350
# configuration would be ignored/result in error due to `pyproject.toml` #
351
##########################################################################
352
353
The following seems to be defined outside of `pyproject.toml`:
354
355
`{field} = {value!r}`
356
357
According to the spec (see the link bellow), however, setuptools CANNOT
358
consider this value unless {field!r} is listed as `dynamic`.
359
360
https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
361
362
For the time being, `setuptools` will still consider the given value (as a
363
**transitional** measure), but please note that future releases of setuptools will
364
follow strictly the standard.
365
366
To prevent this warning, you can list {field!r} under `dynamic` or alternatively
367
remove the `[project]` table from your file and rely entirely on other means of
368
configuration.
369
\n\n!!
370
"""
371
372
@classmethod
373
def message(cls, field, value):
374
from inspect import cleandoc
375
return cleandoc(cls.MESSAGE.format(field=field, value=value))
376
377