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,5 @@
1
+ """Python workspace adapter."""
2
+
3
+ from .adapter import PythonWorkspaceAdapter
4
+
5
+ __all__ = ["PythonWorkspaceAdapter"]
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.8
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any