index.toml-adapter-python 0.1.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.
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Discover uv workspace members and explicitly local Python requirements."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from urllib.parse import unquote, urlsplit
|
|
5
|
+
|
|
6
|
+
from index_toml_contracts import read_workspace_toml
|
|
7
|
+
from index_toml_contracts.workspace import (
|
|
8
|
+
WorkspaceDependency, WorkspaceDiagnostic, WorkspaceMember, WorkspaceSnapshot,
|
|
9
|
+
)
|
|
10
|
+
from packaging.requirements import Requirement
|
|
11
|
+
from packaging.utils import canonicalize_name
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _group_requirements(groups, name, chain=()):
|
|
15
|
+
if name in chain:
|
|
16
|
+
raise ValueError(f"Dependency group cycle: {' -> '.join((*chain, name))}")
|
|
17
|
+
for entry in groups[name]:
|
|
18
|
+
if isinstance(entry, str):
|
|
19
|
+
yield entry
|
|
20
|
+
elif isinstance(entry, dict) and set(entry) == {"include-group"}:
|
|
21
|
+
target = canonicalize_name(entry["include-group"])
|
|
22
|
+
yield from _group_requirements(groups, target, (*chain, name))
|
|
23
|
+
else:
|
|
24
|
+
raise ValueError(f"Invalid dependency group entry in {name!r}: {entry!r}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PythonWorkspaceAdapter:
|
|
28
|
+
name = "python"
|
|
29
|
+
ecosystem = "pypi"
|
|
30
|
+
normalize_name = staticmethod(canonicalize_name)
|
|
31
|
+
|
|
32
|
+
def discover(self, root: Path) -> WorkspaceSnapshot:
|
|
33
|
+
root = root.resolve()
|
|
34
|
+
manifest = "pyproject.toml"
|
|
35
|
+
try:
|
|
36
|
+
root_doc = read_workspace_toml(root / manifest)
|
|
37
|
+
uv = root_doc.get("tool", {}).get("uv", {})
|
|
38
|
+
workspace = uv.get("workspace", {})
|
|
39
|
+
if "workspace" not in uv and "project" not in root_doc:
|
|
40
|
+
raise ValueError("Expected a Python project or uv workspace")
|
|
41
|
+
excluded = {
|
|
42
|
+
path.resolve() for pattern in workspace.get("exclude", []) for path in root.glob(pattern)
|
|
43
|
+
}
|
|
44
|
+
directories = {
|
|
45
|
+
path.resolve() for pattern in workspace.get("members", [])
|
|
46
|
+
for path in root.glob(pattern) if path.is_dir()
|
|
47
|
+
} - excluded
|
|
48
|
+
if "project" in root_doc and (uv.get("package") is not False or "workspace" not in uv):
|
|
49
|
+
directories.add(root)
|
|
50
|
+
documents = {}
|
|
51
|
+
members = []
|
|
52
|
+
by_name = {}
|
|
53
|
+
for directory in sorted(directories):
|
|
54
|
+
manifest = (directory / "pyproject.toml").relative_to(root).as_posix()
|
|
55
|
+
doc = root_doc if directory == root else read_workspace_toml(root / manifest)
|
|
56
|
+
name = doc["project"]["name"]
|
|
57
|
+
identity = self.normalize_name(name)
|
|
58
|
+
member = WorkspaceMember(name, identity, directory.relative_to(root).as_posix(), manifest)
|
|
59
|
+
if identity in by_name:
|
|
60
|
+
raise ValueError(f"Ambiguous Python distribution {name!r}: {by_name[identity].manifest}")
|
|
61
|
+
by_name[identity] = member
|
|
62
|
+
documents[member.path] = doc
|
|
63
|
+
members.append(member)
|
|
64
|
+
|
|
65
|
+
edges = []
|
|
66
|
+
paths = set(documents)
|
|
67
|
+
inherited = uv.get("sources", {})
|
|
68
|
+
for member in members:
|
|
69
|
+
manifest = member.manifest
|
|
70
|
+
doc = documents[member.path]
|
|
71
|
+
project = doc["project"]
|
|
72
|
+
sources = {canonicalize_name(key): (root, value) for key, value in inherited.items()}
|
|
73
|
+
directory = root / member.path
|
|
74
|
+
sources.update({
|
|
75
|
+
canonicalize_name(key): (directory, value)
|
|
76
|
+
for key, value in doc.get("tool", {}).get("uv", {}).get("sources", {}).items()
|
|
77
|
+
})
|
|
78
|
+
requirements = {"runtime": project.get("dependencies", [])}
|
|
79
|
+
requirements.update({
|
|
80
|
+
f"extra:{key}": value for key, value in project.get("optional-dependencies", {}).items()
|
|
81
|
+
})
|
|
82
|
+
groups = {
|
|
83
|
+
canonicalize_name(key): value for key, value in doc.get("dependency-groups", {}).items()
|
|
84
|
+
}
|
|
85
|
+
requirements.update({
|
|
86
|
+
f"group:{key}": _group_requirements(groups, key) for key in groups
|
|
87
|
+
})
|
|
88
|
+
legacy = doc.get("tool", {}).get("uv", {}).get("dev-dependencies", [])
|
|
89
|
+
if legacy:
|
|
90
|
+
requirements["uv:dev"] = legacy
|
|
91
|
+
for category, entries in sorted(requirements.items()):
|
|
92
|
+
for entry in entries:
|
|
93
|
+
req = Requirement(entry)
|
|
94
|
+
identity = self.normalize_name(req.name)
|
|
95
|
+
base, values = sources.get(identity, (directory, []))
|
|
96
|
+
if req.url:
|
|
97
|
+
url = urlsplit(req.url)
|
|
98
|
+
if url.scheme not in {"", "file"} or url.netloc not in {"", "localhost"}:
|
|
99
|
+
continue
|
|
100
|
+
path = unquote(url.path)
|
|
101
|
+
if Path(path.lstrip("/")).drive:
|
|
102
|
+
path = path.lstrip("/")
|
|
103
|
+
base, values = directory, [{"path": path}]
|
|
104
|
+
if isinstance(values, dict):
|
|
105
|
+
values = [values]
|
|
106
|
+
for source in values:
|
|
107
|
+
target = self._target(root, base, source, identity, by_name, paths)
|
|
108
|
+
if target is not None:
|
|
109
|
+
conditions = [str(req.marker) if req.marker else "", source.get("marker", "")]
|
|
110
|
+
edges.append(WorkspaceDependency(
|
|
111
|
+
member.path, target, category, manifest, entry,
|
|
112
|
+
" and ".join(f"({condition})" for condition in conditions if condition) or None,
|
|
113
|
+
))
|
|
114
|
+
return WorkspaceSnapshot(tuple(members), tuple(edges))
|
|
115
|
+
except (OSError, ValueError, KeyError, TypeError, AttributeError) as error:
|
|
116
|
+
return WorkspaceSnapshot(diagnostics=(WorkspaceDiagnostic("python_manifest_error", manifest, str(error)),))
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _target(root, base, source, identity, by_name, paths):
|
|
120
|
+
if source.get("workspace") is True:
|
|
121
|
+
if identity not in by_name:
|
|
122
|
+
raise ValueError(f"Unresolved workspace dependency {identity!r}")
|
|
123
|
+
return by_name[identity].path
|
|
124
|
+
if "path" in source:
|
|
125
|
+
directory = (base / source["path"]).resolve()
|
|
126
|
+
if not directory.is_relative_to(root):
|
|
127
|
+
return None
|
|
128
|
+
target = directory.relative_to(root).as_posix()
|
|
129
|
+
if target in paths:
|
|
130
|
+
if by_name.get(identity) is None or by_name[identity].path != target:
|
|
131
|
+
raise ValueError(f"Local source {target!r} does not declare {identity!r}")
|
|
132
|
+
return target
|
|
133
|
+
if directory.is_dir():
|
|
134
|
+
raise ValueError(f"Local dependency {identity!r} at {target!r} is not a workspace member")
|
|
135
|
+
if not directory.is_file():
|
|
136
|
+
raise ValueError(f"Local dependency {identity!r} has a missing path {target!r}")
|
|
137
|
+
return None
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: index.toml-adapter-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Workspace discovery adapter for Python
|
|
5
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
11
|
+
Requires-Dist: index-toml-contracts>=0.1.0,<0.2
|
|
12
|
+
Requires-Dist: packaging>=24,<27
|
|
13
|
+
Requires-Python: >=3.10, <3.16
|
|
14
|
+
Project-URL: Repository, https://github.com/groupsum/index.toml
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
index_toml_adapter_python/__init__.py,sha256=5Al8OgKPKKRhTRAtBrLTXih0ViFjDYLEeGGqlY24wxA,115
|
|
2
|
+
index_toml_adapter_python/adapter.py,sha256=HrqYYZ7eHC9wL7E-Gz8UFsXXRphDw215F7i1-bAUoao,6972
|
|
3
|
+
index_toml_adapter_python-0.1.0.dist-info/WHEEL,sha256=3OmCxFA0ffBIsX4x0o8CdfSvZAOvO3mzEG8U_z5TvV4,80
|
|
4
|
+
index_toml_adapter_python-0.1.0.dist-info/METADATA,sha256=539GHZXk1FzedQf1naaR57wymOwPBMlzc4zbq6iKVa0,599
|
|
5
|
+
index_toml_adapter_python-0.1.0.dist-info/RECORD,,
|