From 15c830131b241f4e002b8b4392cb70d35f5dff1f Mon Sep 17 00:00:00 2001 From: Franco Pellegrini Date: Fri, 29 May 2026 10:42:20 +0200 Subject: [PATCH] When declaring 'version-overrides' also update 'pyproject.toml'. refs #85 --- CHANGES.md | 3 +- README.md | 16 +++++++ src/mxdev/uv.py | 74 ++++++++++++++++------------ tests/test_uv.py | 122 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 32 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 308ed10..d8c4bd3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,8 @@ ## 5.3.0 (unreleased) - +- 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) diff --git a/README.md b/README.md index a5c1164..f2301f7 100644 --- a/README.md +++ b/README.md @@ -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`: diff --git a/src/mxdev/uv.py b/src/mxdev/uv.py index 85c678f..6695902 100644 --- a/src/mxdev/uv.py +++ b/src/mxdev/uv.py @@ -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 diff --git a/tests/test_uv.py b/tests/test_uv.py index 23b0816..3771c6b 100644 --- a/tests/test_uv.py +++ b/tests/test_uv.py @@ -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()