-
-
Notifications
You must be signed in to change notification settings - Fork 422
/
Copy pathutils.py
750 lines (638 loc) · 27.4 KB
/
utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
from __future__ import annotations
import argparse
import dataclasses as dc
import json
import os
import re
import sys
from collections import OrderedDict
from difflib import SequenceMatcher
from fnmatch import fnmatch
from gettext import gettext as _
from json import dumps
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, MutableMapping, cast, no_type_check
from packaging.specifiers import SpecifierSet
from resolvelib.structs import DirectedGraph
from rich.tree import Tree
from pdm import termui
from pdm.exceptions import PdmArgumentError, ProjectError
from pdm.models.markers import EnvSpec
from pdm.models.requirements import (
Requirement,
filter_requirements_with_extras,
strip_extras,
)
from pdm.models.specifiers import PySpecSet, get_specifier
from pdm.utils import (
comparable_version,
is_path_relative_to,
normalize_name,
url_to_path,
)
if TYPE_CHECKING:
from argparse import Action, _ArgumentGroup
from packaging.version import Version
from resolvelib.resolvers import RequirementInformation, ResolutionImpossible
from pdm.compat import Distribution
from pdm.compat import importlib_metadata as im
from pdm.models.candidates import Candidate
from pdm.project import Project
class PdmFormatter(argparse.RawDescriptionHelpFormatter):
def start_section(self, heading: str | None) -> None:
return super().start_section(termui.style(heading.title() if heading else "", style="warning"))
def _format_usage(
self,
usage: str | None,
actions: Iterable[Action],
groups: Iterable[_ArgumentGroup],
prefix: str | None,
) -> str:
if prefix is None:
prefix = "Usage: "
result = super()._format_usage(usage, actions, groups, prefix) # type: ignore[arg-type]
# Remove continuous spaces
result = re.sub(r" +", " ", result)
if prefix:
return result.replace(prefix, termui.style(prefix, style="warning"))
return result
def _format_action(self, action: Action) -> str:
# determine the required width and the entry label
help_position = min(self._action_max_length + 2, self._max_help_position)
help_width = max(self._width - help_position, 11)
action_width = help_position - self._current_indent - 2
action_header = self._format_action_invocation(action)
# no help; start on same line and add a final newline
if not action.help:
tup = "", self._current_indent, action_header
action_header = "{0:>{1}}{2}\n".format(*tup)
# short action name; start on the same line and pad two spaces
elif len(action_header) <= action_width:
tup = "", self._current_indent, action_header, action_width # type: ignore[assignment]
action_header = "{0:>{1}}{2:<{3}} ".format(*tup)
indent_first = 0
# long action name; start on the next line
else:
tup = "", self._current_indent, action_header
action_header = "{0:>{1}}{2}\n".format(*tup)
indent_first = help_position
# Special format for empty action_header
# - No extra indent block
# - Help info in the same indent level as subactions
if not action_header.strip():
action_header = ""
help_position = self._current_indent
indent_first = self._current_indent
# collect the pieces of the action help
parts = [termui.style(action_header, style="primary")]
# if there was help for the action, add lines of help text
if action.help:
help_text = self._expand_help(action)
help_lines = self._split_lines(help_text, help_width)
parts.append("{:>{}}{}\n".format("", indent_first, help_lines[0]))
for line in help_lines[1:]:
parts.append("{:>{}}{}\n".format("", help_position, line))
# or add a newline if the description doesn't end with one
elif not action_header.endswith("\n"):
if action_header:
parts.append("\n")
# cancel out extra indent when action_header is empty
if not action_header:
self._dedent()
# if there are any sub-actions, add their help as well
for subaction in self._iter_indented_subactions(action):
parts.append(self._format_action(subaction))
# cancel out extra dedent when action_header is empty
if not action_header:
self._indent()
# return a single string
return self._join_parts(parts)
class ArgumentParser(argparse.ArgumentParser):
"""A standard argument parser but with title-cased help."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
kwargs["formatter_class"] = PdmFormatter
kwargs["add_help"] = False
super().__init__(*args, **kwargs)
self.add_argument(
"-h", "--help", action="help", default=argparse.SUPPRESS, help="Show this help message and exit."
)
self._optionals.title = "options"
def parse_known_args(self, args: Any = None, namespace: Any = None) -> Any:
args, argv = super().parse_known_args(args, namespace)
if argv:
msg = _("unrecognized arguments: %s")
self.error(msg % " ".join(argv))
return args, argv
def find_similar_text(origin_name: str, target_names: list[str], ratio: float = 0.5) -> list[str]:
results = []
for name in target_names:
ratio = SequenceMatcher(None, origin_name, name).ratio()
if ratio > 0.4:
results.append((name, ratio))
return [name for name, _ in sorted(results, key=lambda x: x[1], reverse=True)]
def format_similar_command(root_command: str, commands: list[str], script_commands: list[str]) -> str:
similar_commands = find_similar_text(root_command, commands)
similar_script_commands = find_similar_text(root_command, script_commands)
commands_text = "\n".join([f" - {cmd}" for cmd in similar_commands])
script_commands_text = "\n".join([f" - {cmd}" for cmd in similar_script_commands])
message = f"""[red]Command not found: {root_command}[/]
"""
if commands_text:
message += f"""
[green]Did you mean one of these command?
{commands_text}[/]
"""
if script_commands_text:
message += f"""
[yellow]Or one of these script command?
{script_commands_text}[/]
"""
return message
class ErrorArgumentParser(ArgumentParser):
"""A subclass of argparse.ArgumentParser that raises
parsing error rather than exiting.
This does the same as passing exit_on_error=False on Python 3.9+
"""
def _parse_known_args(
self, arg_strings: list[str], namespace: argparse.Namespace, *args: Any, **kwargs: Any
) -> tuple[argparse.Namespace, list[str]]:
try:
return super()._parse_known_args(arg_strings, namespace, *args, **kwargs)
except argparse.ArgumentError as e:
# We raise a dedicated error to avoid being caught by the caller
raise PdmArgumentError(e) from e
@dc.dataclass(frozen=True)
class PackageNode:
"""An internal class for the convenience of dependency graph building."""
name: str = dc.field(hash=True, compare=True)
version: str | None = dc.field(compare=False)
requirements: dict[str, Requirement] = dc.field(compare=False)
def __repr__(self) -> str:
return f"<Package {self.name}=={self.version}>"
def build_dependency_graph(
working_set: Mapping[str, im.Distribution],
env_spec: EnvSpec,
selected: set[str] | None = None,
include_sub: bool = True,
) -> DirectedGraph:
"""Build a dependency graph from locked result."""
graph: DirectedGraph[PackageNode | None] = DirectedGraph()
graph.add(None) # sentinel parent of top nodes.
node_with_extras: set[str] = set()
def add_package(key: str, dist: Distribution | None) -> PackageNode:
name, extras = strip_extras(key)
extras = extras or ()
reqs: dict[str, Requirement] = {}
if dist:
requirements = filter_requirements_with_extras(dist.requires or [], extras, include_default=True)
for req in requirements:
if not req.marker or req.marker.matches(env_spec):
reqs[req.identify()] = req
version: str | None = dist.version
else:
version = None
node = PackageNode(key, version, reqs)
if node not in graph:
if extras:
node_with_extras.add(name)
graph.add(node)
if include_sub:
for k in reqs:
child = add_package(k, working_set.get(strip_extras(k)[0]))
graph.connect(node, child)
return node
selected_map: dict[str, str] = {}
for key in selected or ():
name = key.split("[")[0]
if len(key) >= len(selected_map.get(name, "")):
# Ensure key with extras remains
selected_map[name] = key
for k, dist in working_set.items():
if selected is not None:
name = k.split("[")[0]
if name not in selected_map:
continue
k = selected_map[name]
add_package(k, dist)
for node in list(graph):
if node is not None and not list(graph.iter_parents(node)):
# Top requirements
if node.name in node_with_extras:
# Already included in package[extra], no need to keep the top level
# non-extra package.
graph.remove(node)
else:
graph.connect(None, node)
return graph
def specifier_from_requirement(requirement: Requirement) -> str:
return str(requirement.specifier) or "Any"
def add_package_to_tree(
root: Tree,
graph: DirectedGraph,
package: PackageNode,
required: list[str],
visited: frozenset[str] = frozenset(),
) -> None:
"""Format one package.
:param graph: the dependency graph
:param package: the package instance
:param required: the version required by its parent
:param visited: the visited package collection
"""
version = (
"[error][ not installed ][/]"
if not package.version
else f"[error]{package.version}[/]"
if required
and not any(s in ("Any", "This project") or SpecifierSet(s).contains(package.version) for s in required)
else f"[warning]{package.version}[/]"
)
# escape deps with extras
name = package.name.replace("[", r"\[") if "[" in package.name else package.name
if package.name in visited:
version = r"[error]\[circular][/]"
req_str = f"[ required: {'&&'.join(required)} ]" if required else "[ Not required ]"
node = root.add(f"[req]{name}[/] {version} {req_str}")
if package.name in visited:
return
children = sorted(graph.iter_children(package), key=lambda p: p.name)
for child in children:
required = [specifier_from_requirement(package.requirements[child.name])]
add_package_to_tree(node, graph, child, required, visited | {package.name})
def add_package_to_reverse_tree(
root: Tree,
graph: DirectedGraph,
package: PackageNode,
child: PackageNode | None = None,
requires: str = "",
visited: frozenset[str] = frozenset(),
) -> None:
"""Format one package for output reverse dependency graph."""
version = "[error][ not installed ][/]" if not package.version else f"[warning]{package.version}[/]"
if package.name in visited:
version = r"[error]\[circular][/]"
requires = (
f"[ requires: [error]{requires}[/] ]"
if requires not in ("Any", "")
and child
and child.version
and not SpecifierSet(requires).contains(child.version)
else ""
if not requires
else f"[ requires: {requires} ]"
)
name = package.name.replace("[", r"\[") if "[" in package.name else package.name
node = root.add(f"[req]{name}[/] {version} {requires}")
if package.name in visited:
return
parents: list[PackageNode] = sorted(filter(None, graph.iter_parents(package)), key=lambda p: p.name)
for parent in parents:
requires = specifier_from_requirement(parent.requirements[package.name])
add_package_to_reverse_tree(node, graph, parent, package, requires, visited=visited | {package.name})
return
def package_is_project(package: PackageNode, project: Project) -> bool:
return project.is_distribution and package.name == normalize_name(project.name)
def _format_forward_dependency_graph(
project: Project, graph: DirectedGraph[PackageNode | None], patterns: list[str]
) -> Tree:
"""Format dependency graph for output."""
root = Tree("Dependencies", hide_root=True)
def find_package_to_add(package: PackageNode) -> PackageNode | None:
if not patterns:
return package
to_check = [package]
while to_check:
package = to_check.pop(0)
if package and package_match_patterns(package, patterns):
return package
to_check.extend(graph.iter_children(package))
return None
all_dependencies = {
r.identify(): r
for deps in project.all_dependencies.values()
for r in deps
if not r.marker or r.marker.matches(project.environment.spec)
}
top_level_dependencies = {find_package_to_add(node) for node in graph.iter_children(None) if node}
for package in sorted((node for node in top_level_dependencies if node), key=lambda p: p.name):
required: set[str] = set()
for parent in graph.iter_parents(package):
if parent:
r = specifier_from_requirement(parent.requirements[package.name])
elif package.name in all_dependencies:
r = specifier_from_requirement(all_dependencies[package.name])
elif package_is_project(package, project):
r = "This project"
else:
continue
required.add(r)
add_package_to_tree(root, graph, package, sorted(required))
return root
def _format_reverse_dependency_graph(
project: Project, graph: DirectedGraph[PackageNode | None], patterns: list[str]
) -> Tree:
"""Format reverse dependency graph for output."""
def find_package_to_add(package: PackageNode) -> PackageNode | None:
if not patterns:
return package
to_check = [package]
while to_check:
package = to_check.pop(0)
if package and package_match_patterns(package, patterns):
return package
to_check.extend(graph.iter_parents(package))
return None
root = Tree("Dependencies", hide_root=True)
leaf_nodes = {find_package_to_add(node) for node in graph if not list(graph.iter_children(node)) and node}
for package in sorted((node for node in leaf_nodes if node), key=lambda p: p.name):
if not package:
continue
add_package_to_reverse_tree(root, graph, package)
return root
def build_forward_dependency_json_subtree(
root: PackageNode,
project: Project,
graph: DirectedGraph[PackageNode | None],
required_by: PackageNode | None = None,
visited: frozenset[str] = frozenset(),
) -> dict:
required: set[str] = set()
all_dependencies = {
r.identify(): r
for deps in project.all_dependencies.values()
for r in deps
if not r.marker or r.marker.matches(project.environment.spec)
}
for parent in graph.iter_parents(root):
if parent:
r = specifier_from_requirement(parent.requirements[root.name])
elif not package_is_project(root, project):
requirements = required_by.requirements if required_by else all_dependencies
if root.name in requirements:
r = specifier_from_requirement(requirements[root.name])
else:
continue
else:
r = "This project"
required.add(r)
children = graph.iter_children(root) if root.name not in visited else []
return OrderedDict(
package=root.name,
version=root.version,
required="&&".join(sorted(required)),
dependencies=sorted(
(
build_forward_dependency_json_subtree(p, project, graph, root, visited | {root.name})
for p in children
if p
),
key=lambda d: d["package"],
),
)
def build_reverse_dependency_json_subtree(
root: PackageNode,
project: Project,
graph: DirectedGraph[PackageNode | None],
requires: PackageNode | None = None,
visited: frozenset[str] = frozenset(),
) -> dict:
parents = graph.iter_parents(root) if root.name not in visited else []
return OrderedDict(
package=root.name,
version=root.version,
requires=specifier_from_requirement(root.requirements[requires.name]) if requires else None,
dependents=sorted(
(
build_reverse_dependency_json_subtree(p, project, graph, root, visited | {root.name})
for p in parents
if p
),
key=lambda d: d["package"],
),
)
def package_match_patterns(package: PackageNode, patterns: list[str]) -> bool:
return not patterns or any(fnmatch(package.name, pattern) for pattern in patterns)
def build_dependency_json_tree(
project: Project, graph: DirectedGraph[PackageNode | None], reverse: bool, patterns: list[str]
) -> list[dict]:
def find_package_to_add(package: PackageNode) -> PackageNode | None:
if not patterns:
return package
to_check = [package]
while to_check:
package = to_check.pop(0)
if package and package_match_patterns(package, patterns):
return package
if reverse:
to_check.extend(graph.iter_parents(package))
else:
to_check.extend(graph.iter_children(package))
return None
top_level_packages: Iterable[PackageNode | None]
if reverse:
top_level_packages = filter(lambda n: not list(graph.iter_children(n)), graph) # leaf nodes
build_dependency_json_subtree: Callable = build_reverse_dependency_json_subtree
else:
top_level_packages = graph.iter_children(None) # root nodes
build_dependency_json_subtree = build_forward_dependency_json_subtree
top_level_packages = {find_package_to_add(node) for node in top_level_packages if node}
return [
build_dependency_json_subtree(p, project, graph)
for p in sorted((node for node in top_level_packages if node), key=lambda p: p.name)
]
def show_dependency_graph(
project: Project,
graph: DirectedGraph[PackageNode | None],
reverse: bool = False,
json: bool = False,
patterns: list[str] | None = None,
) -> None:
echo = project.core.ui.echo
if patterns is None:
patterns = []
if json:
echo(
dumps(
build_dependency_json_tree(project, graph, reverse, patterns),
indent=2,
)
)
return
if reverse:
tree = _format_reverse_dependency_graph(project, graph, patterns)
else:
tree = _format_forward_dependency_graph(project, graph, patterns)
echo(tree)
def save_version_specifiers(
requirements: Iterable[Requirement],
resolved: dict[str, list[Candidate]],
save_strategy: str,
) -> None:
"""Rewrite the version specifiers according to the resolved result and save strategy
:param requirements: the requirements to be updated
:param resolved: the resolved mapping
:param save_strategy: compatible/safe_compatible/wildcard/exact
"""
def candidate_version(candidates: list[Candidate]) -> Version | None:
if len(candidates) > 1:
return None
c = candidates[0]
assert c.version is not None
return comparable_version(c.version)
for r in requirements:
name = r.identify()
if r.is_named and not r.specifier and name in resolved:
version = candidate_version(resolved[name])
if version is None:
continue
if save_strategy == "exact":
r.specifier = get_specifier(f"=={version}")
elif save_strategy in ["compatible", "safe_compatible"]:
if version.is_prerelease or version.is_devrelease:
r.specifier = get_specifier(f">={version},<{version.major + 1}")
else:
if save_strategy == "compatible":
r.specifier = get_specifier(f"~={version.major}.{version.minor}")
else:
r.specifier = get_specifier(f"~={version}")
elif save_strategy == "minimum":
r.specifier = get_specifier(f">={version}")
def check_project_file(project: Project) -> None:
"""Check the existence of the project file and throws an error on failure."""
if not project.pyproject.is_valid:
raise ProjectError(
"The pyproject.toml has not been initialized yet. You can do this by running [success]`pdm init`[/]."
) from None
def find_importable_files(project: Project) -> Iterable[tuple[str, Path]]:
"""Find all possible files that can be imported"""
from pdm.formats import FORMATS
for filename in ("Pipfile", "pyproject.toml", "requirements.in", "requirements.txt", "setup.py", "setup.cfg"):
project_file = project.root / filename
if not project_file.exists():
continue
for key, module in FORMATS.items():
if module.check_fingerprint(project, project_file.as_posix()):
yield key, project_file
@no_type_check
def set_env_in_reg(env_name: str, value: str) -> None:
"""Manipulate the WinReg, and add value to the
environment variable if exists or create new.
"""
import winreg
value = os.path.normcase(value)
with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root:
with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as env_key:
try:
old_value, type_ = winreg.QueryValueEx(env_key, env_name)
paths = [os.path.normcase(item) for item in old_value.split(os.pathsep)]
if value in paths:
return
except FileNotFoundError:
paths, type_ = [], winreg.REG_EXPAND_SZ
new_value = os.pathsep.join([value, *paths])
winreg.SetValueEx(env_key, env_name, 0, type_, new_value)
def format_resolution_impossible(err: ResolutionImpossible) -> str:
from pdm.resolver.python import PythonRequirement
causes: list[RequirementInformation] = err.causes
info_lines: set[str] = set()
if not causes:
return ""
if all(isinstance(cause.requirement, PythonRequirement) for cause in causes):
project_requires: PythonRequirement = next(cause.requirement for cause in causes if cause.parent is None)
pyspec = cast(PySpecSet, project_requires.specifier)
conflicting = [
cause
for cause in causes
if cause.parent is not None and not cause.requirement.specifier.is_superset(pyspec)
]
result = [
"Unable to find a resolution because the following dependencies don't work "
"on all Python versions in the range of the project's `requires-python`: "
f"[success]{pyspec}[/]."
]
for req, parent in conflicting:
pyspec &= req.specifier
info_lines.add(f" {req.as_line()} (from {parent!r})")
result.extend(sorted(info_lines))
if pyspec.is_empty():
result.append("Consider changing the version specifiers of the dependencies to be compatible")
else:
result.append(
"A possible solution is to change the value of `requires-python` "
f"in pyproject.toml to [success]{pyspec}[/]."
)
return "\n".join(result)
if len(causes) == 1:
return (
"Unable to find a resolution for "
f"[success]{causes[0].requirement.identify()}[/]\n"
"Please make sure the package name is correct."
)
result = [
"Unable to find a resolution for "
f"[success]{causes[0].requirement.identify()}[/]\n"
"because of the following conflicts:"
]
for req, parent in causes:
info_lines.add(f" {req.as_line()} (from {parent if parent else 'project'})")
result.extend(sorted(info_lines))
result.append(
"To fix this, you could loosen the dependency version constraints in "
"pyproject.toml. See https://pdm-project.org/en/latest/usage/lockfile/"
"#solve-the-locking-failure for more details."
)
return "\n".join(result)
def merge_dictionary(target: MutableMapping[Any, Any], input: Mapping[Any, Any], append_array: bool = True) -> None:
"""Merge the input dict with the target while preserving the existing values
properly. This will update the target dictionary in place.
List values will be extended, but only if the value is not already in the list.
"""
for key, value in input.items():
if key not in target:
target[key] = value
elif isinstance(target[key], dict):
merge_dictionary(target[key], value, append_array=append_array)
elif isinstance(target[key], list) and append_array:
target[key].extend(x for x in value if x not in target[key])
if hasattr(target[key], "multiline"):
target[key].multiline(True) # type: ignore[attr-defined]
else:
target[key] = value
def is_pipx_installation() -> bool:
return sys.prefix.split(os.sep)[-3:-1] == ["pipx", "venvs"]
def is_homebrew_installation() -> bool:
return "/libexec" in sys.prefix.replace("\\", "/")
def is_scoop_installation() -> bool:
return os.name == "nt" and is_path_relative_to(sys.prefix, Path.home() / "scoop/apps/pdm")
def get_dist_location(dist: Distribution) -> str:
direct_url = dist.read_text("direct_url.json")
if not direct_url:
return ""
direct_url_data = json.loads(direct_url)
url = cast(str, direct_url_data["url"])
if url.startswith("file:"):
path = url_to_path(url)
editable = direct_url_data.get("dir_info", {}).get("editable", False)
return f"{'-e ' if editable else ''}{path}"
return ""
def get_pep582_path(project: Project) -> str:
from pdm.compat import resources_open_binary
script_dir = Path(__file__).parent.parent / "pep582"
if script_dir.exists():
return str(script_dir)
script_dir = project.global_config.config_file.parent / "pep582"
if script_dir.joinpath("sitecustomize.py").exists():
return str(script_dir)
script_dir.mkdir(parents=True, exist_ok=True)
with resources_open_binary("pdm.pep582", "sitecustomize.py") as f:
script_dir.joinpath("sitecustomize.py").write_bytes(f.read())
return str(script_dir)
def use_venv(project: Project, name: str) -> None:
from pdm.cli.commands.venv.utils import get_venv_with_name
from pdm.environments import PythonEnvironment
venv = get_venv_with_name(project, cast(str, name))
project.core.ui.info(f"In virtual environment: [success]{venv.root}[/]", verbosity=termui.Verbosity.DETAIL)
project.environment = PythonEnvironment(project, python=str(venv.interpreter))
def normalize_pattern(pattern: str) -> str:
"""Normalize a pattern to a valid name for a package."""
return re.sub(r"[^A-Za-z0-9*?]+", "-", pattern).lower()