Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a4e0ab3
build: Add tomlkit as a core dependency for uv support
erral Mar 27, 2026
3f94e9b
feat: Merge UvPyprojectUpdater hook directly into mxdev core
erral Mar 27, 2026
ba6ee86
test: Add proper tests for UvPyprojectUpdater hook with managed=true …
erral Mar 27, 2026
6e3d691
docs: Document UV pyproject updater integration
erral Mar 27, 2026
2670512
fix: Update entrypoint key to 'hook' and attribute contributor in CHA…
erral Mar 27, 2026
5ca157c
test: Add specific test for managed=false skipping
erral Mar 27, 2026
b5b0637
docs: Move and expand UV Pyproject Integration documentation
erral Mar 27, 2026
ca05f18
docs: Remove duplicate UV Pyproject Integration section
erral Mar 27, 2026
c2b3360
test: Rework test_hook_skips_when_pyproject_toml_missing to use tmp_path
erral Mar 27, 2026
645f5f7
Update README.md
erral Mar 28, 2026
bed9e2c
build: Make tomlkit optional and update documentation per PR review
erral Apr 1, 2026
24c505b
refactor: Lazy load tomlkit, use atomic writes, and fix path resolution
erral Apr 1, 2026
0a07ad3
refactor: Remove project.dependencies mutation and clean up dead code
erral Apr 1, 2026
da10744
test: Add missing test coverage and remove obsolete tests
erral Apr 1, 2026
6dedfa7
move imports to the top of the file
erral Apr 1, 2026
415a594
move imports to the top of the file
erral Apr 1, 2026
24ef1bd
Use TYPE_CHECKING for tomlkit type hint
erral Apr 1, 2026
86f69c7
fix: Defer tomlkit import until uv management is confirmed
erral Apr 1, 2026
05df311
fix: Clean up temporary files on write failure
erral Apr 1, 2026
49bb647
test: Remove misleading relative path resolution test
erral Apr 1, 2026
2860b1b
fix: Defer tomlkit import using tomllib fallback strategy
erral Apr 1, 2026
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
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
## Changes

## 5.2.0 (unreleased)

- Feature: Built-in integration with `uv` through `pyproject.toml`. When `mxdev` is run, it checks if the project has a `pyproject.toml` containing `[tool.uv]` with `managed = true`. If so, mxdev automatically adds checked-out packages to `[tool.uv.sources]`. This allows for seamless use of `uv sync` or `uv run` with local checkouts. `tomlkit` is now an optional dependency (install with `mxdev[uv]`) to preserve `pyproject.toml` formatting during updates.
[erral]

## 5.1.0

Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ If there is a source section defined for the same package, the source will be us
Note: When using [uv](https://pypi.org/project/uv/) pip install the version overrides here are not needed, since it [supports overrides natively](https://github.com/astral-sh/uv?tab=readme-ov-file#dependency-overrides).
With uv it is recommended to create an `overrides.txt` file with the version overrides and use `uv pip install --override overrides.txt [..]` to install the packages.


##### `ignores`

Ignore packages that are already defined in a dependent constraints file.
Expand Down Expand Up @@ -295,6 +294,32 @@ Mxdev will

Now, use the generated requirements and constraints files with i.e. `pip install -r requirements-mxdev.txt`.

## uv pyproject.toml integration

mxdev includes a built-in hook to automatically update your `pyproject.toml` file when working with [uv](https://docs.astral.sh/uv/)-managed projects.

To use this feature, you must install mxdev with the `uv` extra:

```bash
pip install mxdev[uv]
```

If your `pyproject.toml` contains the `[tool.uv]` table with `managed = true`:
```toml
[tool.uv]
managed = true
```

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

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`:
```toml
[tool.uv]
managed = false
```

## Example Configuration

### Example `mx.ini`
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ classifiers = [
dependencies = ["packaging"]

[project.optional-dependencies]
uv = ["tomlkit>=0.12.0"]
mypy = []
test = [
"pytest",
"pytest-cov",
"pytest-mock",
"httpretty",
"coverage[toml]",
"tomlkit>=0.12.0",
]

[project.urls]
Expand All @@ -41,6 +43,9 @@ Source = "https://github.com/mxstack/mxdev/"
[project.scripts]
mxdev = "mxdev.main:main"

[project.entry-points.mxdev]
hook = "mxdev.uv:UvPyprojectUpdater"

[project.entry-points."mxdev.workingcopytypes"]
svn = "mxdev.vcs.svn:SVNWorkingCopy"
git = "mxdev.vcs.git:GitWorkingCopy"
Expand Down
144 changes: 144 additions & 0 deletions src/mxdev/uv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from mxdev.hooks import Hook
from mxdev.state import State
from pathlib import Path
from typing import TYPE_CHECKING

import logging
import os
import tempfile


if TYPE_CHECKING:
import tomlkit


logger = logging.getLogger("mxdev")


class UvPyprojectUpdater(Hook):
"""An mxdev hook that updates pyproject.toml during the write phase for uv-managed projects."""

namespace = "uv"

def read(self, state: State) -> None:
pass

def write(self, state: State) -> None:
pyproject_path = Path(state.configuration.settings.get("directory", ".")) / "pyproject.toml"
if not pyproject_path.exists():
logger.debug("[%s] pyproject.toml not found, skipping.", self.namespace)
return

try:
content = pyproject_path.read_text(encoding="utf-8")
except OSError as e:
logger.error("[%s] Failed to read pyproject.toml: %s", self.namespace, e)
return

# Attempt to parse using standard library (Python 3.11+)
try:
import tomllib

parsed = tomllib.loads(content)
if parsed.get("tool", {}).get("uv", {}).get("managed") is not True:
logger.debug(
"[%s] Project not explicitly managed by uv ([tool.uv] managed=true missing), skipping.",
self.namespace,
)
return
except ImportError:
# Fallback for Python 3.10: fast string check to avoid tomlkit overhead
if "[tool.uv]" not in content:
logger.debug(
"[%s] Project not explicitly managed by uv ([tool.uv] managed=true missing), skipping.",
self.namespace,
)
return
except Exception:
# If the parser fails (e.g., malformed TOML), just skip.
return

# Now we are confident it's a uv project, require our heavy dependency
try:
from typing import TYPE_CHECKING

if not TYPE_CHECKING:
import tomlkit
except ImportError:
raise RuntimeError("tomlkit is required for the uv hook. Install it with: pip install mxdev[uv]")

doc = tomlkit.loads(content)

# Check for the UV managed signal
tool_uv = doc.get("tool", {}).get("uv", {})
if tool_uv.get("managed") is not True:
logger.debug(
"[%s] Project not explicitly managed by uv ([tool.uv] managed=true missing), skipping.", self.namespace
)
return

logger.info("[%s] Updating pyproject.toml...", self.namespace)
self._update_pyproject(doc, state)

tmp = None
try:
with tempfile.NamedTemporaryFile(
mode="w", dir=pyproject_path.parent, suffix=".tmp", delete=False, encoding="utf-8"
) as f:
tomlkit.dump(doc, f)
tmp = f.name
os.replace(tmp, str(pyproject_path))
tmp = None # success, don't clean up
logger.info("[%s] Successfully updated pyproject.toml", self.namespace)
except OSError as e:
logger.error("[%s] Failed to write pyproject.toml: %s", self.namespace, e)
finally:
if tmp and os.path.exists(tmp):
os.unlink(tmp)

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:
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:
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)

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

uv_sources[pkg_name] = source_table
Loading
Loading