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/__main__.py +13 -6
- dependence/_utilities.py +352 -163
- dependence/freeze.py +111 -53
- dependence/update.py +75 -57
- dependence/upgrade.py +223 -0
- {dependence-1.0.2.dist-info → dependence-1.2.5.dist-info}/METADATA +84 -18
- dependence-1.2.5.dist-info/RECORD +11 -0
- {dependence-1.0.2.dist-info → dependence-1.2.5.dist-info}/WHEEL +1 -1
- dependence-1.0.2.dist-info/RECORD +0 -10
- {dependence-1.0.2.dist-info → dependence-1.2.5.dist-info}/entry_points.txt +0 -0
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
92
|
-
cwd:
|
|
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 (
|
|
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) ->
|
|
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:
|
|
173
|
-
b:
|
|
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:
|
|
183
|
-
bs:
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
303
|
-
|
|
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 =
|
|
409
|
+
basename: str = path.name.lower()
|
|
306
410
|
if basename == "setup.cfg":
|
|
307
411
|
return ConfigurationFileType.SETUP_CFG
|
|
308
|
-
|
|
412
|
+
if basename == "tox.ini":
|
|
309
413
|
return ConfigurationFileType.TOX_INI
|
|
310
|
-
|
|
414
|
+
if basename == "pyproject.toml":
|
|
311
415
|
return ConfigurationFileType.PYPROJECT_TOML
|
|
312
|
-
|
|
416
|
+
if basename.endswith(".txt"):
|
|
313
417
|
return ConfigurationFileType.REQUIREMENTS_TXT
|
|
314
|
-
|
|
418
|
+
if basename.endswith(".toml"):
|
|
315
419
|
return ConfigurationFileType.TOML
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
523
|
+
metadata,
|
|
353
524
|
)
|
|
354
525
|
|
|
355
526
|
|
|
356
|
-
@
|
|
357
|
-
def
|
|
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
|
-
|
|
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
|
|
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() ->
|
|
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:
|
|
560
|
+
installed: dict[str, Distribution] = {}
|
|
393
561
|
for distribution in _get_distributions():
|
|
394
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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:
|
|
529
|
-
include_pointers:
|
|
530
|
-
exclude_pointers:
|
|
531
|
-
) -> Iterable[
|
|
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:
|
|
585
|
-
exclude_pointers:
|
|
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:
|
|
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:
|
|
619
|
-
exclude_pointers:
|
|
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
|
-
|
|
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
|
-
|
|
825
|
+
if configuration_file_type == ConfigurationFileType.TOX_INI:
|
|
646
826
|
return _iter_tox_ini_requirement_strings(path=path)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
940
|
+
def _setup(arguments: tuple[str, ...]) -> None:
|
|
764
941
|
try:
|
|
765
|
-
check_output((sys.executable, "setup.py"
|
|
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:
|
|
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
|
|
966
|
+
return map_editable_project_locations().get(normalize_name(name), "")
|
|
790
967
|
|
|
791
968
|
|
|
792
|
-
def setup_egg_info(directory:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
)
|
|
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:
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
"-
|
|
919
|
-
|
|
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
|
-
|
|
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
|
|
1151
|
+
print(message) # noqa: T201
|
|
1152
|
+
raise
|
|
948
1153
|
try:
|
|
949
|
-
check_output(command
|
|
950
|
-
except CalledProcessError
|
|
951
|
-
print(message)
|
|
952
|
-
raise
|
|
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:
|
|
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
|
|
988
|
-
requirement: Requirement,
|
|
1192
|
+
def _get_installed_distribution(
|
|
989
1193
|
name: str,
|
|
990
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
1228
|
+
depth: int | None = None,
|
|
1038
1229
|
) -> Iterable[str]:
|
|
1039
1230
|
name: str = normalize_name(requirement.name)
|
|
1040
|
-
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:
|
|
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:
|
|
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_:
|
|
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_)}
|