Skip to content

Commit e615111

Browse files
committed
Fix #83: prune stale [tool.uv.sources] entries
mxdev tags each source entry it writes with a '# managed by mxdev' marker comment and reconciles them on every run: managed entries whose package was removed from mx.ini (or switched to install-mode = skip) are pruned, while user-defined sources without the marker are left untouched.
1 parent 5c27c5b commit e615111

3 files changed

Lines changed: 52 additions & 22 deletions

File tree

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
fully managed by mxdev. Disable via `uv-constraint-dependencies = false` in `[settings]`.
99
Inspired by Maik Derstappen's `uv-import-constraint-dependencies`. [jensens]
1010

11+
- Fix #83: Remove stale entries from `[tool.uv.sources]` when a package is removed from
12+
`mx.ini` (or switched to `install-mode = skip`). mxdev tags the source entries it writes
13+
with a `# managed by mxdev` marker and prunes managed entries that are no longer
14+
configured, while leaving user-defined sources untouched. [jensens]
15+
1116

1217
## 5.3.2 (2026-05-30)
1318

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,9 @@ managed = true
311311
```
312312

313313
mxdev will automatically inject the local VCS paths of your developed packages into `[tool.uv.sources]`.
314+
Each entry mxdev writes is tagged with a `# managed by mxdev` marker comment. On every run mxdev
315+
reconciles these: entries whose package was removed from `mx.ini` (or switched to `install-mode = skip`)
316+
are pruned, while any user-defined sources you added yourself are left untouched.
314317

315318
Any `version-overrides` declared in `mx.ini` are also written to `[tool.uv] override-dependencies`. For example:
316319
```ini

src/mxdev/uv.py

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from mxdev.hooks import Hook
33
from mxdev.state import State
44
from pathlib import Path
5+
from typing import Any
56
from typing import TYPE_CHECKING
67

78
import logging
@@ -15,6 +16,17 @@
1516

1617
logger = logging.getLogger("mxdev")
1718

19+
# Trailing comment used to tag the [tool.uv.sources] entries mxdev writes, so
20+
# stale ones can be pruned without touching user-defined sources.
21+
_UV_SOURCE_MARKER = "managed by mxdev"
22+
23+
24+
def _is_mxdev_managed_source(value: Any) -> bool:
25+
"""Return True if a [tool.uv.sources] value carries the mxdev marker comment."""
26+
trivia = getattr(value, "trivia", None)
27+
comment = getattr(trivia, "comment", "") or ""
28+
return _UV_SOURCE_MARKER in comment
29+
1830

1931
def _constraints_to_uv(constraints: list[str]) -> list[tuple[str, str]]:
2032
"""Turn resolved constraint lines into ordered uv array items.
@@ -138,31 +150,35 @@ def _update_pyproject(self, doc: "tomlkit.TOMLDocument", state: State) -> None:
138150
write_constraints = to_bool(settings.get("uv-constraint-dependencies", "true"))
139151
constraint_items = _constraints_to_uv(state.constraints) if write_constraints else []
140152

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
153+
# Packages mxdev manages as path sources. A package in "skip" install-mode
154+
# gets no source entry (and an existing one is pruned below).
155+
managed_sources = {name: data for name, data in packages.items() if data.get("install-mode") != "skip"}
147156

148157
if "tool" not in doc:
149158
doc.add("tool", tomlkit.table())
150159
if "uv" not in doc["tool"]:
151160
doc["tool"]["uv"] = tomlkit.table()
152-
153-
# 1. Update [tool.uv.sources]
154-
if packages:
155-
if "sources" not in doc["tool"]["uv"]:
156-
doc["tool"]["uv"]["sources"] = tomlkit.table()
157-
158-
uv_sources = doc["tool"]["uv"]["sources"]
159-
160-
for pkg_name, pkg_data in packages.items():
161+
uv = doc["tool"]["uv"]
162+
163+
# 1. Reconcile [tool.uv.sources]: write the current managed sources and
164+
# prune mxdev-managed entries whose package was removed from mx.ini.
165+
# Foreign (user-defined) sources without the mxdev marker are never
166+
# touched.
167+
existing_sources = uv.get("sources")
168+
if managed_sources or existing_sources is not None:
169+
if existing_sources is None:
170+
uv["sources"] = tomlkit.table()
171+
uv_sources = uv["sources"]
172+
173+
# Prune stale mxdev-managed entries.
174+
for key in list(uv_sources.keys()):
175+
if _is_mxdev_managed_source(uv_sources[key]) and key not in managed_sources:
176+
del uv_sources[key]
177+
178+
# Write / refresh current managed entries, each tagged with the marker.
179+
for pkg_name, pkg_data in managed_sources.items():
161180
install_mode = pkg_data.get("install-mode", "editable")
162181

163-
if install_mode == "skip":
164-
continue
165-
166182
target_dir = Path(pkg_data.get("target", "sources"))
167183
package_path = target_dir / pkg_name
168184
subdirectory = pkg_data.get("subdirectory", "")
@@ -186,13 +202,19 @@ def _update_pyproject(self, doc: "tomlkit.TOMLDocument", state: State) -> None:
186202
source_table.append("editable", False)
187203

188204
uv_sources[pkg_name] = source_table
205+
uv_sources[pkg_name].trivia.comment_ws = " "
206+
uv_sources[pkg_name].trivia.comment = f"# {_UV_SOURCE_MARKER}"
207+
208+
# Drop the table entirely if reconciliation emptied it.
209+
if len(uv_sources) == 0:
210+
del uv["sources"]
189211

190212
# 2. Update [tool.uv] override-dependencies from version-overrides
191213
if overrides:
192214
override_array = tomlkit.array()
193215
override_array.extend(overrides.values())
194216
override_array.multiline(True)
195-
doc["tool"]["uv"]["override-dependencies"] = override_array
217+
uv["override-dependencies"] = override_array
196218

197219
# 3. Update [tool.uv] constraint-dependencies from resolved constraints
198220
if write_constraints:
@@ -205,7 +227,7 @@ def _update_pyproject(self, doc: "tomlkit.TOMLDocument", state: State) -> None:
205227
constraint_array.add_line(comment=text)
206228
else:
207229
constraint_array.add_line(text)
208-
doc["tool"]["uv"]["constraint-dependencies"] = constraint_array
209-
elif "constraint-dependencies" in doc["tool"]["uv"]:
230+
uv["constraint-dependencies"] = constraint_array
231+
elif "constraint-dependencies" in uv:
210232
# Resolved set is empty: drop a stale mxdev-managed array.
211-
del doc["tool"]["uv"]["constraint-dependencies"]
233+
del uv["constraint-dependencies"]

0 commit comments

Comments
 (0)