Skip to content

Commit 1ec5ae4

Browse files
committed
uv hook: write resolved constraints to [tool.uv] constraint-dependencies
1 parent eaffcc0 commit 1ec5ae4

2 files changed

Lines changed: 140 additions & 2 deletions

File tree

‎src/mxdev/uv.py‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from mxdev.config import to_bool
12
from mxdev.hooks import Hook
23
from mxdev.state import State
34
from pathlib import Path
@@ -132,9 +133,17 @@ def _update_pyproject(self, doc: "tomlkit.TOMLDocument", state: State) -> None:
132133

133134
packages = state.configuration.packages
134135
overrides = state.configuration.overrides
136+
settings = state.configuration.settings
135137

136-
if not packages and not overrides:
137-
return
138+
write_constraints = to_bool(settings.get("uv-constraint-dependencies", "true"))
139+
constraint_items = _constraints_to_uv(state.constraints) if write_constraints else []
140+
141+
if not packages and not overrides and not constraint_items:
142+
# Nothing to add. The only reason to continue is to drop a stale
143+
# mxdev-managed constraint-dependencies array when the feature is on.
144+
uv_table = doc.get("tool", {}).get("uv")
145+
if not write_constraints or uv_table is None or "constraint-dependencies" not in uv_table:
146+
return
138147

139148
if "tool" not in doc:
140149
doc.add("tool", tomlkit.table())
@@ -184,3 +193,19 @@ def _update_pyproject(self, doc: "tomlkit.TOMLDocument", state: State) -> None:
184193
override_array.extend(overrides.values())
185194
override_array.multiline(True)
186195
doc["tool"]["uv"]["override-dependencies"] = override_array
196+
197+
# 3. Update [tool.uv] constraint-dependencies from resolved constraints
198+
if write_constraints:
199+
if constraint_items:
200+
constraint_array = tomlkit.array()
201+
constraint_array.multiline(True)
202+
constraint_array.add_line(comment="managed by mxdev - do not edit")
203+
for kind, text in constraint_items:
204+
if kind == "comment":
205+
constraint_array.add_line(comment=text)
206+
else:
207+
constraint_array.add_line(text)
208+
doc["tool"]["uv"]["constraint-dependencies"] = constraint_array
209+
elif "constraint-dependencies" in doc["tool"]["uv"]:
210+
# Resolved set is empty: drop a stale mxdev-managed array.
211+
del doc["tool"]["uv"]["constraint-dependencies"]

