tgzr.package_management 0.101__tar.gz → 0.102__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tgzr.package_management
3
- Version: 0.101
3
+ Version: 0.102
4
4
  Summary: tgzr package_management engine
5
5
  Project-URL: Documentation, https://github.com/open-tgzr/tgzr.package_management#readme
6
6
  Project-URL: Issues, https://github.com/open-tgzr/tgzr.package_management/issues
@@ -20,6 +20,9 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
20
20
  Requires-Python: >=3.9
21
21
  Requires-Dist: hatch
22
22
  Requires-Dist: importlib-metadata
23
+ Requires-Dist: msgspec
24
+ Requires-Dist: packaging
25
+ Requires-Dist: toml
23
26
  Requires-Dist: uv
24
27
  Description-Content-Type: text/markdown
25
28
 
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["hatchling", "hatch-vcs"]
2
+ requires = ["hatchling", "hatch-vcs"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
@@ -10,9 +10,7 @@ readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = "GPL-3.0-or-later"
12
12
  keywords = []
13
- authors = [
14
- { name = "Dee", email = "dee.sometech@gmail.com" },
15
- ]
13
+ authors = [{ name = "Dee", email = "dee.sometech@gmail.com" }]
16
14
  classifiers = [
17
15
  "Development Status :: 4 - Beta",
18
16
  "Programming Language :: Python",
@@ -26,8 +24,11 @@ classifiers = [
26
24
  ]
27
25
 
28
26
  dependencies = [
27
+ "packaging",
29
28
  "uv",
30
29
  "hatch",
30
+ 'msgspec',
31
+ "toml",
31
32
  "importlib-metadata", # needed to support modern Distribution api with py3.9
32
33
  ]
33
34
 
@@ -55,9 +56,7 @@ artifacts = ['_version.py']
55
56
  packages = ["src/tgzr"]
56
57
 
57
58
  [tool.hatch.envs.types]
58
- extra-dependencies = [
59
- "mypy>=1.0.0",
60
- ]
59
+ extra-dependencies = ["mypy>=1.0.0"]
61
60
 
62
61
  [tool.hatch.envs.types.scripts]
63
62
  check = "mypy --install-types --non-interactive {args:src/tgzr/package_management tests}"
@@ -66,19 +65,14 @@ check = "mypy --install-types --non-interactive {args:src/tgzr/package_managemen
66
65
  source_pkgs = ["tgzr.package_management", "tests"]
67
66
  branch = true
68
67
  parallel = true
69
- omit = [
70
- "src/tgzr/package_management/_version.py",
71
- ]
68
+ omit = ["src/tgzr/package_management/_version.py"]
72
69
 
73
70
  [tool.coverage.paths]
74
- tgzr.package_management = ["src/tgzr/package_management", "*/tgzr.package_management/src/tgzr/package_management"]
71
+ tgzr.package_management = [
72
+ "src/tgzr/package_management",
73
+ "*/tgzr.package_management/src/tgzr/package_management",
74
+ ]
75
75
  tests = ["tests", "*/tgzr/package_management/tests"]
76
76
 
77
77
  [tool.coverage.report]
78
- exclude_lines = [
79
- "no cov",
80
- "if __name__ == .__main__.:",
81
- "if TYPE_CHECKING:",
82
- ]
83
-
84
-
78
+ exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.101'
32
- __version_tuple__ = version_tuple = (0, 101)
31
+ __version__ = version = '0.102'
32
+ __version_tuple__ = version_tuple = (0, 102)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib_metadata import Distribution as _CoreDistribution
4
+
5
+
6
+ class Distribution(_CoreDistribution):
7
+ # TODO: add a better API for location
8
+ pass
@@ -1,14 +1,32 @@
1
1
  from __future__ import annotations
2
+ from typing import Literal
2
3
 
3
4
  import os
4
5
  import platform
5
6
  from pathlib import Path
6
7
 
7
8
  from .venv import Venv
9
+ from .workspace import Workspace
8
10
 
9
11
 
10
12
  class PackageManager:
11
13
  def __init__(self, root: Path) -> None:
14
+ """
15
+ The venvs and workspace created by this manager will
16
+ be located in their group folder:
17
+ root/
18
+ group1/
19
+ venv1
20
+ venv2
21
+ workspace1
22
+ group2/
23
+ venv3
24
+ venv4
25
+ workspace2
26
+ group3/
27
+ workspace3
28
+ """
29
+
12
30
  self._root = root
13
31
 
14
32
  @property
@@ -73,3 +91,24 @@ class PackageManager:
73
91
  venv.create(prompt, clear_existing=exist_ok)
74
92
  venv.install_uv()
75
93
  return venv
94
+
95
+ def get_workspace_path(self, workspace_name: str, group: str) -> Path:
96
+ return self.root / group / workspace_name
97
+
98
+ def get_workspace(self, workspace_name: str, group: str) -> Workspace:
99
+ return Workspace(self.get_workspace_path(workspace_name, group))
100
+
101
+ def create_workspace(
102
+ self,
103
+ workspace_name: str,
104
+ group: str,
105
+ description: str | None = None,
106
+ python_version: str | None = None,
107
+ vcs: Literal["git", "none"] | None = None,
108
+ exist_ok: bool = False,
109
+ ):
110
+ workspace = self.get_workspace(workspace_name, group)
111
+ if workspace.exists() and not exist_ok:
112
+ raise ValueError(f"The workspace {workspace.path} already exists!")
113
+
114
+ workspace.create(description, python_version, vcs)
@@ -124,7 +124,7 @@ class PluginManager(Generic[PluginType]):
124
124
  plugin_or_list_of_plugins = loaded() # type: ignore
125
125
  except Exception as err:
126
126
  raise ValueError(
127
- f"Error while executing callable entry point value (ep={entry_point}): {err}"
127
+ f"Error while executing callable entry point value (ep={entry_point}): {err} ({loaded=}, {ManagedPluginType=})"
128
128
  )
129
129
  return self._resolve_plugins(
130
130
  loaded=plugin_or_list_of_plugins, entry_point=entry_point
@@ -0,0 +1,262 @@
1
+ """
2
+
3
+ API to edit pyproject.toml files.
4
+
5
+ Originally from https://jcristharif.com/msgspec/examples/pyproject-toml.html
6
+
7
+ """
8
+
9
+ from __future__ import annotations
10
+ from typing import Any
11
+
12
+ from pathlib import Path
13
+
14
+ import msgspec
15
+
16
+
17
+ class Base(
18
+ msgspec.Struct,
19
+ omit_defaults=True,
20
+ forbid_unknown_fields=True,
21
+ rename="kebab",
22
+ ):
23
+ """A base class holding some common settings.
24
+
25
+ - We set ``omit_defaults = True`` to omit any fields containing only their
26
+ default value from the output when encoding.
27
+ - We set ``forbid_unknown_fields = True`` to error nicely if an unknown
28
+ field is present in the input TOML. This helps catch typo errors early,
29
+ and is also required per PEP 621.
30
+ - We set ``rename = "kebab"`` to rename all fields to use kebab case when
31
+ encoding/decoding, as this is the convention used in pyproject.toml. For
32
+ example, this will rename ``requires_python`` to ``requires-python``.
33
+ """
34
+
35
+ pass
36
+
37
+
38
+ class BuildSystem(Base):
39
+ requires: list[str] = []
40
+ build_backend: str | None = None
41
+ backend_path: list[str] = []
42
+
43
+
44
+ class Readme(Base):
45
+ file: str | None = None
46
+ text: str | None = None
47
+ content_type: str | None = None
48
+
49
+
50
+ class License(Base):
51
+ file: str | None = None
52
+ text: str | None = None
53
+
54
+
55
+ class Contributor(Base):
56
+ name: str | None = None
57
+ email: str | None = None
58
+
59
+
60
+ class Project(Base):
61
+ name: str | None = None
62
+ version: str | None = None
63
+ description: str | None = None
64
+ readme: str | Readme | None = None
65
+ license: str | License | None = None
66
+ authors: list[Contributor] = []
67
+ maintainers: list[Contributor] = []
68
+ keywords: list[str] | None = None
69
+ classifiers: list[str] = []
70
+ urls: dict[str, str] = {}
71
+ requires_python: str | None = None
72
+ dependencies: list[str] = []
73
+ optional_dependencies: dict[str, list[str]] = {}
74
+ scripts: dict[str, str] = {}
75
+ gui_scripts: dict[str, str] = {}
76
+ entry_points: dict[str, dict[str, str]] = {}
77
+ dynamic: list[str] = []
78
+
79
+
80
+ class ToolUVSource(Base):
81
+ index: str | None = None
82
+ workspace: bool | None = False
83
+ path: str | None = None
84
+ editable: bool | None = None
85
+
86
+
87
+ class ToolUVIndex(Base):
88
+ name: str
89
+ url: str
90
+ explicit: bool | None = None
91
+
92
+
93
+ class ToolUVWorkspace(Base):
94
+ members: list[str] = []
95
+
96
+
97
+ class ToolUV(Base):
98
+ sources: dict[str, ToolUVSource] = {}
99
+ index: list[ToolUVIndex] = []
100
+ workspace: ToolUVWorkspace | None = None
101
+
102
+
103
+ class ToolHatchMetadata(Base):
104
+ allow_custom_classifiers: bool | None = False
105
+
106
+
107
+ class ToolHatchVersion(Base):
108
+ path: str | None = None
109
+ source: str | None = None
110
+ fallback_version: str | None = None
111
+
112
+
113
+ class ToolHatchBuildTarget(Base):
114
+ artifacts: list[str] = []
115
+ packages: list[str] = []
116
+ hooks: dict[str, Any] = {}
117
+
118
+
119
+ class ToolHatchBuild(Base):
120
+ artifacts: list[str] = []
121
+ packages: list[str] = []
122
+ hooks: dict[str, Any] = {}
123
+ targets: dict[str, ToolHatchBuildTarget] = {}
124
+
125
+
126
+ class ToolHatch(Base):
127
+ metadata: dict[str, str | bool] = {}
128
+ envs: dict[str, Any] | None = None
129
+ version: ToolHatchVersion | None = None
130
+ build: ToolHatchBuild | None = None
131
+ # publish: dict[str, str] | None = {}
132
+
133
+
134
+ class ToolCoverageRun(Base):
135
+ source_pkgs: list[str] = msgspec.field(default=[], name="source_pkgs")
136
+ branch: bool = True
137
+ parallel: bool = True
138
+ omit: list[str] = []
139
+
140
+
141
+ class ToolCoverage(Base):
142
+ run: Any | None = None
143
+ paths: dict[str, Any] = {}
144
+ report: Any | None = None
145
+
146
+
147
+ class ToolSetuptoolsScme(Base):
148
+ version_file: str | None = msgspec.field(default=None, name="version_file")
149
+
150
+
151
+ class ToolRuffLintISort(Base):
152
+ force_sort_within_sections: bool = False
153
+
154
+
155
+ class ToolRuffLint(Base):
156
+ isort: ToolRuffLintISort | None = None
157
+
158
+
159
+ class ToolRuff(Base):
160
+ lint: ToolRuffLint | None = None
161
+
162
+
163
+ class ToolMypy(Base):
164
+ check_untyped_defs: bool = False
165
+ disallow_untyped_defs: bool = False
166
+ disallow_untyped_calls: bool = False
167
+ overrides: list[Any] = []
168
+
169
+
170
+ class Tools(Base):
171
+ """
172
+ We can't specify a Tool subclass depending on the Tool name,
173
+ so this Tool struct needs to handle ALL the tools we need at
174
+ once :/
175
+ Sucks, but good enough for me :p
176
+ """
177
+
178
+ hatch: ToolHatch | None = None
179
+ uv: ToolUV | None = None
180
+ coverage: ToolCoverage | None = None
181
+ setuptools_scm: ToolSetuptoolsScme | None = msgspec.field(
182
+ default=None, name="setuptools_scm"
183
+ )
184
+ ruff: ToolRuff | None = None
185
+ mypy: ToolMypy | None = None
186
+ setuptools: Any | None = None
187
+
188
+
189
+ class PyProject(Base):
190
+
191
+ build_system: BuildSystem | None = None
192
+ project: Project | None = None
193
+ dependency_groups: dict[str, list[str]] = {}
194
+ tool: Tools | None = None
195
+
196
+ def set_filepath(self, filepath: Path):
197
+ self._filepath = filepath
198
+
199
+
200
+ def load_pyproject(filepath: Path | str):
201
+ filepath = Path(filepath)
202
+ with filepath.open() as fp:
203
+ data = fp.read()
204
+ pyproject = msgspec.toml.decode(data, type=PyProject)
205
+ return pyproject
206
+
207
+
208
+ def save_pyproject(pyproject: PyProject, filepath: Path | str):
209
+ filepath = Path(filepath)
210
+ data = msgspec.toml.encode(pyproject)
211
+ with filepath.open("wb") as fp:
212
+ fp.write(data)
213
+
214
+
215
+ def test():
216
+ import toml
217
+ import json
218
+ import rich
219
+ import dictdiffer
220
+
221
+ stats = dict(
222
+ tested=0,
223
+ failed=0,
224
+ )
225
+
226
+ def assert_roundtrip(path: str | Path):
227
+ stats["tested"] += 1
228
+ path = Path(path)
229
+ try:
230
+ pp = load_pyproject(path)
231
+ except Exception as err:
232
+ raise Exception(f"pyproject load failed for {path}: {err}")
233
+ pp_dict = json.loads(msgspec.json.encode(pp))
234
+ toml_dict = toml.load(path)
235
+ try:
236
+ assert pp_dict == toml_dict
237
+ except AssertionError:
238
+ stats["failed"] += 1
239
+ if 0:
240
+ print(10 * "##")
241
+ rich.print(pp_dict)
242
+ print(10 * "--")
243
+ rich.print(toml_dict)
244
+
245
+ print(f"Diff found for {str(path)}:")
246
+ for diff in dictdiffer.diff(pp_dict, toml_dict):
247
+ print(diff)
248
+
249
+ print(f"Roundtrip failed for {str(path)}!")
250
+
251
+ root_path = Path("/home/dee/DEV/_OPEN-TGZR_")
252
+ for folder in root_path.iterdir():
253
+ pyproject_filename = folder / "pyproject.toml"
254
+ if pyproject_filename.exists():
255
+ print("Testing", pyproject_filename)
256
+ assert_roundtrip(pyproject_filename)
257
+
258
+ print(f"Stats: {stats}")
259
+
260
+
261
+ if __name__ == "__main__":
262
+ test()
@@ -194,26 +194,26 @@ class Venv:
194
194
  ret.append(dist)
195
195
  return ret
196
196
 
197
- def get_packages_slow(self) -> list[tuple[str, str, str]]:
198
- stdout, stderr = self.get_cmd_output(
199
- cmd_name="uv",
200
- # cmd_args=["pip", "tree", "-d", "0"],
201
- cmd_args=["pip", "list", "--format", "json"],
202
- )
203
- try:
204
- data = json.loads(stdout)
205
- except Exception as err:
206
- raise ValueError(f"Error parsing pip list output: {err}")
207
- ret = []
208
- for entry in data:
209
- ret.append(
210
- (
211
- entry["name"],
212
- entry["version"],
213
- entry.get("editable_project_location"),
214
- )
215
- )
216
- return ret
197
+ # def get_packages_slow(self) -> list[tuple[str, str, str]]:
198
+ # stdout, stderr = self.get_cmd_output(
199
+ # cmd_name="uv",
200
+ # # cmd_args=["pip", "tree", "-d", "0"],
201
+ # cmd_args=["pip", "list", "--format", "json"],
202
+ # )
203
+ # try:
204
+ # data = json.loads(stdout)
205
+ # except Exception as err:
206
+ # raise ValueError(f"Error parsing pip list output: {err}")
207
+ # ret = []
208
+ # for entry in data:
209
+ # ret.append(
210
+ # (
211
+ # entry["name"],
212
+ # entry["version"],
213
+ # entry.get("editable_project_location"),
214
+ # )
215
+ # )
216
+ # return ret
217
217
 
218
218
  def get_plugins(
219
219
  self, group_filter: str | None
@@ -230,23 +230,70 @@ class Venv:
230
230
  plugins.append([ep, dist])
231
231
  return plugins
232
232
 
233
- def get_plugins_slow(
234
- self, group_filter: str | None
235
- ) -> list[importlib_metadata.EntryPoint]:
236
- cmd_args = ["studio", "plugins-here", "--format", "json"]
237
- if group_filter:
238
- cmd_args.extend(["--group-filter", group_filter])
239
- stdout, stderr = self.get_cmd_output("tgzr", cmd_args)
240
- # print("???", [stdout, stderr])
241
- stdout = stdout.split(">>> JSON:", 1)[-1]
242
-
243
- data = json.loads(stdout)
244
- # print("-->", data)
245
- ret = []
246
- for entry in data:
247
- # print(entry)
248
- ep = importlib_metadata.EntryPoint(
249
- entry["name"], entry["value"], entry["group"]
250
- )
251
- ret.append(ep)
252
- return ret
233
+ # def get_plugins_slow(
234
+ # self, group_filter: str | None
235
+ # ) -> list[importlib_metadata.EntryPoint]:
236
+ # cmd_args = ["studio", "plugins-here", "--format", "json"]
237
+ # if group_filter:
238
+ # cmd_args.extend(["--group-filter", group_filter])
239
+ # stdout, stderr = self.get_cmd_output("tgzr", cmd_args)
240
+ # # print("???", [stdout, stderr])
241
+ # stdout = stdout.split(">>> JSON:", 1)[-1]
242
+
243
+ # data = json.loads(stdout)
244
+ # # print("-->", data)
245
+ # ret = []
246
+ # for entry in data:
247
+ # # print(entry)
248
+ # ep = importlib_metadata.EntryPoint(
249
+ # entry["name"], entry["value"], entry["group"]
250
+ # )
251
+ # ret.append(ep)
252
+ # return ret
253
+
254
+ def hatch_version_bump(self, package_path: Path, bump_type: str):
255
+ hatch_exe = self.get_exe("hatch")
256
+ subprocess.call(
257
+ [hatch_exe, "version", bump_type],
258
+ cwd=package_path,
259
+ )
260
+
261
+ def hatch_build(
262
+ self,
263
+ package_path: str | Path,
264
+ dist_path: str | Path,
265
+ allow_custom_classifiers=True,
266
+ ):
267
+ env = None
268
+ if allow_custom_classifiers:
269
+ env = os.environ.copy()
270
+ # This is needed to build a package with custom classifiers:
271
+ env["HATCH_METADATA_CLASSIFIERS_NO_VERIFY"] = "1"
272
+
273
+ hatch_exe = self.get_exe("hatch")
274
+ subprocess.call(
275
+ [hatch_exe, "build", "-t", "sdist", dist_path],
276
+ cwd=package_path,
277
+ env=env,
278
+ )
279
+
280
+ def hatch_publish(
281
+ self, package_path: str | Path, dist_path: Path, repo_url: str, **options: str
282
+ ):
283
+ hatch_options = sum([["-o", f"{k}={v}"] for k, v in options.items()], [])
284
+ hatch_exe = self.get_exe("hatch")
285
+ cmd = [
286
+ hatch_exe,
287
+ "publish",
288
+ # "--publisher",
289
+ # "tgzr-pipeline-asset",
290
+ "--repo",
291
+ repo_url,
292
+ *hatch_options,
293
+ *dist_path.iterdir(),
294
+ ]
295
+ # print("--->", cmd)
296
+ subprocess.call(
297
+ cmd,
298
+ cwd=package_path,
299
+ )
@@ -0,0 +1,264 @@
1
+ from __future__ import annotations
2
+ from typing import Literal, Any
3
+
4
+ import os
5
+ from pathlib import Path
6
+ import subprocess
7
+ import subprocess
8
+ import logging
9
+
10
+ from packaging.requirements import Requirement
11
+ import uv
12
+
13
+ from . import pyproject
14
+ from .pyproject import (
15
+ PyProject,
16
+ Project,
17
+ Tools,
18
+ ToolUV,
19
+ ToolUVIndex,
20
+ ToolUVSource,
21
+ ToolUVWorkspace,
22
+ )
23
+ from .venv import Venv
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class Workspace:
29
+ """
30
+ A uv workspace
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ path: Path | str,
36
+ ) -> None:
37
+ self._path = Path(path)
38
+ self._pyproject_filename = self._path / "pyproject.toml"
39
+ self._pyproject: PyProject | None = None
40
+
41
+ @property
42
+ def path(self) -> Path:
43
+ """The Path of the workspace."""
44
+ return self._path
45
+
46
+ @property
47
+ def name(self) -> str:
48
+ """The name of the workspace folder."""
49
+ return self._path.name
50
+
51
+ @property
52
+ def group(self) -> str:
53
+ """The name of the parent folder (sometime representing a group of workspaces)."""
54
+ return self._path.parent.name
55
+
56
+ def exists(self) -> bool:
57
+ """
58
+ Returns True if the folder exists and contains a pyproject.toml
59
+ NB: It could be True for non-workspace folders, we assume you don't mess up your project paths.
60
+ """
61
+ return self._path.exists() and self._pyproject_filename.exists()
62
+
63
+ def venv(self) -> Venv:
64
+ venv_name = ".venv"
65
+ return Venv(self.path / venv_name)
66
+
67
+ def create(
68
+ self,
69
+ description: str | None = None,
70
+ python_version: str | None = None,
71
+ vcs: Literal["git", "none"] | None = None,
72
+ ) -> None:
73
+ description = (
74
+ description
75
+ or "A UV workspace, managed by tgzr.package_management.workspcace."
76
+ )
77
+ descr_args = []
78
+ if description is not None:
79
+ descr_args = ["--description", description]
80
+
81
+ py_args = []
82
+ if python_version is not None:
83
+ py_args = ["-p", python_version]
84
+
85
+ uv_exe = uv.find_uv_bin()
86
+ cmd = [
87
+ uv_exe,
88
+ "init",
89
+ "--no-package",
90
+ "--vcs",
91
+ vcs,
92
+ *py_args,
93
+ "--no-workspace",
94
+ *descr_args,
95
+ "--author-from",
96
+ "auto",
97
+ str(self.path),
98
+ ]
99
+ print(f"Creating workspace {self.path}: {cmd}")
100
+ subprocess.check_call(cmd)
101
+
102
+ @property
103
+ def pyproject(self) -> PyProject:
104
+ if self._pyproject is None:
105
+ self._pyproject = pyproject.load_pyproject(self._pyproject_filename)
106
+ if self.pyproject.tool is None:
107
+ self.pyproject.tool = Tools(uv=ToolUV())
108
+ if self.pyproject.tool.uv is None:
109
+ self.pyproject.tool.uv = ToolUV()
110
+
111
+ return self._pyproject
112
+
113
+ @property
114
+ def tool_uv(self) -> ToolUV:
115
+ """The PyProject.tool.uv configuration."""
116
+ return self.pyproject.tool.uv # type: ignore self.pyproject ensure it's not None!
117
+
118
+ def save_pyproject(self) -> None:
119
+ pyproject.save_pyproject(self.pyproject, self._pyproject_filename)
120
+
121
+ def get_index(self, name: str) -> ToolUVIndex | None:
122
+ for index in self.tool_uv.index:
123
+ if index.name == name:
124
+ return index
125
+
126
+ def ensure_index(self, name: str, url: str, explicit: bool | None = None) -> None:
127
+ """
128
+ Will create or update the index with name `name`.
129
+ """
130
+ index_to_set = ToolUVIndex(name=name, url=url, explicit=explicit)
131
+ found = False
132
+ for index in self.tool_uv.index:
133
+ if index == index_to_set:
134
+ # it is already set exactly as requested
135
+ return
136
+
137
+ if index.name == index_to_set.name:
138
+ found = True
139
+ index.url = index_to_set.url
140
+ index.explicit = index_to_set.explicit
141
+ break
142
+ if not found:
143
+ self.tool_uv.index.append(index_to_set)
144
+ self.save_pyproject()
145
+
146
+ def set_source(
147
+ self,
148
+ source_name: str,
149
+ index_name: str | None = None,
150
+ workspace: bool | None = None,
151
+ path: Path | str | None = None,
152
+ editable: bool | None = None,
153
+ ) -> None:
154
+ """
155
+ Beware: not all combinations of index/workspace/path/editable are valid!
156
+
157
+ When index_name is given, an index with that name must already
158
+ have been defined. You can use `self.ensure_index()` if needed.
159
+ """
160
+ source = self.tool_uv.sources.get(source_name)
161
+ if source is None:
162
+ source = ToolUVSource()
163
+ self.tool_uv.sources[source_name] = source
164
+
165
+ if index_name is not None:
166
+ source.index = index_name
167
+
168
+ if workspace is not None:
169
+ source.workspace = workspace
170
+
171
+ if path is not None:
172
+ source.path = str(path)
173
+
174
+ if editable is not None:
175
+ source.editable = editable
176
+
177
+ self.save_pyproject()
178
+
179
+ def add_dependencies(self, group: str = "", *new_requirements):
180
+ if self.pyproject.project is None:
181
+ self.pyproject.project = Project()
182
+
183
+ if group is None:
184
+ deps = self.pyproject.project.dependencies
185
+ else:
186
+ try:
187
+ deps = self.pyproject.dependency_groups[group]
188
+ except KeyError:
189
+ deps = []
190
+ self.pyproject.dependency_groups[group] = deps
191
+
192
+ to_remove = []
193
+ for new in new_requirements:
194
+ new_req = Requirement(new)
195
+ for old in deps:
196
+ if Requirement(old).name == new_req.name:
197
+ to_remove.append(old)
198
+ break
199
+ deps.append(new)
200
+
201
+ for obsolet in to_remove:
202
+ deps.remove(obsolet)
203
+
204
+ self.save_pyproject()
205
+
206
+ def add_member(self, member: str):
207
+ if self.tool_uv.workspace is None:
208
+ self.tool_uv.workspace = ToolUVWorkspace()
209
+ self.tool_uv.workspace.members.append(member)
210
+ self.save_pyproject()
211
+
212
+ def run(self, console_script_name: str, *args, **extra_env: str):
213
+ uv_exe = uv.find_uv_bin()
214
+ cmd = [
215
+ uv_exe,
216
+ "run",
217
+ # "--python",
218
+ # str(python),
219
+ "--directory",
220
+ str(self.path),
221
+ console_script_name,
222
+ *args,
223
+ ]
224
+ env = None
225
+ if extra_env:
226
+ env = os.environ.copy()
227
+ env.update(extra_env)
228
+
229
+ print(f"Workspace run: {self.path}: {cmd}")
230
+ subprocess.check_call(cmd)
231
+
232
+ def run_python_command(self, command: str) -> None:
233
+ uv_exe = uv.find_uv_bin()
234
+ cmd = [
235
+ uv_exe,
236
+ "run",
237
+ # "--python",
238
+ # str(python),
239
+ "--directory",
240
+ str(self.path),
241
+ "python",
242
+ "-c",
243
+ command,
244
+ ]
245
+ print(f"Workspace run python cmd: {self.path}: {cmd}")
246
+ subprocess.check_call(cmd)
247
+
248
+ def sync(
249
+ self, allow_upgrade: bool = True, allow_custom_classifiers: bool = False
250
+ ) -> None:
251
+ env = None
252
+ if allow_custom_classifiers:
253
+ env = os.environ.copy()
254
+ # This is needed to build the packages with custom classifiers:
255
+ env["HATCH_METADATA_CLASSIFIERS_NO_VERIFY"] = "1"
256
+
257
+ more_options = []
258
+ if allow_upgrade:
259
+ more_options.append("--upgrade")
260
+
261
+ uv_exe = uv.find_uv_bin()
262
+ cmd = [uv_exe, "--project", self.path, "sync", *more_options]
263
+ print(f"Sync workspace {self.path}: {cmd}")
264
+ subprocess.check_call(cmd, env=env)