Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## 5.3.0 (unreleased)

<!-- Add future changes here -->
- Feature: The `uv` hook now also propagates `version-overrides` from `mx.ini` to `[tool.uv] override-dependencies` in `pyproject.toml`. This keeps version pins in sync between mxdev and uv-managed projects.
[frapell]


## 5.2.0 (2026-05-28)
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,22 @@ managed = true

mxdev will automatically inject the local VCS paths of your developed packages into `[tool.uv.sources]`.

Any `version-overrides` declared in `mx.ini` are also written to `[tool.uv] override-dependencies`. For example:
```ini
[settings]
version-overrides =
baz.baaz==1.9.32
somepackage==3.0.0
```
results in:
```toml
[tool.uv]
override-dependencies = [
"baz.baaz==1.9.32",
"somepackage==3.0.0",
]
```

This allows you to seamlessly use `uv sync` or `uv run` with the packages mxdev has checked out for you, without needing to use `requirements-mxdev.txt`.

To disable this feature, you can either remove the `managed = true` flag from your `pyproject.toml`, or explicitly set it to `false`:
Expand Down
74 changes: 43 additions & 31 deletions src/mxdev/uv.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,45 +100,57 @@ def _update_pyproject(self, doc: "tomlkit.TOMLDocument", state: State) -> None:
"""Modify the pyproject.toml document based on mxdev state."""
import tomlkit

if not state.configuration.packages:
packages = state.configuration.packages
overrides = state.configuration.overrides

if not packages and not overrides:
return

# 1. Update [tool.uv.sources]
if "tool" not in doc:
doc.add("tool", tomlkit.table())
if "uv" not in doc["tool"]:
doc["tool"]["uv"] = tomlkit.table()
if "sources" not in doc["tool"]["uv"]:
doc["tool"]["uv"]["sources"] = tomlkit.table()

uv_sources = doc["tool"]["uv"]["sources"]

for pkg_name, pkg_data in state.configuration.packages.items():
install_mode = pkg_data.get("install-mode", "editable")

if install_mode == "skip":
continue

target_dir = Path(pkg_data.get("target", "sources"))
package_path = target_dir / pkg_name
subdirectory = pkg_data.get("subdirectory", "")
if subdirectory:
package_path = package_path / subdirectory

try:
if package_path.is_absolute():
rel_path = package_path.relative_to(Path.cwd()).as_posix()
else:
# 1. Update [tool.uv.sources]
if packages:
if "sources" not in doc["tool"]["uv"]:
doc["tool"]["uv"]["sources"] = tomlkit.table()

uv_sources = doc["tool"]["uv"]["sources"]

for pkg_name, pkg_data in packages.items():
install_mode = pkg_data.get("install-mode", "editable")

if install_mode == "skip":
continue

target_dir = Path(pkg_data.get("target", "sources"))
package_path = target_dir / pkg_name
subdirectory = pkg_data.get("subdirectory", "")
if subdirectory:
package_path = package_path / subdirectory

try:
if package_path.is_absolute():
rel_path = package_path.relative_to(Path.cwd()).as_posix()
else:
rel_path = package_path.as_posix()
except ValueError:
rel_path = package_path.as_posix()
except ValueError:
rel_path = package_path.as_posix()

source_table = tomlkit.inline_table()
source_table.append("path", rel_path)
source_table = tomlkit.inline_table()
source_table.append("path", rel_path)

if install_mode == "editable":
source_table.append("editable", True)
elif install_mode == "fixed":
source_table.append("editable", False)

if install_mode == "editable":
source_table.append("editable", True)
elif install_mode == "fixed":
source_table.append("editable", False)
uv_sources[pkg_name] = source_table

uv_sources[pkg_name] = source_table
# 2. Update [tool.uv] override-dependencies from version-overrides
if overrides:
override_array = tomlkit.array()
override_array.extend(overrides.values())
override_array.multiline(True)
doc["tool"]["uv"]["override-dependencies"] = override_array
122 changes: 122 additions & 0 deletions tests/test_uv.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,128 @@ def test_update_pyproject_respects_install_modes(tmp_path, monkeypatch):
assert "skip-pkg" not in sources


def test_update_pyproject_writes_version_overrides(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
hook = UvPyprojectUpdater()

mx_ini = """
[settings]
version-overrides =
baz.baaz==1.9.32
somepackage==3.0.0

[pkg1]
url = https://example.com/pkg1.git
target = sources
install-mode = editable
"""
(tmp_path / "mx.ini").write_text(mx_ini.strip())
config = Configuration("mx.ini")
state = State(config)

initial_toml = """
[project]
name = "test"
dependencies = []

[tool.uv]
managed = true
"""
(tmp_path / "pyproject.toml").write_text(initial_toml.strip())

hook.write(state)

doc = tomlkit.parse((tmp_path / "pyproject.toml").read_text())
overrides = doc["tool"]["uv"]["override-dependencies"]
assert list(overrides) == ["baz.baaz==1.9.32", "somepackage==3.0.0"]


def test_update_pyproject_replaces_existing_version_overrides(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
hook = UvPyprojectUpdater()

mx_ini = """
[settings]
version-overrides =
newpkg==2.0.0
"""
(tmp_path / "mx.ini").write_text(mx_ini.strip())
config = Configuration("mx.ini")
state = State(config)

initial_toml = """
[project]
name = "test"
dependencies = []

[tool.uv]
managed = true
override-dependencies = ["stalepkg==0.1.0"]
"""
(tmp_path / "pyproject.toml").write_text(initial_toml.strip())

hook.write(state)

doc = tomlkit.parse((tmp_path / "pyproject.toml").read_text())
overrides = doc["tool"]["uv"]["override-dependencies"]
assert list(overrides) == ["newpkg==2.0.0"]


def test_update_pyproject_no_overrides_no_packages_skips(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
hook = UvPyprojectUpdater()

(tmp_path / "mx.ini").write_text("[settings]")
config = Configuration("mx.ini")
state = State(config)

initial_toml = """
[project]
name = "test"
dependencies = []

[tool.uv]
managed = true
"""
(tmp_path / "pyproject.toml").write_text(initial_toml.strip())

hook.write(state)

doc = tomlkit.parse((tmp_path / "pyproject.toml").read_text())
assert "override-dependencies" not in doc["tool"]["uv"]
assert "sources" not in doc["tool"]["uv"]


def test_update_pyproject_overrides_only_no_packages(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
hook = UvPyprojectUpdater()

mx_ini = """
[settings]
version-overrides =
onlyoverride==1.0.0
"""
(tmp_path / "mx.ini").write_text(mx_ini.strip())
config = Configuration("mx.ini")
state = State(config)

initial_toml = """
[project]
name = "test"
dependencies = []

[tool.uv]
managed = true
"""
(tmp_path / "pyproject.toml").write_text(initial_toml.strip())

hook.write(state)

doc = tomlkit.parse((tmp_path / "pyproject.toml").read_text())
assert list(doc["tool"]["uv"]["override-dependencies"]) == ["onlyoverride==1.0.0"]
assert "sources" not in doc["tool"]["uv"]


def test_update_pyproject_idempotency(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
hook = UvPyprojectUpdater()
Expand Down
Loading