dependence 1.0.2__py3-none-any.whl → 1.2.5__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 CHANGED
@@ -1,7 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import functools
2
4
  import json
3
5
  import os
4
6
  import re
7
+ import shutil
5
8
  import sys
6
9
  from collections import deque
7
10
  from configparser import ConfigParser, SectionProxy
@@ -17,21 +20,11 @@ from subprocess import DEVNULL, PIPE, CalledProcessError, list2cmdline, run
17
20
  from traceback import format_exception
18
21
  from typing import (
19
22
  IO,
20
- AbstractSet,
23
+ TYPE_CHECKING,
21
24
  Any,
22
- Callable,
23
- Container,
24
- Dict,
25
- Hashable,
26
- Iterable,
27
- List,
28
- MutableSet,
29
- Optional,
30
- Set,
31
- Tuple,
32
25
  TypedDict,
33
- Union,
34
26
  cast,
27
+ overload,
35
28
  )
36
29
  from warnings import warn
37
30
 
@@ -40,15 +33,122 @@ from jsonpointer import resolve_pointer # type: ignore
40
33
  from packaging.requirements import InvalidRequirement, Requirement
41
34
  from packaging.utils import canonicalize_name
42
35
 
43
- _BUILTIN_DISTRIBUTION_NAMES: Tuple[str] = ("distribute",)
36
+ if TYPE_CHECKING:
37
+ from collections.abc import (
38
+ Callable,
39
+ Container,
40
+ Hashable,
41
+ Iterable,
42
+ MutableSet,
43
+ )
44
+ from collections.abc import (
45
+ Set as AbstractSet,
46
+ )
47
+
48
+ _BUILTIN_DISTRIBUTION_NAMES: tuple[str] = ("distribute",)
44
49
  _UNSAFE_CHARACTERS_PATTERN: re.Pattern = re.compile("[^A-Za-z0-9.]+")
45
50
 
46
51
 
52
+ class DefinitionExistsError(Exception):
53
+ """
54
+ This error is raised when an attempt is made to redefine
55
+ a singleton class instance.
56
+ """
57
+
58
+
59
+ _module_locals: dict[str, Any] = locals()
60
+
61
+
62
+ class Undefined:
63
+ """
64
+ This class is intended to indicate that a parameter has not been passed
65
+ to a keyword argument in situations where `None` is to be used as a
66
+ meaningful value.
67
+
68
+ The `Undefined` class is a singleton, so only one instance of this class
69
+ is permitted: `sob.UNDEFINED`.
70
+ """
71
+
72
+ __module__ = "sob"
73
+
74
+ def __init__(self) -> None:
75
+ # Only one instance of `Undefined` is permitted, so initialization
76
+ # checks to make sure this is the first use.
77
+ if "UNDEFINED" in _module_locals:
78
+ message: str = f"{self!r} may only be instantiated once."
79
+ raise DefinitionExistsError(message)
80
+
81
+ def __repr__(self) -> str:
82
+ # Represent instances of this class using the qualified name for the
83
+ # constant `UNDEFINED`.
84
+ return "sob.UNDEFINED"
85
+
86
+ def __bool__(self) -> bool:
87
+ # `UNDEFINED` cast as a boolean is `False` (as with `None`)
88
+ return False
89
+
90
+ def __hash__(self) -> int:
91
+ return 0
92
+
93
+ def __eq__(self, other: object) -> bool:
94
+ # Another object is only equal to this if it shares the same id, since
95
+ # there should only be one instance of this class defined
96
+ return other is self
97
+
98
+ def __reduce__(self) -> tuple[Callable[[], Undefined], tuple]:
99
+ return _undefined, ()
100
+
101
+
102
+ UNDEFINED: Undefined = Undefined()
103
+
104
+
105
+ def _undefined() -> Undefined:
106
+ return UNDEFINED
107
+
108
+
109
+ cache: Any
110
+ try:
111
+ from functools import cache # type: ignore
112
+ except ImportError:
113
+ from functools import lru_cache
114
+
115
+ cache = lru_cache(maxsize=None)
116
+
117
+
118
+ def as_tuple(
119
+ user_function: Callable[..., Iterable[Any]],
120
+ ) -> Callable[..., tuple[Any, ...]]:
121
+ """
122
+ This is a decorator which will return an iterable as a tuple.
123
+ """
124
+
125
+ def wrapper(*args: Any, **kwargs: Any) -> tuple[Any, ...]:
126
+ return tuple(user_function(*args, **kwargs) or ())
127
+
128
+ return functools.update_wrapper(wrapper, user_function)
129
+
130
+
131
+ def as_cached_tuple(
132
+ maxsize: int | None = None, *, typed: bool = False
133
+ ) -> Callable[[Callable[..., Iterable[Any]]], Callable[..., tuple[Any, ...]]]:
134
+ """
135
+ This is a decorator which will return an iterable as a tuple,
136
+ and cache that tuple.
137
+
138
+ Parameters:
139
+
140
+ - maxsize (int|None) = None: The maximum number of items to cache.
141
+ - typed (bool) = False: For class methods, should the cache be distinct for
142
+ sub-classes?
143
+ """
144
+ return functools.lru_cache(maxsize=maxsize, typed=typed)(as_tuple)
145
+
146
+
47
147
  def iter_distinct(items: Iterable[Hashable]) -> Iterable:
