]>
crepu.dev Git - config.git/blob - djavu-asus/emacs/elpy/rpc-venv/lib/python3.11/site-packages/setuptools/config/expand.py
c8db2c4b4993cb010fdad537055671fdd1880a87
1 """Utility functions to expand configuration directives or special values
4 We can split the process of interpreting configuration files into 2 steps:
6 1. The parsing the file contents from strings to value objects
7 that can be understand by Python (for example a string with a comma
8 separated list of keywords into an actual Python list of strings).
10 2. The expansion (or post-processing) of these values according to the
11 semantics ``setuptools`` assign to them (for example a configuration field
12 with the ``file:`` directive should be expanded from a list of file paths to
13 a single string with the contents of those files concatenated)
15 This module focus on the second step, and therefore allow sharing the expansion
16 functions among several configuration file formats.
18 **PRIVATE MODULE**: API reserved for setuptools internal usage only.
27 from glob
import iglob
28 from configparser
import ConfigParser
29 from importlib
.machinery
import ModuleSpec
30 from itertools
import chain
45 from pathlib
import Path
46 from types
import ModuleType
48 from distutils
.errors
import DistutilsOptionError
50 from .._path
import same_path
as _same_path
53 from setuptools
.dist
import Distribution
# noqa
54 from setuptools
.discovery
import ConfigDiscovery
# noqa
55 from distutils
.dist
import DistributionMetadata
# noqa
57 chain_iter
= chain
.from_iterable
58 _Path
= Union
[str, os
.PathLike
]
60 _V
= TypeVar("_V", covariant
=True)
64 """Proxy to a module object that avoids executing arbitrary code."""
66 def __init__(self
, name
: str, spec
: ModuleSpec
):
67 module
= ast
.parse(pathlib
.Path(spec
.origin
).read_bytes())
68 vars(self
).update(locals())
71 def _find_assignments(self
) -> Iterator
[Tuple
[ast
.AST
, ast
.AST
]]:
72 for statement
in self
.module
.body
:
73 if isinstance(statement
, ast
.Assign
):
74 yield from ((target
, statement
.value
) for target
in statement
.targets
)
75 elif isinstance(statement
, ast
.AnnAssign
) and statement
.value
:
76 yield (statement
.target
, statement
.value
)
78 def __getattr__(self
, attr
):
79 """Attempt to load an attribute "statically", via :func:`ast.literal_eval`."""
82 ast
.literal_eval(value
)
83 for target
, value
in self
._find
_assignments
()
84 if isinstance(target
, ast
.Name
) and target
.id == attr
86 except Exception as e
:
87 raise AttributeError(f
"{self.name} has no attribute {attr}") from e
91 patterns
: Iterable
[str], root_dir
: Optional
[_Path
] = None
93 """Expand the list of glob patterns, but preserving relative paths.
95 :param list[str] patterns: List of glob patterns
96 :param str root_dir: Path to which globs should be relative
97 (current directory by default)
100 glob_characters
= {'*', '?', '[', ']', '{', '}'}
102 root_dir
= root_dir
or os
.getcwd()
103 for value
in patterns
:
105 # Has globby characters?
106 if any(char
in value
for char
in glob_characters
):
107 # then expand the glob pattern while keeping paths *relative*:
108 glob_path
= os
.path
.abspath(os
.path
.join(root_dir
, value
))
109 expanded_values
.extend(sorted(
110 os
.path
.relpath(path
, root_dir
).replace(os
.sep
, "/")
111 for path
in iglob(glob_path
, recursive
=True)))
114 # take the value as-is
115 path
= os
.path
.relpath(value
, root_dir
).replace(os
.sep
, "/")
116 expanded_values
.append(path
)
118 return expanded_values
121 def read_files(filepaths
: Union
[str, bytes
, Iterable
[_Path
]], root_dir
=None) -> str:
122 """Return the content of the files concatenated using ``\n`` as str
124 This function is sandboxed and won't reach anything outside ``root_dir``
126 (By default ``root_dir`` is the current directory).
128 from setuptools
.extern
.more_itertools
import always_iterable
130 root_dir
= os
.path
.abspath(root_dir
or os
.getcwd())
131 _filepaths
= (os
.path
.join(root_dir
, path
) for path
in always_iterable(filepaths
))
134 for path
in _filter_existing_files(_filepaths
)
135 if _assert_local(path
, root_dir
)
139 def _filter_existing_files(filepaths
: Iterable
[_Path
]) -> Iterator
[_Path
]:
140 for path
in filepaths
:
141 if os
.path
.isfile(path
):
144 warnings
.warn(f
"File {path!r} cannot be found")
147 def _read_file(filepath
: Union
[bytes
, _Path
]) -> str:
148 with io
.open(filepath
, encoding
='utf-8') as f
:
152 def _assert_local(filepath
: _Path
, root_dir
: str):
153 if Path(os
.path
.abspath(root_dir
)) not in Path(os
.path
.abspath(filepath
)).parents
:
154 msg
= f
"Cannot access {filepath!r} (or anything outside {root_dir!r})"
155 raise DistutilsOptionError(msg
)
162 package_dir
: Optional
[Mapping
[str, str]] = None,
163 root_dir
: Optional
[_Path
] = None
165 """Reads the value of an attribute from a module.
167 This function will try to read the attributed statically first
168 (via :func:`ast.literal_eval`), and only evaluate the module if it fails.
171 read_attr("package.attr")
172 read_attr("package.module.attr")
174 :param str attr_desc: Dot-separated string describing how to reach the
175 attribute (see examples above)
176 :param dict[str, str] package_dir: Mapping of package names to their
177 location in disk (represented by paths relative to ``root_dir``).
178 :param str root_dir: Path to directory containing all the packages in
179 ``package_dir`` (current directory by default).
182 root_dir
= root_dir
or os
.getcwd()
183 attrs_path
= attr_desc
.strip().split('.')
184 attr_name
= attrs_path
.pop()
185 module_name
= '.'.join(attrs_path
)
186 module_name
= module_name
or '__init__'
187 _parent_path
, path
, module_name
= _find_module(module_name
, package_dir
, root_dir
)
188 spec
= _find_spec(module_name
, path
)
191 return getattr(StaticModule(module_name
, spec
), attr_name
)
193 # fallback to evaluate module
194 module
= _load_spec(spec
, module_name
)
195 return getattr(module
, attr_name
)
198 def _find_spec(module_name
: str, module_path
: Optional
[_Path
]) -> ModuleSpec
:
199 spec
= importlib
.util
.spec_from_file_location(module_name
, module_path
)
200 spec
= spec
or importlib
.util
.find_spec(module_name
)
203 raise ModuleNotFoundError(module_name
)
208 def _load_spec(spec
: ModuleSpec
, module_name
: str) -> ModuleType
:
209 name
= getattr(spec
, "__name__", module_name
)
210 if name
in sys
.modules
:
211 return sys
.modules
[name
]
212 module
= importlib
.util
.module_from_spec(spec
)
213 sys
.modules
[name
] = module
# cache (it also ensures `==` works on loaded items)
214 spec
.loader
.exec_module(module
) # type: ignore
219 module_name
: str, package_dir
: Optional
[Mapping
[str, str]], root_dir
: _Path
220 ) -> Tuple
[_Path
, Optional
[str], str]:
221 """Given a module (that could normally be imported by ``module_name``
222 after the build is complete), find the path to the parent directory where
223 it is contained and the canonical name that could be used to import it
224 considering the ``package_dir`` in the build configuration and ``root_dir``
226 parent_path
= root_dir
227 module_parts
= module_name
.split('.')
229 if module_parts
[0] in package_dir
:
230 # A custom path was specified for the module we want to import
231 custom_path
= package_dir
[module_parts
[0]]
232 parts
= custom_path
.rsplit('/', 1)
234 parent_path
= os
.path
.join(root_dir
, parts
[0])
235 parent_module
= parts
[1]
237 parent_module
= custom_path
238 module_name
= ".".join([parent_module
, *module_parts
[1:]])
239 elif '' in package_dir
:
240 # A custom parent directory was specified for all root modules
241 parent_path
= os
.path
.join(root_dir
, package_dir
[''])
243 path_start
= os
.path
.join(parent_path
, *module_name
.split("."))
245 (f
"{path_start}.py", os
.path
.join(path_start
, "__init__.py")),
246 iglob(f
"{path_start}.*")
248 module_path
= next((x
for x
in candidates
if os
.path
.isfile(x
)), None)
249 return parent_path
, module_path
, module_name
253 qualified_class_name
: str,
254 package_dir
: Optional
[Mapping
[str, str]] = None,
255 root_dir
: Optional
[_Path
] = None
257 """Given a qualified class name, return the associated class object"""
258 root_dir
= root_dir
or os
.getcwd()
259 idx
= qualified_class_name
.rfind('.')
260 class_name
= qualified_class_name
[idx
+ 1 :]
261 pkg_name
= qualified_class_name
[:idx
]
263 _parent_path
, path
, module_name
= _find_module(pkg_name
, package_dir
, root_dir
)
264 module
= _load_spec(_find_spec(module_name
, path
), module_name
)
265 return getattr(module
, class_name
)
269 values
: Dict
[str, str],
270 package_dir
: Optional
[Mapping
[str, str]] = None,
271 root_dir
: Optional
[_Path
] = None
272 ) -> Dict
[str, Callable
]:
273 """Given a dictionary mapping command names to strings for qualified class
274 names, apply :func:`resolve_class` to the dict values.
276 return {k
: resolve_class(v
, package_dir
, root_dir
) for k
, v
in values
.items()}
282 fill_package_dir
: Optional
[Dict
[str, str]] = None,
283 root_dir
: Optional
[_Path
] = None,
286 """Works similarly to :func:`setuptools.find_packages`, but with all
287 arguments given as keyword arguments. Moreover, ``where`` can be given
288 as a list (the results will be simply concatenated).
290 When the additional keyword argument ``namespaces`` is ``True``, it will
291 behave like :func:`setuptools.find_namespace_packages`` (i.e. include
292 implicit namespaces as per :pep:`420`).
294 The ``where`` argument will be considered relative to ``root_dir`` (or the current
295 working directory when ``root_dir`` is not given).
297 If the ``fill_package_dir`` argument is passed, this function will consider it as a
298 similar data structure to the ``package_dir`` configuration parameter add fill-in
299 any missing package location.
303 from setuptools
.discovery
import construct_package_dir
304 from setuptools
.extern
.more_itertools
import unique_everseen
, always_iterable
307 from setuptools
.discovery
import PEP420PackageFinder
as PackageFinder
309 from setuptools
.discovery
import PackageFinder
# type: ignore
311 root_dir
= root_dir
or os
.curdir
312 where
= kwargs
.pop('where', ['.'])
313 packages
: List
[str] = []
314 fill_package_dir
= {} if fill_package_dir
is None else fill_package_dir
315 search
= list(unique_everseen(always_iterable(where
)))
317 if len(search
) == 1 and all(not _same_path(search
[0], x
) for x
in (".", root_dir
)):
318 fill_package_dir
.setdefault("", search
[0])
321 package_path
= _nest_path(root_dir
, path
)
322 pkgs
= PackageFinder
.find(package_path
, **kwargs
)
323 packages
.extend(pkgs
)
325 fill_package_dir
.get("") == path
326 or os
.path
.samefile(package_path
, root_dir
)
328 fill_package_dir
.update(construct_package_dir(pkgs
, path
))
333 def _nest_path(parent
: _Path
, path
: _Path
) -> str:
334 path
= parent
if path
in {".", ""} else os
.path
.join(parent
, path
)
335 return os
.path
.normpath(path
)
338 def version(value
: Union
[Callable
, Iterable
[Union
[str, int]], str]) -> str:
339 """When getting the version directly from an attribute,
340 it should be normalised to string.
345 value
= cast(Iterable
[Union
[str, int]], value
)
347 if not isinstance(value
, str):
348 if hasattr(value
, '__iter__'):
349 value
= '.'.join(map(str, value
))
356 def canonic_package_data(package_data
: dict) -> dict:
357 if "*" in package_data
:
358 package_data
[""] = package_data
.pop("*")
362 def canonic_data_files(
363 data_files
: Union
[list, dict], root_dir
: Optional
[_Path
] = None
364 ) -> List
[Tuple
[str, List
[str]]]:
365 """For compatibility with ``setup.py``, ``data_files`` should be a list
366 of pairs instead of a dict.
368 This function also expands glob patterns.
370 if isinstance(data_files
, list):
374 (dest
, glob_relative(patterns
, root_dir
))
375 for dest
, patterns
in data_files
.items()
379 def entry_points(text
: str, text_source
="entry-points") -> Dict
[str, dict]:
380 """Given the contents of entry-points file,
381 process it into a 2-level dictionary (``dict[str, dict[str, str]]``).
382 The first level keys are entry-point groups, the second level keys are
383 entry-point names, and the second level values are references to objects
384 (that correspond to the entry-point value).
386 parser
= ConfigParser(default_section
=None, delimiters
=("=",)) # type: ignore
387 parser
.optionxform
= str # case sensitive
388 parser
.read_string(text
, text_source
)
389 groups
= {k
: dict(v
.items()) for k
, v
in parser
.items()}
390 groups
.pop(parser
.default_section
, None)
394 class EnsurePackagesDiscovered
:
395 """Some expand functions require all the packages to already be discovered before
396 they run, e.g. :func:`read_attr`, :func:`resolve_class`, :func:`cmdclass`.
398 Therefore in some cases we will need to run autodiscovery during the evaluation of
399 the configuration. However, it is better to postpone calling package discovery as
400 much as possible, because some parameters can influence it (e.g. ``package_dir``),
401 and those might not have been processed yet.
404 def __init__(self
, distribution
: "Distribution"):
405 self
._dist
= distribution
409 """Trigger the automatic package discovery, if it is still necessary."""
412 self
._dist
.set_defaults(name
=False) # Skip name, we can still be parsing
417 def __exit__(self
, _exc_type
, _exc_value
, _traceback
):
419 self
._dist
.set_defaults
.analyse_name() # Now we can set a default name
421 def _get_package_dir(self
) -> Mapping
[str, str]:
423 pkg_dir
= self
._dist
.package_dir
424 return {} if pkg_dir
is None else pkg_dir
427 def package_dir(self
) -> Mapping
[str, str]:
428 """Proxy to ``package_dir`` that may trigger auto-discovery when used."""
429 return LazyMappingProxy(self
._get
_package
_dir
)
432 class LazyMappingProxy(Mapping
[_K
, _V
]):
433 """Mapping proxy that delays resolving the target object, until really needed.
435 >>> def obtain_mapping():
436 ... print("Running expensive function!")
437 ... return {"key": "value", "other key": "other value"}
438 >>> mapping = LazyMappingProxy(obtain_mapping)
440 Running expensive function!
442 >>> mapping["other key"]
446 def __init__(self
, obtain_mapping_value
: Callable
[[], Mapping
[_K
, _V
]]):
447 self
._obtain
= obtain_mapping_value
448 self
._value
: Optional
[Mapping
[_K
, _V
]] = None
450 def _target(self
) -> Mapping
[_K
, _V
]:
451 if self
._value
is None:
452 self
._value
= self
._obtain
()
455 def __getitem__(self
, key
: _K
) -> _V
:
456 return self
._target
()[key
]
458 def __len__(self
) -> int:
459 return len(self
._target
())
461 def __iter__(self
) -> Iterator
[_K
]:
462 return iter(self
._target
())