‎tests/test_uv.py‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,3 +487,116 @@ def test_constraints_to_uv_filters_and_preserves_order():
487487
def test_constraints_to_uv_empty_input():
488488
assert _constraints_to_uv([]) == []
489489
assert _constraints_to_uv(["\n", "#" * 79 + "\n", " \n"]) == []
490+
491+
492+
def test_writes_constraint_dependencies(tmp_path, monkeypatch):
493+
monkeypatch.chdir(tmp_path)
494+
hook = UvPyprojectUpdater()
495+
(tmp_path / "mx.ini").write_text("[settings]")
496+
config = Configuration("mx.ini")
497+
state = State(config)
498+
state.constraints = [
499+
"# begin constraints from: https://example.com/a.txt\n",
500+
"Zope==6.0\n",
501+
"# AccessControl==7.3 -> mxdev disabled (source)\n",
502+
"# end constraints from: https://example.com/a.txt\n",
503+
]
504+
505+
(tmp_path / "pyproject.toml").write_text(
506+
'[project]\nname = "test"\ndependencies = []\n\n[tool.uv]\nmanaged = true\n'
507+
)
508+
509+
hook.write(state)
510+
511+
content = (tmp_path / "pyproject.toml").read_text()
512+
assert "# managed by mxdev - do not edit" in content
513+
assert "# begin constraints from: https://example.com/a.txt" in content
514+
assert "# AccessControl==7.3 -> mxdev disabled (source)" in content
515+
doc = tomlkit.parse(content)
516+
assert list(doc["tool"]["uv"]["constraint-dependencies"]) == ["Zope==6.0"]
517+
518+
519+
def test_opt_out_disables_constraint_dependencies(tmp_path, monkeypatch):
520+
monkeypatch.chdir(tmp_path)
521+
hook = UvPyprojectUpdater()
522+
(tmp_path / "mx.ini").write_text("[settings]\nuv-constraint-dependencies = false\n")
523+
config = Configuration("mx.ini")
524+
state = State(config)
525+
state.constraints = ["Zope==6.0\n"]
526+
527+
(tmp_path / "pyproject.toml").write_text(
528+
'[project]\nname = "test"\ndependencies = []\n\n[tool.uv]\nmanaged = true\n'
529+
)
530+
531+
hook.write(state)
532+
533+
doc = tomlkit.parse((tmp_path / "pyproject.toml").read_text())
534+
assert "constraint-dependencies" not in doc["tool"]["uv"]
535+
536+
537+
def test_replaces_existing_constraint_dependencies(tmp_path, monkeypatch):
538+
monkeypatch.chdir(tmp_path)
539+
hook = UvPyprojectUpdater()
540+
(tmp_path / "mx.ini").write_text("[settings]")
541+
config = Configuration("mx.ini")
542+
state = State(config)
543+
state.constraints = ["Zope==6.0\n"]
544+
545+
(tmp_path / "pyproject.toml").write_text(
546+
'[project]\nname = "test"\ndependencies = []\n\n'
547+
"[tool.uv]\nmanaged = true\n"
548+
'constraint-dependencies = [\n "OldPin==0.0.1",\n]\n'
549+
)
550+
551+
hook.write(state)
552+
553+
content = (tmp_path / "pyproject.toml").read_text()
554+
assert "OldPin" not in content
555+
doc = tomlkit.parse(content)
556+
assert list(doc["tool"]["uv"]["constraint-dependencies"]) == ["Zope==6.0"]
557+
558+
559+
def test_constraint_dependencies_idempotency(tmp_path, monkeypatch):
560+
monkeypatch.chdir(tmp_path)
561+
hook = UvPyprojectUpdater()
562+
(tmp_path / "mx.ini").write_text("[settings]")
563+
config = Configuration("mx.ini")
564+
state = State(config)
565+
state.constraints = [
566+
"# begin constraints from: https://example.com/a.txt\n",
567+
"Zope==6.0\n",
568+
"AccessControl==7.3\n",
569+
"# end constraints from: https://example.com/a.txt\n",
570+
]
571+
572+
(tmp_path / "pyproject.toml").write_text(
573+
'[project]\nname = "test"\ndependencies = []\n\n[tool.uv]\nmanaged = true\n'
574+
)
575+
576+
hook.write(state)
577+
first = (tmp_path / "pyproject.toml").read_text()
578+
hook.write(state)
579+
second = (tmp_path / "pyproject.toml").read_text()
580+
assert first == second
581+
582+
583+
def test_empty_constraints_removes_stale_managed_array(tmp_path, monkeypatch):
584+
monkeypatch.chdir(tmp_path)
585+
hook = UvPyprojectUpdater()
586+
# A package ensures _update_pyproject does not early-return.
587+
mx_ini = "[settings]\n[pkg1]\nurl = https://example.com/pkg1.git\n" "target = sources\ninstall-mode = editable\n"
588+
(tmp_path / "mx.ini").write_text(mx_ini)
589+
config = Configuration("mx.ini")
590+
state = State(config)
591+
state.constraints = [] # nothing resolved
592+
593+
(tmp_path / "pyproject.toml").write_text(
594+
'[project]\nname = "test"\ndependencies = []\n\n'
595+
"[tool.uv]\nmanaged = true\n"
596+
'constraint-dependencies = [\n "StalePin==9.9.9",\n]\n'
597+
)
598+
599+
hook.write(state)
600+
601+
doc = tomlkit.parse((tmp_path / "pyproject.toml").read_text())
602+
assert "constraint-dependencies" not in doc["tool"]["uv"]

0 commit comments

Comments
 (0)