dependence 0.3.6__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dependence/utilities.py DELETED
@@ -1,1034 +0,0 @@
1
- import functools
2
- import os
3
- import re
4
- import sys
5
- from collections import deque
6
- from configparser import ConfigParser, SectionProxy
7
- from enum import Enum, auto
8
- from glob import iglob
9
- from importlib.metadata import Distribution, PackageNotFoundError
10
- from importlib.metadata import distribution as _get_distribution
11
- from importlib.metadata import distributions as _get_distributions
12
- from itertools import chain
13
- from pathlib import Path
14
- from runpy import run_path
15
- from shutil import rmtree
16
- from subprocess import CalledProcessError, list2cmdline
17
- from types import ModuleType
18
- from typing import (
19
- IO,
20
- AbstractSet,
21
- Any,
22
- Container,
23
- Dict,
24
- Iterable,
25
- List,
26
- MutableSet,
27
- Optional,
28
- Tuple,
29
- Union,
30
- cast,
31
- )
32
- from warnings import warn
33
-
34
- import tomli
35
- from packaging.requirements import InvalidRequirement, Requirement
36
- from packaging.utils import canonicalize_name
37
-
38
- from ._utilities import (
39
- append_exception_text,
40
- check_output,
41
- deprecated,
42
- get_exception_text,
43
- iter_distinct,
44
- )
45
-
46
- _BUILTIN_DISTRIBUTION_NAMES: Tuple[str] = ("distribute",)
47
-
48
-
49
- _UNSAFE_CHARACTERS_PATTERN: re.Pattern = re.compile("[^A-Za-z0-9.]+")
50
-
51
-
52
- def normalize_name(name: str) -> str:
53
- """
54
- Normalize a project/distribution name
55
- """
56
- return _UNSAFE_CHARACTERS_PATTERN.sub("-", canonicalize_name(name)).lower()
57
-
58
-
59
- class ConfigurationFileType(Enum):
60
- REQUIREMENTS_TXT = auto()
61
- SETUP_CFG = auto()
62
- TOX_INI = auto()
63
- PYPROJECT_TOML = auto()
64
-
65
-
66
- @functools.lru_cache()
67
- def get_configuration_file_type(path: str) -> ConfigurationFileType:
68
- if not os.path.isfile(path):
69
- raise FileNotFoundError(path)
70
- basename: str = os.path.basename(path).lower()
71
- if basename == "setup.cfg":
72
- return ConfigurationFileType.SETUP_CFG
73
- elif basename == "tox.ini":
74
- return ConfigurationFileType.TOX_INI
75
- elif basename == "pyproject.toml":
76
- return ConfigurationFileType.PYPROJECT_TOML
77
- elif basename.endswith(".txt"):
78
- return ConfigurationFileType.REQUIREMENTS_TXT
79
- else:
80
- raise ValueError(
81
- f"{path} is not a recognized type of configuration file."
82
- )
83
-
84
-
85
- def is_configuration_file(path: str) -> bool:
86
- try:
87
- get_configuration_file_type(path)
88
- except (FileNotFoundError, ValueError):
89
- return False
90
- return True
91
-
92
-
93
- def _get_editable_finder_location(path_name: str) -> str:
94
- key: str
95
- value: Any
96
- init_globals: Dict[str, Any]
97
- try:
98
- init_globals = run_path(path_name)
99
- except Exception:
100
- return ""
101
- for key, value in init_globals.items():
102
- if key.startswith("__editable__"):
103
- finder: ModuleType = value
104
- module_name: str
105
- module_location: str
106
- for module_name, module_location in getattr(
107
- finder, "MAPPING", {}
108
- ).items():
109
- path: Path = Path(module_location)
110
- index: int
111
- for index in range(len(module_name.split("."))):
112
- path = path.parent
113
- while path != path.parent:
114
- if (
115
- path.joinpath("setup.py").is_file()
116
- or path.joinpath("setup.cfg").is_file()
117
- or path.joinpath("pyproject.toml").is_file()
118
- ):
119
- return str(path)
120
- path = path.parent
121
- return ""
122
-
123
-
124
- def _iter_path_editable_distribution_locations(
125
- directory: str,
126
- ) -> Iterable[Tuple[str, str]]:
127
- directory_path: Path = Path(directory)
128
- file_path: Path
129
- for file_path in chain(
130
- directory_path.glob("*.egg-link"),
131
- directory_path.glob("__editable__.*.pth"),
132
- ):
133
- name: str
134
- if file_path.name.endswith(".egg-link"):
135
- name = file_path.name[:-9]
136
- else:
137
- name = file_path.name[13:-4].partition("-")[0]
138
- name = normalize_name(name)
139
- with open(file_path) as file_io:
140
- location: str = file_io.read().strip().partition("\n")[0]
141
- if os.path.exists(location):
142
- yield name, location
143
- else:
144
- location = _get_editable_finder_location(str(file_path))
145
- if location:
146
- yield name, location
147
-
148
-
149
- def _iter_editable_distribution_locations() -> Iterable[Tuple[str, str]]:
150
- yield from chain(
151
- *map(_iter_path_editable_distribution_locations, sys.path)
152
- )
153
-
154
-
155
- @functools.lru_cache()
156
- def get_editable_distributions_locations() -> Dict[str, str]:
157
- """
158
- Get a mapping of (normalized) editable distribution names to their
159
- locations.
160
- """
161
- return dict(_iter_editable_distribution_locations())
162
-
163
-
164
- def cache_clear() -> None:
165
- """
166
- Clear distribution metadata caches
167
- """
168
- get_installed_distributions.cache_clear()
169
- get_editable_distributions_locations.cache_clear()
170
- is_editable.cache_clear()
171
- is_installed.cache_clear()
172
- get_requirement_string_distribution_name.cache_clear()
173
-
174
-
175
- def refresh_editable_distributions() -> None:
176
- """
177
- Update distribution information for editable installs
178
- """
179
- name: str
180
- location: str
181
- for name, location in get_editable_distributions_locations().items():
182
- _install_requirement_string(location, name=name, editable=True)
183
-
184
-
185
- @functools.lru_cache()
186
- def get_installed_distributions() -> Dict[str, Distribution]:
187
- """
188
- Return a dictionary of installed distributions.
189
- """
190
- refresh_editable_distributions()
191
- installed: Dict[str, Distribution] = {}
192
- for distribution in _get_distributions():
193
- installed[normalize_name(distribution.metadata["Name"])] = distribution
194
- return installed
195
-
196
-
197
- def get_distribution(name: str) -> Distribution:
198
- return get_installed_distributions()[normalize_name(name)]
199
-
200
-
201
- @functools.lru_cache()
202
- def is_installed(distribution_name: str) -> bool:
203
- return normalize_name(distribution_name) in get_installed_distributions()
204
-
205
-
206
- def get_requirement_distribution_name(requirement: Requirement) -> str:
207
- return normalize_name(requirement.name)
208
-
209
-
210
- @functools.lru_cache()
211
- def get_requirement_string_distribution_name(requirement_string: str) -> str:
212
- return get_requirement_distribution_name(
213
- get_requirement(requirement_string)
214
- )
215
-
216
-
217
- @functools.lru_cache()
218
- def is_requirement_string(requirement_string: str) -> bool:
219
- try:
220
- Requirement(requirement_string)
221
- except InvalidRequirement:
222
- return False
223
- return True
224
-
225
-
226
- def _iter_file_requirement_strings(path: str) -> Iterable[str]:
227
- lines: List[str]
228
- requirement_file_io: IO[str]
229
- with open(path) as requirement_file_io:
230
- lines = requirement_file_io.readlines()
231
- return filter(is_requirement_string, lines)
232
-
233
-
234
- def _iter_setup_cfg_requirement_strings(path: str) -> Iterable[str]:
235
- parser: ConfigParser = ConfigParser()
236
- parser.read(path)
237
- requirement_strings: Iterable[str] = ()
238
- if ("options" in parser) and ("install_requires" in parser["options"]):
239
- requirement_strings = chain(
240
- requirement_strings,
241
- filter(
242
- is_requirement_string,
243
- parser["options"]["install_requires"].split("\n"),
244
- ),
245
- )
246
- if "options.extras_require" in parser:
247
- extras_require: SectionProxy = parser["options.extras_require"]
248
- extra_requirements_string: str
249
- for extra_requirements_string in extras_require.values():
250
- requirement_strings = chain(
251
- requirement_strings,
252
- filter(
253
- is_requirement_string,
254
- extra_requirements_string.split("\n"),
255
- ),
256
- )
257
- return iter_distinct(requirement_strings)
258
-
259
-
260
- def _iter_tox_ini_requirement_strings(
261
- path: Union[str, Path, ConfigParser] = "",
262
- string: str = "",
263
- ) -> Iterable[str]:
264
- """
265
- Parse a tox.ini file and yield the requirements found in the `deps`
266
- options of each section.
267
-
268
- Parameters:
269
-
270
- - path (str|Path) = "": The path to a tox.ini file
271
- - string (str) = "": The contents of a tox.ini file
272
- """
273
- parser: ConfigParser = ConfigParser()
274
- if path:
275
- assert (
276
- not string
277
- ), "Either `path` or `string` arguments may be provided, but not both"
278
- parser.read(path)
279
- else:
280
- assert string, "Either a `path` or `string` argument must be provided"
281
- parser.read_string(string)
282
-
283
- def get_section_option_requirements(
284
- section_name: str, option_name: str
285
- ) -> Iterable[str]:
286
- if parser.has_option(section_name, option_name):
287
- return filter(
288
- is_requirement_string,
289
- parser.get(section_name, option_name).split("\n"),
290
- )
291
- return ()
292
-
293
- def get_section_requirements(section_name: str) -> Iterable[str]:
294
- requirements: Iterable[str] = get_section_option_requirements(
295
- section_name, "deps"
296
- )
297
- if section_name == "tox":
298
- requirements = chain(
299
- requirements,
300
- get_section_option_requirements(section_name, "requires"),
301
- )
302
- return requirements
303
-
304
- return iter_distinct(
305
- chain(("tox",), *map(get_section_requirements, parser.sections()))
306
- )
307
-
308
-
309
- def _iter_pyproject_toml_requirement_strings(
310
- path: str,
311
- exclude_build_system: bool = False,
312
- exclude_project: bool = False,
313
- exclude_project_dependencies: bool = False,
314
- exclude_project_optional_dependencies: bool = False,
315
- include_project_optional_dependencies: Iterable[str] = frozenset(),
316
- exclude_tools: bool = False,
317
- exclude_tox: bool = False,
318
- ) -> Iterable[str]:
319
- """
320
- Read a pyproject.toml file and yield the requirements found.
321
-
322
- - exclude_build_system (bool) = False: If `True`, build-system
323
- requirements will not be included
324
- - exclude_project (bool) = False: If `True`, build-system
325
- requirements will not be included
326
- - exclude_project_dependencies (bool) = False: If `True`, project
327
- dependencies will not be included
328
- - exclude_project_optional_dependencies (bool) = False: If `True`, project
329
- optional dependencies will not be included
330
- - include_project_optional_dependencies ({str}) = frozenset(): If a
331
- non-empty set is provided, *only* dependencies for the specified extras
332
- (options) will be included
333
- - exclude_tools (bool) = False: If `True`, tool requirements will not be
334
- included
335
- - exclude_tox (bool) = False: If `True`, tool.tox dependencies will not be
336
- included
337
- """
338
- include_project_optional_dependencies = (
339
- include_project_optional_dependencies
340
- if isinstance(include_project_optional_dependencies, set)
341
- else frozenset(include_project_optional_dependencies)
342
- )
343
- pyproject_io: IO[str]
344
- with open(path) as pyproject_io:
345
- pyproject: Dict[str, Any] = tomli.loads(pyproject_io.read())
346
- # Build system requirements
347
- if (
348
- ("build-system" in pyproject)
349
- and ("requires" in pyproject["build-system"])
350
- and not exclude_build_system
351
- ):
352
- yield from pyproject["build-system"]["requires"]
353
- # Project requirements
354
- if ("project" in pyproject) and not exclude_project:
355
- if (
356
- "dependencies" in pyproject["project"]
357
- ) and not exclude_project_dependencies:
358
- yield from pyproject["project"]["dependencies"]
359
- if (
360
- "optional-dependencies" in pyproject["project"]
361
- ) and not exclude_project_optional_dependencies:
362
- key: str
363
- values: Iterable[str]
364
- for key, values in pyproject["project"][
365
- "optional-dependencies"
366
- ].items():
367
- if (not include_project_optional_dependencies) or (
368
- key in include_project_optional_dependencies
369
- ):
370
- yield from values
371
- # Tool Requirements
372
- if ("tool" in pyproject) and not exclude_tools:
373
- # Tox
374
- if ("tox" in pyproject["tool"]) and not exclude_tox:
375
- if "legacy_tox_ini" in pyproject["tool"]["tox"]:
376
- yield from _iter_tox_ini_requirement_strings(
377
- string=pyproject["tool"]["tox"]["legacy_tox_ini"]
378
- )
379
-
380
-
381
- def iter_configuration_file_requirement_strings(
382
- path: str,
383
- exclude_build_system: bool = False,
384
- exclude_project: bool = False,
385
- exclude_project_dependencies: bool = False,
386
- exclude_project_optional_dependencies: bool = False,
387
- include_project_optional_dependencies: AbstractSet[str] = frozenset(),
388
- exclude_tools: bool = False,
389
- exclude_tox: bool = False,
390
- ) -> Iterable[str]:
391
- """
392
- Read a configuration file and yield the parsed requirements.
393
-
394
- Parameters:
395
-
396
- - path (str): The path to a configuration file
397
-
398
- Parameters only applicable to `pyproject.toml` files:
399
-
400
- - exclude_build_system (bool) = False: If `True`, build-system
401
- requirements will not be included
402
- - exclude_project (bool) = False: If `True`, build-system
403
- requirements will not be included
404
- - exclude_project_dependencies (bool) = False: If `True`, project
405
- dependencies will not be included
406
- - exclude_project_optional_dependencies (bool) = False: If `True`, project
407
- optional dependencies will not be included
408
- - include_project_optional_dependencies ({str}) = frozenset(): If a
409
- non-empty set is provided, *only* dependencies for the specified extras
410
- (options) will be included
411
- - exclude_tools (bool) = False: If `True`, tool requirements will not be
412
- included
413
- - exclude_tox (bool) = False: If `True`, tool.tox dependencies will not be
414
- included
415
- """
416
- configuration_file_type: ConfigurationFileType = (
417
- get_configuration_file_type(path)
418
- )
419
- if configuration_file_type == ConfigurationFileType.SETUP_CFG:
420
- return _iter_setup_cfg_requirement_strings(path)
421
- elif configuration_file_type == ConfigurationFileType.PYPROJECT_TOML:
422
- return _iter_pyproject_toml_requirement_strings(
423
- path,
424
- exclude_build_system=exclude_build_system,
425
- exclude_project=exclude_project,
426
- exclude_project_dependencies=exclude_project_dependencies,
427
- exclude_project_optional_dependencies=(
428
- exclude_project_optional_dependencies
429
- ),
430
- include_project_optional_dependencies=(
431
- include_project_optional_dependencies
432
- ),
433
- exclude_tools=exclude_tools,
434
- exclude_tox=exclude_tox,
435
- )
436
- elif configuration_file_type == ConfigurationFileType.TOX_INI:
437
- return _iter_tox_ini_requirement_strings(path=path)
438
- else:
439
- assert (
440
- configuration_file_type == ConfigurationFileType.REQUIREMENTS_TXT
441
- )
442
- return _iter_file_requirement_strings(path)
443
-
444
-
445
- @functools.lru_cache()
446
- def is_editable(name: str) -> bool:
447
- """
448
- Return `True` if the indicated distribution is an editable installation.
449
- """
450
- return bool(normalize_name(name) in get_editable_distributions_locations())
451
-
452
-
453
- def _get_setup_cfg_metadata(path: str, key: str) -> str:
454
- if os.path.basename(path).lower() != "setup.cfg":
455
- if not os.path.isdir(path):
456
- path = os.path.dirname(path)
457
- path = os.path.join(path, "setup.cfg")
458
- if os.path.isfile(path):
459
- parser: ConfigParser = ConfigParser()
460
- parser.read(path)
461
- if "metadata" in parser:
462
- return parser.get("metadata", key, fallback="")
463
- else:
464
- warn(f"No `metadata` section found in: {path}")
465
- return ""
466
-
467
-
468
- def _get_setup_py_metadata(path: str, args: Tuple[str, ...]) -> str:
469
- """
470
- Execute a setup.py script with `args` and return the response.
471
-
472
- Parameters:
473
-
474
- - path (str)
475
- - args ([str])
476
- """
477
- value: str = ""
478
- current_directory: str = os.path.abspath(os.curdir)
479
- directory: str = path
480
- try:
481
- if os.path.basename(path).lower() == "setup.py":
482
- directory = os.path.dirname(path)
483
- os.chdir(directory)
484
- else:
485
- if not os.path.isdir(path):
486
- directory = os.path.dirname(path)
487
- os.chdir(directory)
488
- path = os.path.join(directory, "setup.py")
489
- if os.path.isfile(path):
490
- command: Tuple[str, ...] = (sys.executable, path) + args
491
- try:
492
- value = check_output(command).strip().split("\n")[-1]
493
- except CalledProcessError:
494
- warn(
495
- f"A package name could not be found in {path}, "
496
- "attempting to refresh egg info"
497
- f"\nError ignored: {get_exception_text()}"
498
- )
499
- # re-write egg info and attempt to get the name again
500
- setup_egg_info(directory)
501
- try:
502
- value = check_output(command).strip().split("\n")[-1]
503
- except Exception:
504
- warn(
505
- f"A package name could not be found in {path}"
506
- f"\nError ignored: {get_exception_text()}"
507
- )
508
- finally:
509
- os.chdir(current_directory)
510
- return value
511
-
512
-
513
- def _get_pyproject_toml_project_metadata(path: str, key: str) -> str:
514
- if os.path.basename(path).lower() != "pyproject.toml":
515
- if not os.path.isdir(path):
516
- path = os.path.dirname(path)
517
- path = os.path.join(path, "pyproject.toml")
518
- if os.path.isfile(path):
519
- pyproject_io: IO[str]
520
- with open(path) as pyproject_io:
521
- pyproject: Dict[str, Any] = tomli.loads(pyproject_io.read())
522
- if "project" in pyproject:
523
- return pyproject["project"].get(key, "")
524
- return ""
525
-
526
-
527
- def get_setup_distribution_name(path: str) -> str:
528
- """
529
- Get a distribution's name from setup.py, setup.cfg or pyproject.toml
530
- """
531
- return normalize_name(
532
- _get_setup_cfg_metadata(path, "name")
533
- or _get_pyproject_toml_project_metadata(path, "name")
534
- or _get_setup_py_metadata(path, ("--name",))
535
- )
536
-
537
-
538
- def get_setup_distribution_version(path: str) -> str:
539
- """
540
- Get a distribution's version from setup.py, setup.cfg or pyproject.toml
541
- """
542
- return (
543
- _get_setup_cfg_metadata(path, "version")
544
- or _get_pyproject_toml_project_metadata(path, "version")
545
- or _get_setup_py_metadata(path, ("--version",))
546
- )
547
-
548
-
549
- def _setup(arguments: Tuple[str, ...]) -> None:
550
- try:
551
- check_output((sys.executable, "setup.py") + arguments)
552
- except CalledProcessError:
553
- warn(f"Ignoring error: {get_exception_text()}")
554
-
555
-
556
- def _setup_location(
557
- location: Union[str, Path], arguments: Iterable[Tuple[str, ...]]
558
- ) -> None:
559
- if isinstance(location, str):
560
- location = Path(location)
561
- # If there is no setup.py file, we can't update egg info
562
- if not location.joinpath("setup.py").is_file():
563
- return
564
- if isinstance(arguments, str):
565
- arguments = (arguments,)
566
- current_directory: Path = Path(os.curdir).absolute()
567
- os.chdir(location)
568
- try:
569
- deque(map(_setup, arguments), maxlen=0)
570
- finally:
571
- os.chdir(current_directory)
572
-
573
-
574
- @deprecated()
575
- def setup_dist_egg_info(directory: str) -> None:
576
- """
577
- Refresh dist-info and egg-info for the editable package installed in
578
- `directory`
579
- """
580
- directory = os.path.abspath(directory)
581
- if not os.path.isdir(directory):
582
- directory = os.path.dirname(directory)
583
- _setup_location(
584
- directory,
585
- (
586
- ("-q", "dist_info"),
587
- ("-q", "egg_info"),
588
- ),
589
- )
590
-
591
-
592
- def get_editable_distribution_location(name: str) -> str:
593
- return get_editable_distributions_locations().get(normalize_name(name), "")
594
-
595
-
596
- @deprecated()
597
- def setup_dist_info(
598
- directory: Union[str, Path], output_dir: Union[str, Path] = ""
599
- ) -> None:
600
- """
601
- Refresh dist-info for the editable package installed in
602
- `directory`
603
- """
604
- if isinstance(directory, str):
605
- directory = Path(directory)
606
- directory = directory.absolute()
607
- if not directory.is_dir():
608
- directory = directory.parent
609
- if isinstance(output_dir, str) and output_dir:
610
- output_dir = Path(output_dir)
611
- _setup_location(
612
- directory,
613
- (
614
- ("-q", "dist_info")
615
- + (("--output-dir", str(output_dir)) if output_dir else ()),
616
- ),
617
- )
618
-
619
-
620
- def setup_egg_info(directory: Union[str, Path], egg_base: str = "") -> None:
621
- """
622
- Refresh egg-info for the editable package installed in
623
- `directory` (only applicable for packages using a `setup.py` script)
624
- """
625
- if isinstance(directory, str):
626
- directory = Path(directory)
627
- directory = directory.absolute()
628
- if not directory.is_dir():
629
- directory = directory.parent
630
- # If there is a setup.py, and a *.dist-info directory, but that
631
- # *.dist-info directory has no RECORD, we need to remove the *.dist-info
632
- # directory
633
- if directory.joinpath("setup.py").is_file():
634
- dist_info: str
635
- for dist_info in iglob(str(directory.joinpath("*.dist-info"))):
636
- dist_info_path: Path = Path(dist_info)
637
- if not dist_info_path.joinpath("RECORD").is_file():
638
- rmtree(dist_info_path)
639
- _setup_location(
640
- directory,
641
- (("-q", "egg_info") + (("--egg-base", egg_base) if egg_base else ()),),
642
- )
643
-
644
-
645
- def get_requirement(
646
- requirement_string: str,
647
- ) -> Requirement:
648
- try:
649
- return Requirement(requirement_string)
650
- except InvalidRequirement:
651
- # Try to parse the requirement as an installation target location,
652
- # such as can be used with `pip install`
653
- location: str = requirement_string
654
- extras: str = ""
655
- if "[" in requirement_string and requirement_string.endswith("]"):
656
- parts: List[str] = requirement_string.split("[")
657
- location = "[".join(parts[:-1])
658
- extras = f"[{parts[-1]}"
659
- location = os.path.abspath(location)
660
- name: str = get_setup_distribution_name(location)
661
- assert name, f"No distribution found in {location}"
662
- return Requirement(f"{name}{extras}")
663
-
664
-
665
- def get_required_distribution_names(
666
- requirement_string: str,
667
- exclude: Iterable[str] = (),
668
- recursive: bool = True,
669
- echo: bool = False,
670
- depth: Optional[int] = None,
671
- ) -> MutableSet[str]:
672
- """
673
- Return a `tuple` of all distribution names which are required by the
674
- distribution specified in `requirement_string`.
675
-
676
- Parameters:
677
-
678
- - requirement_string (str): A distribution name, or a requirement string
679
- indicating both a distribution name and extras.
680
- - exclude ([str]): The name of one or more distributions to *exclude*
681
- from requirements lookup. Please note that excluding a distribution will
682
- also halt recursive lookup of requirements for that distribution.
683
- - recursive (bool): If `True` (the default), required distributions will
684
- be obtained recursively.
685
- - echo (bool) = False: If `True`, commands and responses executed in
686
- subprocesses will be printed to `sys.stdout`
687
- - depth (int|None) = None: The maximum depth of recursion to follow
688
- requirements. If `None` (the default), recursion is not restricted.
689
- """
690
- if isinstance(exclude, str):
691
- exclude = set((normalize_name(exclude),))
692
- else:
693
- exclude = set(map(normalize_name, exclude))
694
- return set(
695
- _iter_requirement_names(
696
- get_requirement(requirement_string),
697
- exclude=exclude,
698
- recursive=recursive,
699
- echo=echo,
700
- depth=depth,
701
- )
702
- )
703
-
704
-
705
- def _get_requirement_name(requirement: Requirement) -> str:
706
- return normalize_name(requirement.name)
707
-
708
-
709
- def install_requirement(
710
- requirement: Union[str, Requirement],
711
- echo: bool = True,
712
- ) -> None:
713
- """
714
- Install a requirement
715
-
716
- Parameters:
717
-
718
- - requirement (str)
719
- - echo (bool) = True: If `True` (default), the `pip install`
720
- commands will be echoed to `sys.stdout`
721
- """
722
- if isinstance(requirement, str):
723
- requirement = Requirement(requirement)
724
- return _install_requirement(requirement)
725
-
726
-
727
- def _install_requirement_string(
728
- requirement_string: str,
729
- name: str = "",
730
- editable: bool = False,
731
- ) -> None:
732
- """
733
- Install a requirement string with no dependencies, compilation, build
734
- isolation, etc.
735
- """
736
- command: Tuple[str, ...] = (
737
- sys.executable,
738
- "-m",
739
- "pip",
740
- "install",
741
- "--no-deps",
742
- "--no-compile",
743
- "--no-build-isolation",
744
- )
745
- if editable:
746
- command += (
747
- "-e",
748
- requirement_string,
749
- )
750
- if sys.version_info < (3, 9):
751
- command += (
752
- "--config-settings",
753
- "editable_mode=compat",
754
- )
755
- else:
756
- command += (requirement_string,)
757
- try:
758
- check_output(command)
759
- except CalledProcessError as error:
760
- message: str = (
761
- (
762
- f"\n{list2cmdline(command)}" f"\nCould not install {name}"
763
- if name == requirement_string
764
- else (
765
- f"\n{list2cmdline(command)}"
766
- f"\nCould not install {name} from "
767
- f"{requirement_string}"
768
- )
769
- )
770
- if name
771
- else (
772
- f"\n{list2cmdline(command)}"
773
- f"\nCould not install {requirement_string}"
774
- )
775
- )
776
- if not editable:
777
- append_exception_text(
778
- error,
779
- message,
780
- )
781
- raise error
782
- try:
783
- check_output(command + ("--force-reinstall",))
784
- except CalledProcessError as retry_error:
785
- append_exception_text(
786
- retry_error,
787
- message,
788
- )
789
- raise retry_error
790
-
791
-
792
- def _install_requirement(
793
- requirement: Requirement,
794
- ) -> None:
795
- requirement_string: str = str(requirement)
796
- # Get the distribution name
797
- distribution: Optional[Distribution] = None
798
- editable_location: str = ""
799
- try:
800
- distribution = _get_distribution(requirement.name)
801
- editable_location = get_editable_distribution_location(
802
- distribution.metadata["Name"]
803
- )
804
- except (PackageNotFoundError, KeyError):
805
- pass
806
- # If the requirement is installed and editable, re-install from
807
- # the editable location
808
- if distribution and editable_location:
809
- # Assemble a requirement specifier for the editable install
810
- requirement_string = editable_location
811
- if requirement.extras:
812
- requirement_string = (
813
- f"{requirement_string}[{','.join(requirement.extras)}]"
814
- )
815
- _install_requirement_string(
816
- requirement_string=requirement_string,
817
- name=normalize_name(requirement.name),
818
- editable=bool(editable_location),
819
- )
820
- # Refresh the metadata
821
- cache_clear()
822
-
823
-
824
- def _get_requirement_distribution(
825
- requirement: Requirement,
826
- name: str,
827
- reinstall: bool = True,
828
- echo: bool = False,
829
- ) -> Optional[Distribution]:
830
- if name in _BUILTIN_DISTRIBUTION_NAMES:
831
- return None
832
- try:
833
- return get_installed_distributions()[name]
834
- except KeyError:
835
- if not reinstall:
836
- raise
837
- if echo:
838
- warn(
839
- f'The required distribution "{name}" was not installed, '
840
- "attempting to install it now..."
841
- )
842
- # Attempt to install the requirement...
843
- install_requirement(requirement, echo=echo)
844
- return _get_requirement_distribution(
845
- requirement, name, reinstall=False, echo=echo
846
- )
847
-
848
-
849
- def _iter_distribution_requirements(
850
- distribution: Distribution,
851
- extras: Tuple[str, ...] = (),
852
- exclude: Container[str] = (),
853
- ) -> Iterable[Requirement]:
854
- if not distribution.requires:
855
- return
856
- requirement: Requirement
857
- for requirement in map(Requirement, distribution.requires):
858
- if (
859
- (requirement.marker is None)
860
- or any(
861
- requirement.marker.evaluate({"extra": extra})
862
- for extra in extras
863
- )
864
- ) and (normalize_name(requirement.name) not in exclude):
865
- yield requirement
866
-
867
-
868
- def _iter_requirement_names(
869
- requirement: Requirement,
870
- exclude: MutableSet[str],
871
- recursive: bool = True,
872
- echo: bool = False,
873
- depth: Optional[int] = None,
874
- ) -> Iterable[str]:
875
- name: str = normalize_name(requirement.name)
876
- extras: Tuple[str, ...] = tuple(requirement.extras)
877
- if name in exclude:
878
- return ()
879
- # Ensure we don't follow the same requirement again, causing cyclic
880
- # recursion
881
- exclude.add(name)
882
- distribution: Optional[Distribution] = _get_requirement_distribution(
883
- requirement, name, echo=echo
884
- )
885
- if distribution is None:
886
- return ()
887
- requirements: Tuple[Requirement, ...] = tuple(
888
- iter_distinct(
889
- _iter_distribution_requirements(
890
- distribution,
891
- extras=extras,
892
- exclude=exclude,
893
- ),
894
- )
895
- )
896
- lateral_exclude: MutableSet[str] = set()
897
-
898
- def iter_requirement_names_(
899
- requirement_: Requirement,
900
- depth_: Optional[int] = None,
901
- ) -> Iterable[str]:
902
- if (depth_ is None) or depth_ >= 0:
903
- yield from _iter_requirement_names(
904
- requirement_,
905
- exclude=cast(
906
- MutableSet[str],
907
- exclude
908
- | (
909
- lateral_exclude
910
- - set((_get_requirement_name(requirement_),))
911
- ),
912
- ),
913
- recursive=recursive,
914
- echo=echo,
915
- depth=None if (depth_ is None) else depth_ - 1,
916
- )
917
-
918
- def not_excluded(name: str) -> bool:
919
- if name not in exclude:
920
- # Add this to the exclusions
921
- lateral_exclude.add(name)
922
- return True
923
- return False
924
-
925
- requirement_names: Iterable[str] = filter(
926
- not_excluded, map(_get_requirement_name, requirements)
927
- )
928
- if recursive:
929
- requirement_: Requirement
930
- requirement_names = chain(
931
- requirement_names,
932
- *map(
933
- lambda requirement_: iter_requirement_names_(
934
- requirement_, None if (depth is None) else depth - 1
935
- ),
936
- requirements,
937
- ),
938
- )
939
- return requirement_names
940
-
941
-
942
- @deprecated()
943
- def _iter_requirement_strings_required_distribution_names(
944
- requirement_strings: Iterable[str],
945
- echo: bool = False,
946
- ) -> Iterable[str]:
947
- visited_requirement_strings: MutableSet[str] = set()
948
- if isinstance(requirement_strings, str):
949
- requirement_strings = (requirement_strings,)
950
-
951
- def get_required_distribution_names_(
952
- requirement_string: str,
953
- ) -> MutableSet[str]:
954
- if requirement_string not in visited_requirement_strings:
955
- try:
956
- name: str = get_requirement_string_distribution_name(
957
- requirement_string
958
- )
959
- visited_requirement_strings.add(requirement_string)
960
- return cast(
961
- MutableSet[str],
962
- set((name,))
963
- | get_required_distribution_names(
964
- requirement_string, echo=echo
965
- ),
966
- )
967
- except KeyError:
968
- pass
969
- return set()
970
-
971
- return iter_distinct(
972
- chain(*map(get_required_distribution_names_, requirement_strings)),
973
- )
974
-
975
-
976
- @deprecated()
977
- def get_requirements_required_distribution_names(
978
- requirements: Iterable[str] = (),
979
- echo: bool = False,
980
- ) -> MutableSet[str]:
981
- """
982
- Get the distributions required by one or more specified distributions or
983
- configuration files.
984
-
985
- Parameters:
986
-
987
- - requirements ([str]): One or more requirement specifiers (for example:
988
- "requirement-name[extra-a,extra-b]" or ".[extra-a, extra-b]) and/or paths
989
- to a setup.cfg, pyproject.toml, tox.ini or requirements.txt file
990
- """
991
- # Separate requirement strings from requirement files
992
- if isinstance(requirements, str):
993
- requirements = set((requirements,))
994
- else:
995
- requirements = set(requirements)
996
- requirement_files: MutableSet[str] = set(
997
- filter(is_configuration_file, requirements)
998
- )
999
- requirement_strings: MutableSet[str] = cast(
1000
- MutableSet[str], requirements - requirement_files
1001
- )
1002
- name: str
1003
- return set(
1004
- _iter_requirement_strings_required_distribution_names(
1005
- iter_distinct(
1006
- chain(
1007
- requirement_strings,
1008
- *map(
1009
- iter_configuration_file_requirement_strings,
1010
- requirement_files,
1011
- ),
1012
- )
1013
- ),
1014
- echo=echo,
1015
- )
1016
- )
1017
-
1018
-
1019
- @deprecated()
1020
- def iter_distribution_location_file_paths(location: str) -> Iterable[str]:
1021
- location = os.path.abspath(location)
1022
- name: str = get_setup_distribution_name(location)
1023
- setup_egg_info(location)
1024
- metadata_path: str = os.path.join(
1025
- location, f"{name.replace('-', '_')}.egg-info"
1026
- )
1027
- distribution: Distribution = Distribution.at(metadata_path)
1028
- if not distribution.files:
1029
- raise RuntimeError(f"No metadata found at {metadata_path}")
1030
- path: str
1031
- return map(
1032
- lambda path: os.path.abspath(os.path.join(location, path)),
1033
- distribution.files,
1034
- )