48
148
  """
49
149
  Yield distinct elements, preserving order
50
150
  """
51
- visited: Set[Hashable] = set()
151
+ visited: set[Hashable] = set()
52
152
  item: Hashable
53
153
  for item in items:
54
154
  if item not in visited:
@@ -88,8 +188,9 @@ def iter_parse_delimited_values(
88
188
 
89
189
 
90
190
  def check_output(
91
- args: Tuple[str, ...],
92
- cwd: Union[str, Path] = "",
191
+ args: tuple[str, ...],
192
+ cwd: str | Path = "",
193
+ *,
93
194
  echo: bool = False,
94
195
  ) -> str:
95
196
  """
@@ -98,13 +199,13 @@ def check_output(
98
199
 
99
200
  Parameters:
100
201
 
101
- - command (Tuple[str, ...]): The command to run
202
+ - command (tuple[str, ...]): The command to run
102
203
  """
103
204
  if echo:
104
205
  if cwd:
105
- print("$", "cd", cwd, "&&", list2cmdline(args))
206
+ print("$", "cd", cwd, "&&", list2cmdline(args)) # noqa: T201
106
207
  else:
107
- print("$", list2cmdline(args))
208
+ print("$", list2cmdline(args)) # noqa: T201
108
209
  output: str = run(
109
210
  args,
110
211
  stdout=PIPE,
@@ -113,7 +214,7 @@ def check_output(
113
214
  cwd=cwd or None,
114
215
  ).stdout.decode("utf-8", errors="ignore")
115
216
  if echo:
116
- print(output)
217
+ print(output) # noqa: T201
117
218
  return output
118
219
 
119
220
 
@@ -164,13 +265,13 @@ def deprecated(message: str = "") -> Callable[..., Callable[..., Any]]:
164
265
  return decorating_function
165
266
 
166
267
 
167
- def split_dot(path: str) -> Tuple[str, ...]:
268
+ def split_dot(path: str) -> tuple[str, ...]:
168
269
  return tuple(path.split("."))
169
270
 
170
271
 
171
272
  def tuple_starts_with(
172
- a: Tuple[str, ...],
173
- b: Tuple[str, ...],
273
+ a: tuple[str, ...],
274
+ b: tuple[str, ...],
174
275
  ) -> bool:
175
276
  """
176
277
  Determine if tuple `a` starts with tuple `b`
@@ -179,18 +280,18 @@ def tuple_starts_with(
179
280
 
180
281
 
181
282
  def tuple_starts_with_any(
182
- a: Tuple[str, ...],
183
- bs: Tuple[Tuple[str, ...], ...],
283
+ a: tuple[str, ...],
284
+ bs: tuple[tuple[str, ...], ...],
184
285
  ) -> bool:
185
286
  """
186
287
  Determine if tuple `a` starts with any tuple in `bs`
187
288
  """
188
- b: Tuple[str, ...]
289
+ b: tuple[str, ...]
189
290
  return any(tuple_starts_with(a, b) for b in bs)
190
291
 
191
292
 
192
293
  def iter_find_qualified_lists(
193
- data: Union[Dict[str, Any], list],
294
+ data: dict[str, Any] | list,
194
295
  item_condition: Callable[[Any], bool],
195
296
  exclude_object_ids: AbstractSet[int] = frozenset(),
196
297
  ) -> Iterable[list]:
@@ -262,9 +363,8 @@ def iter_find_qualified_lists(
262
363
  if id(data) in exclude_object_ids:
263
364
  return
264
365
  if isinstance(data, dict):
265
- _key: str
266
366
  value: Any
267
- for _key, value in data.items():
367
+ for value in data.values():
268
368
  if isinstance(value, (list, dict)):
269
369
  yield from iter_find_qualified_lists(
270
370
  value, item_condition, exclude_object_ids
@@ -299,75 +399,143 @@ class ConfigurationFileType(Enum):
299
399
 
300
400
 
301
401
  @functools.lru_cache
302
- def get_configuration_file_type(path: str) -> ConfigurationFileType:
303
- if not os.path.isfile(path):
402
+ def get_configuration_file_type(
403
+ path: str | Path, default: Any = UNDEFINED
404
+ ) -> ConfigurationFileType:
405
+ if isinstance(path, str):
406
+ path = Path(path)
407
+ if not path.is_file():
304
408
  raise FileNotFoundError(path)
305
- basename: str = os.path.basename(path).lower()
409
+ basename: str = path.name.lower()
306
410
  if basename == "setup.cfg":
307
411
  return ConfigurationFileType.SETUP_CFG
308
- elif basename == "tox.ini":
412
+ if basename == "tox.ini":
309
413
  return ConfigurationFileType.TOX_INI
310
- elif basename == "pyproject.toml":
414
+ if basename == "pyproject.toml":
311
415
  return ConfigurationFileType.PYPROJECT_TOML
312
- elif basename.endswith(".txt"):
416
+ if basename.endswith(".txt"):
313
417
  return ConfigurationFileType.REQUIREMENTS_TXT
314
- elif basename.endswith(".toml"):
418
+ if basename.endswith(".toml"):
315
419
  return ConfigurationFileType.TOML
316
- else:
317
- raise ValueError(
318
- f"{path} is not a recognized type of configuration file."
420
+ if default is UNDEFINED:
421
+ message: str = (
422
+ f"{path!s} is not a recognized type of configuration file."
319
423
  )
424
+ raise ValueError(message)
425
+ return default
320
426
 
321
427
 
322
428
  def is_configuration_file(path: str) -> bool:
323
- try:
324
- get_configuration_file_type(path)
325
- except (FileNotFoundError, ValueError):
326
- return False
327
- return True
429
+ return get_configuration_file_type(path, default=None) is not None
430
+
431
+
432
+ @overload
433
+ def iter_configuration_files(path: str) -> Iterable[str]: ...
434
+
435
+
436
+ @overload
437
+ def iter_configuration_files(path: Path) -> Iterable[Path]: ...
438
+
439
+
440
+ def iter_configuration_files(path: str | Path) -> Iterable[Path | str]:
441
+ """
442
+ Iterate over the project configuration files for the given path.
443
+ If the path is a file path—yields only that path. If the path is a
444
+ directory, yields all configuration files in that directory.
445
+ """
446
+ if os.path.exists(path):
447
+ if os.path.isdir(path):
448
+ child: Path
449
+ for child in filter(
450
+ Path.is_file,
451
+ (
452
+ path.iterdir()
453
+ if isinstance(path, Path)
454
+ else Path(path).iterdir()
455
+ ),
456
+ ):
457
+ if (
458
+ get_configuration_file_type(child, default=None)
459
+ is not None
460
+ ):
461
+ yield (
462
+ child
463
+ if isinstance(path, Path)
464
+ else str(child.absolute())
465
+ )
466
+ elif get_configuration_file_type(path, default=None) is not None:
467
+ yield path
468
+
469
+
470
+ def _iter_editable_project_locations() -> Iterable[tuple[str, str]]:
471
+ metadata: PackageMetadata
472
+ for name, metadata in map_pip_list().items():
473
+ editable_project_location: str | None = metadata.get(
474
+ "editable_project_location"
475
+ )
476
+ if editable_project_location:
477
+ yield (
478
+ name,
479
+ editable_project_location,
480
+ )
328
481
 
329
482
 
330
- class _EditablePackageMetadata(TypedDict):
483
+ @functools.lru_cache
484
+ def map_editable_project_locations() -> dict[str, str]:
485
+ """
486
+ Get a mapping of (normalized) editable distribution names to their
487
+ locations.
488
+ """
489
+ return dict(_iter_editable_project_locations())
490
+
491
+
492
+ class PackageMetadata(TypedDict, total=False):
331
493
  name: str
332
494
  version: str
333
495
  editable_project_location: str
334
496
 
335
497
 
336
- def _iter_editable_distribution_locations() -> Iterable[Tuple[str, str]]:
337
- metadata: _EditablePackageMetadata
338
- for metadata in json.loads(
339
- check_output(
340
- (
341
- sys.executable,
342
- "-m",
343
- "pip",
344
- "list",
345
- "--editable",
346
- "--format=json",
347
- )
498
+ def _iter_pip_list() -> Iterable[tuple[str, PackageMetadata]]:
499
+ uv: str | None = shutil.which("uv")
500
+ command: tuple[str, ...]
501
+ if uv:
502
+ command = (
503
+ uv,
504
+ "pip",
505
+ "list",
506
+ "--python",
507
+ sys.executable,
508
+ "--format=json",
348
509
  )
349
- ):
510
+ else:
511
+ # If `uv` is not available, use `pip`
512
+ command = (
513
+ sys.executable,
514
+ "-m",
515
+ "pip",
516
+ "list",
517
+ "--format=json",
518
+ )
519
+ metadata: PackageMetadata
520
+ for metadata in json.loads(check_output(command)):
350
521
  yield (
351
522
  normalize_name(metadata["name"]),
352
- metadata["editable_project_location"],
523
+ metadata,
353
524
  )
354
525
 
355
526
 
356
- @functools.lru_cache
357
- def get_editable_distributions_locations() -> Dict[str, str]:
358
- """
359
- Get a mapping of (normalized) editable distribution names to their
360
- locations.
361
- """
362
- return dict(_iter_editable_distribution_locations())
527
+ @cache
528
+ def map_pip_list() -> dict[str, PackageMetadata]:
529
+ return dict(_iter_pip_list())
363
530
 
364
531
 
365
532
  def cache_clear() -> None:
366
533
  """
367
534
  Clear distribution metadata caches
368
535
  """
536
+ map_pip_list.cache_clear()
369
537
  get_installed_distributions.cache_clear()
370
- get_editable_distributions_locations.cache_clear()
538
+ map_editable_project_locations.cache_clear()
371
539
  is_editable.cache_clear()
372
540
  is_installed.cache_clear()
373
541
  get_requirement_string_distribution_name.cache_clear()
@@ -379,19 +547,25 @@ def refresh_editable_distributions() -> None:
379
547
  """
380
548
  name: str
381
549
  location: str
382
- for name, location in get_editable_distributions_locations().items():
550
+ for name, location in map_editable_project_locations().items():
383
551
  _install_requirement_string(location, name=name, editable=True)
384
552
 
385
553
 
386
554
  @functools.lru_cache
387
- def get_installed_distributions() -> Dict[str, Distribution]:
555
+ def get_installed_distributions() -> dict[str, Distribution]:
388
556
  """
389
557
  Return a dictionary of installed distributions.
390
558
  """
391
559
  refresh_editable_distributions()
392
- installed: Dict[str, Distribution] = {}
560
+ installed: dict[str, Distribution] = {}
393
561
  for distribution in _get_distributions():
394
- installed[normalize_name(distribution.metadata["Name"])] = distribution
562
+ name: str = distribution.metadata["Name"]
563
+ if distribution.version is None:
564
+ # If no version can be found, use pip to find the version
565
+ distribution.metadata["Version"] = (
566
+ map_pip_list().get(name, {}).get("version")
567
+ )
568
+ installed[normalize_name(name)] = distribution
395
569
  return installed
396
570
 
397
571
 
@@ -425,7 +599,7 @@ def is_requirement_string(requirement_string: str) -> bool:
425
599
 
426
600
 
427
601
  def _iter_file_requirement_strings(path: str) -> Iterable[str]:
428
- lines: List[str]
602
+ lines: list[str]
429
603
  requirement_file_io: IO[str]
430
604
  with open(path) as requirement_file_io:
431
605
  lines = requirement_file_io.readlines()
@@ -459,7 +633,7 @@ def _iter_setup_cfg_requirement_strings(path: str) -> Iterable[str]:
459
633
 
460
634
 
461
635
  def _iter_tox_ini_requirement_strings(
462
- path: Union[str, Path, ConfigParser] = "",
636
+ path: str | Path | ConfigParser = "",
463
637
  string: str = "",
464
638
  ) -> Iterable[str]:
465
639
  """
@@ -472,13 +646,19 @@ def _iter_tox_ini_requirement_strings(
472
646
  - string (str) = "": The contents of a tox.ini file
473
647
  """
474
648
  parser: ConfigParser = ConfigParser()
649
+ message: str
475
650
  if path:
476
- assert (
477
- not string
478
- ), "Either `path` or `string` arguments may be provided, but not both"
651
+ if string:
652
+ message = (
653
+ "Either `path` or `string` arguments may be provided, but not "
654
+ "both"
655
+ )
656
+ raise ValueError(message)
479
657
  parser.read(path)
480
658
  else:
481
- assert string, "Either a `path` or `string` argument must be provided"
659
+ if not string:
660
+ message = "Either a `path` or `string` argument must be provided"
661
+ raise ValueError(message)
482
662
  parser.read_string(string)
483
663
 
484
664
  def get_section_option_requirements(
@@ -525,10 +705,10 @@ def _is_installed_requirement_string(item: Any) -> bool:
525
705
 
526
706
 
527
707
  def iter_find_requirements_lists(
528
- document: Union[Dict[str, Any], list],
529
- include_pointers: Tuple[str, ...] = (),
530
- exclude_pointers: Tuple[str, ...] = (),
531
- ) -> Iterable[List[str]]:
708
+ document: dict[str, Any] | list,
709
+ include_pointers: tuple[str, ...] = (),
710
+ exclude_pointers: tuple[str, ...] = (),
711
+ ) -> Iterable[list[str]]:
532
712
  """
533
713
  Recursively yield all lists of valid requirement strings for installed
534
714
  packages. Exclusions are resolved before inclusions.
@@ -581,8 +761,8 @@ def iter_find_requirements_lists(
581
761
 
582
762
  def _iter_toml_requirement_strings(
583
763
  path: str,
584
- include_pointers: Tuple[str, ...] = (),
585
- exclude_pointers: Tuple[str, ...] = (),
764
+ include_pointers: tuple[str, ...] = (),
765
+ exclude_pointers: tuple[str, ...] = (),
586
766
  ) -> Iterable[str]:
587
767
  """
588
768
  Read a TOML file and yield the requirements found.
@@ -597,7 +777,7 @@ def _iter_toml_requirement_strings(
597
777
  # Parse pyproject.toml
598
778
  try:
599
779
  with open(path, "rb") as pyproject_io:
600
- document: Dict[str, Any] = tomli.load(pyproject_io)
780
+ document: dict[str, Any] = tomli.load(pyproject_io)
601
781
  except FileNotFoundError:
602
782
  return
603
783
  # Find requirements
@@ -615,8 +795,8 @@ def _iter_toml_requirement_strings(
615
795
  def iter_configuration_file_requirement_strings(
616
796
  path: str,
617
797
  *,
618
- include_pointers: Tuple[str, ...] = (),
619
- exclude_pointers: Tuple[str, ...] = (),
798
+ include_pointers: tuple[str, ...] = (),
799
+ exclude_pointers: tuple[str, ...] = (),
620
800
  ) -> Iterable[str]:
621
801
  """
622
802
  Read a configuration file and yield the parsed requirements.
@@ -633,7 +813,7 @@ def iter_configuration_file_requirement_strings(
633
813
  )
634
814
  if configuration_file_type == ConfigurationFileType.SETUP_CFG:
635
815
  return _iter_setup_cfg_requirement_strings(path)
636
- elif configuration_file_type in (
816
+ if configuration_file_type in (
637
817
  ConfigurationFileType.PYPROJECT_TOML,
638
818
  ConfigurationFileType.TOML,
639
819
  ):
@@ -642,13 +822,11 @@ def iter_configuration_file_requirement_strings(
642
822
  include_pointers=include_pointers,
643
823
  exclude_pointers=exclude_pointers,
644
824
  )
645
- elif configuration_file_type == ConfigurationFileType.TOX_INI:
825
+ if configuration_file_type == ConfigurationFileType.TOX_INI:
646
826
  return _iter_tox_ini_requirement_strings(path=path)
647
- else:
648
- assert (
649
- configuration_file_type == ConfigurationFileType.REQUIREMENTS_TXT
650
- )
651
- return _iter_file_requirement_strings(path)
827
+ if configuration_file_type != ConfigurationFileType.REQUIREMENTS_TXT:
828
+ raise ValueError(configuration_file_type)
829
+ return _iter_file_requirement_strings(path)
652
830
 
653
831
 
654
832
  @functools.lru_cache
@@ -656,7 +834,7 @@ def is_editable(name: str) -> bool:
656
834
  """
657
835
  Return `True` if the indicated distribution is an editable installation.
658
836
  """
659
- return bool(normalize_name(name) in get_editable_distributions_locations())
837
+ return bool(normalize_name(name) in map_editable_project_locations())
660
838
 
661
839
 
662
840
  def _get_setup_cfg_metadata(path: str, key: str) -> str:
@@ -669,15 +847,14 @@ def _get_setup_cfg_metadata(path: str, key: str) -> str:
669
847
  parser.read(path)
670
848
  if "metadata" in parser:
671
849
  return parser.get("metadata", key, fallback="")
672
- else:
673
- warn(
674
- f"No `metadata` section found in: {path}",
675
- stacklevel=2,
676
- )
850
+ warn(
851
+ f"No `metadata` section found in: {path}",
852
+ stacklevel=2,
853
+ )
677
854
  return ""
678
855
 
679
856
 
680
- def _get_setup_py_metadata(path: str, args: Tuple[str, ...]) -> str:
857
+ def _get_setup_py_metadata(path: str, args: tuple[str, ...]) -> str:
681
858
  """
682
859
  Execute a setup.py script with `args` and return the response.
683
860
 
@@ -699,7 +876,7 @@ def _get_setup_py_metadata(path: str, args: Tuple[str, ...]) -> str:
699
876
  os.chdir(directory)
700
877
  path = os.path.join(directory, "setup.py")
701
878
  if os.path.isfile(path):
702
- command: Tuple[str, ...] = (sys.executable, path) + args
879
+ command: tuple[str, ...] = (sys.executable, path, *args)
703
880
  try:
704
881
  value = check_output(command).strip().split("\n")[-1]
705
882
  except CalledProcessError:
@@ -713,7 +890,7 @@ def _get_setup_py_metadata(path: str, args: Tuple[str, ...]) -> str:
713
890
  setup_egg_info(directory)
714
891
  try:
715
892
  value = check_output(command).strip().split("\n")[-1]
716
- except Exception:
893
+ except Exception: # noqa: BLE001
717
894
  warn(
718
895
  f"A package name could not be found in {path}"
719
896
  f"\nError ignored: {get_exception_text()}",
@@ -732,7 +909,7 @@ def _get_pyproject_toml_project_metadata(path: str, key: str) -> str:
732
909
  if os.path.isfile(path):
733
910
  pyproject_io: IO[str]
734
911
  with open(path) as pyproject_io:
735
- pyproject: Dict[str, Any] = tomli.loads(pyproject_io.read())
912
+ pyproject: dict[str, Any] = tomli.loads(pyproject_io.read())
736
913
  if "project" in pyproject:
737
914
  return pyproject["project"].get(key, "")
738
915
  return ""
@@ -760,15 +937,15 @@ def get_setup_distribution_version(path: str) -> str:
760
937
  )
761
938
 
762
939
 
763
- def _setup(arguments: Tuple[str, ...]) -> None:
940
+ def _setup(arguments: tuple[str, ...]) -> None:
764
941
  try:
765
- check_output((sys.executable, "setup.py") + arguments)
942
+ check_output((sys.executable, "setup.py", *arguments))
766
943
  except CalledProcessError:
767
944
  warn(f"Ignoring error: {get_exception_text()}", stacklevel=2)
768
945
 
769
946
 
770
947
  def _setup_location(
771
- location: Union[str, Path], arguments: Iterable[Tuple[str, ...]]
948
+ location: str | Path, arguments: Iterable[tuple[str, ...]]
772
949
  ) -> None:
773
950
  if isinstance(location, str):
774
951
  location = Path(location)
@@ -786,10 +963,10 @@ def _setup_location(
786
963
 
787
964
 
788
965
  def get_editable_distribution_location(name: str) -> str:
789
- return get_editable_distributions_locations().get(normalize_name(name), "")
966
+ return map_editable_project_locations().get(normalize_name(name), "")
790
967
 
791
968
 
792
- def setup_egg_info(directory: Union[str, Path], egg_base: str = "") -> None:
969
+ def setup_egg_info(directory: str | Path, egg_base: str = "") -> None:
793
970
  """
794
971
  Refresh egg-info for the editable package installed in
795
972
  `directory` (only applicable for packages using a `setup.py` script)
@@ -819,27 +996,30 @@ def get_requirement(
819
996
  ) -> Requirement:
820
997
  try:
821
998
  return Requirement(requirement_string)
822
- except InvalidRequirement:
999
+ except InvalidRequirement as error:
823
1000
  # Try to parse the requirement as an installation target location,
824
1001
  # such as can be used with `pip install`
825
1002
  location: str = requirement_string
826
1003
  extras: str = ""
827
1004
  if "[" in requirement_string and requirement_string.endswith("]"):
828
- parts: List[str] = requirement_string.split("[")
1005
+ parts: list[str] = requirement_string.split("[")
829
1006
  location = "[".join(parts[:-1])
830
1007
  extras = f"[{parts[-1]}"
831
1008
  location = os.path.abspath(location)
832
1009
  name: str = get_setup_distribution_name(location)
833
- assert name, f"No distribution found in {location}"
1010
+ if not name:
1011
+ message: str = f"No distribution found in {location}"
1012
+ raise FileNotFoundError(message) from error
834
1013
  return Requirement(f"{name}{extras}")
835
1014
 
836
1015
 
837
1016
  def get_required_distribution_names(
838
1017
  requirement_string: str,
1018
+ *,
839
1019
  exclude: Iterable[str] = (),
840
1020
  recursive: bool = True,
841
1021
  echo: bool = False,
842
- depth: Optional[int] = None,
1022
+ depth: int | None = None,
843
1023
  ) -> MutableSet[str]:
844
1024
  """
845
1025
  Return a `tuple` of all distribution names which are required by the
@@ -878,10 +1058,11 @@ def _get_requirement_name(requirement: Requirement) -> str:
878
1058
  return normalize_name(requirement.name)
879
1059
 
880
1060
 
881
- def install_requirement(
882
- requirement: Union[str, Requirement],
883
- echo: bool = True,
884
- ) -> None:
1061
+ @deprecated(
1062
+ "dependence._utilities.install_requirement is deprecated and will be "
1063
+ "removed in a future release."
1064
+ )
1065
+ def install_requirement(requirement: str | Requirement) -> None:
885
1066
  """
886
1067
  Install a requirement
887
1068
 
@@ -899,27 +1080,51 @@ def install_requirement(
899
1080
  def _install_requirement_string(
900
1081
  requirement_string: str,
901
1082
  name: str = "",
1083
+ *,
902
1084
  editable: bool = False,
903
1085
  ) -> None:
904
1086
  """
905
1087
  Install a requirement string with no dependencies, compilation, build
906
1088
  isolation, etc.
907
1089
  """
908
- command: Tuple[str, ...] = (
909
- sys.executable,
910
- "-m",
911
- "pip",
912
- "install",
913
- "--no-deps",
914
- "--no-compile",
915
- )
916
- if editable:
917
- command += (
918
- "-e",
919
- requirement_string,
1090
+ command: tuple[str, ...]
1091
+ uv: str | None = shutil.which("uv")
1092
+ if uv:
1093
+ command = (
1094
+ uv,
1095
+ "pip",
1096
+ "install",
1097
+ "--python",
1098
+ sys.executable,
1099
+ "--no-deps",
1100
+ "--no-compile",
1101
+ *(
1102
+ (
1103
+ "-e",
1104
+ requirement_string,
1105
+ )
1106
+ if editable
1107
+ else (requirement_string,)
1108
+ ),
920
1109
  )
921
1110
  else:
922
- command += (requirement_string,)
1111
+ # If `uv` is not available, use `pip`
1112
+ command = (
1113
+ sys.executable,
1114
+ "-m",
1115
+ "pip",
1116
+ "install",
1117
+ "--no-deps",
1118
+ "--no-compile",
1119
+ *(
1120
+ (
1121
+ "-e",
1122
+ requirement_string,
1123
+ )
1124
+ if editable
1125
+ else (requirement_string,)
1126
+ ),
1127
+ )
923
1128
  try:
924
1129
  check_output(command)
925
1130
  except CalledProcessError as error:
@@ -943,13 +1148,13 @@ def _install_requirement_string(
943
1148
  )
944
1149
  )
945
1150
  if not editable:
946
- print(message)
947
- raise error
1151
+ print(message) # noqa: T201
1152
+ raise
948
1153
  try:
949
- check_output(command + ("--force-reinstall",))
950
- except CalledProcessError as retry_error:
951
- print(message)
952
- raise retry_error
1154
+ check_output((*command, "--force-reinstall"))
1155
+ except CalledProcessError:
1156
+ print(message) # noqa: T201
1157
+ raise
953
1158
 
954
1159
 
955
1160
  def _install_requirement(
@@ -957,7 +1162,7 @@ def _install_requirement(
957
1162
  ) -> None:
958
1163
  requirement_string: str = str(requirement)
959
1164
  # Get the distribution name
960
- distribution: Optional[Distribution] = None
1165
+ distribution: Distribution | None = None
961
1166
  editable_location: str = ""
962
1167
  try:
963
1168
  distribution = _get_distribution(requirement.name)
@@ -984,35 +1189,20 @@ def _install_requirement(
984
1189
  cache_clear()
985
1190
 
986
1191
 
987
- def _get_requirement_distribution(
988
- requirement: Requirement,
1192
+ def _get_installed_distribution(
989
1193
  name: str,
990
- reinstall: bool = True,
991
- echo: bool = False,
992
- ) -> Optional[Distribution]:
1194
+ ) -> Distribution | None:
993
1195
  if name in _BUILTIN_DISTRIBUTION_NAMES:
994
1196
  return None
995
1197
  try:
996
1198
  return get_installed_distributions()[name]
997
1199
  except KeyError:
998
- if not reinstall:
999
- raise
1000
- if echo:
1001
- warn(
1002
- f'The required distribution "{name}" was not installed, '
1003
- "attempting to install it now...",
1004
- stacklevel=2,
1005
- )
1006
- # Attempt to install the requirement...
1007
- install_requirement(requirement, echo=echo)
1008
- return _get_requirement_distribution(
1009
- requirement, name, reinstall=False, echo=echo
1010
- )
1200
+ return None
1011
1201
 
1012
1202
 
1013
1203
  def _iter_distribution_requirements(
1014
1204
  distribution: Distribution,
1015
- extras: Tuple[str, ...] = (),
1205
+ extras: tuple[str, ...] = (),
1016
1206
  exclude: Container[str] = (),
1017
1207
  ) -> Iterable[Requirement]:
1018
1208
  if not distribution.requires:
@@ -1031,24 +1221,23 @@ def _iter_distribution_requirements(
1031
1221
 
1032
1222
  def _iter_requirement_names(
1033
1223
  requirement: Requirement,
1224
+ *,
1034
1225
  exclude: MutableSet[str],
1035
1226
  recursive: bool = True,
1036
1227
  echo: bool = False,
1037
- depth: Optional[int] = None,
1228
+ depth: int | None = None,
1038
1229
  ) -> Iterable[str]:
1039
1230
  name: str = normalize_name(requirement.name)
1040
- extras: Tuple[str, ...] = tuple(requirement.extras)
1231
+ extras: tuple[str, ...] = tuple(requirement.extras)
1041
1232
  if name in exclude:
1042
1233
  return ()
1043
1234
  # Ensure we don't follow the same requirement again, causing cyclic
1044
1235
  # recursion
1045
1236
  exclude.add(name)
1046
- distribution: Optional[Distribution] = _get_requirement_distribution(
1047
- requirement, name, echo=echo
1048
- )
1237
+ distribution: Distribution | None = _get_installed_distribution(name)
1049
1238
  if distribution is None:
1050
1239
  return ()
1051
- requirements: Tuple[Requirement, ...] = tuple(
1240
+ requirements: tuple[Requirement, ...] = tuple(
1052
1241
  iter_distinct(
1053
1242
  _iter_distribution_requirements(
1054
1243
  distribution,
@@ -1061,13 +1250,13 @@ def _iter_requirement_names(
1061
1250
 
1062
1251
  def iter_requirement_names_(
1063
1252
  requirement_: Requirement,
1064
- depth_: Optional[int] = None,
1253
+ depth_: int | None = None,
1065
1254
  ) -> Iterable[str]:
1066
1255
  if (depth_ is None) or depth_ >= 0:
1067
1256
  yield from _iter_requirement_names(
1068
1257
  requirement_,
1069
1258
  exclude=cast(
1070
- MutableSet[str],
1259
+ "MutableSet[str]",
1071
1260
  exclude
1072
1261
  | (
1073
1262
  lateral_exclude - {_get_requirement_name(requirement_)}