Skip to content

Commit

Permalink
use uv.sources table for internal deps (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
carderne authored Aug 27, 2024
1 parent a35a5a9 commit 83d3802
Show file tree
Hide file tree
Showing 25 changed files with 166 additions and 458 deletions.
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,4 @@ jobs:
- run: |
uvx --from build pyproject-build --installer uv --outdir=dist una
uvx --from build pyproject-build --installer uv --outdir=dist plugins/hatch
uvx --from build pyproject-build --installer uv --outdir=dist plugins/pdm
- uses: pypa/gh-action-pypi-publish@release/v1
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,20 @@

</div>

Una is a tool to make Python monorepos easier. It is a CLI tool and a build plugin that does the following things:
Una is a tool to make Python monorepos with [uv](https://docs.astral.sh/uv/) easier.
It is a CLI tool and a build plugin that does the following things:

1. Enable builds of individual apps or projects within a monorepo.
2. Ensure that internal and external dependencies are correctly specified.

Una doesn't try to replicate a full build system such as [Bazel](https://bazel.build/) or [Pants](https://www.pantsbuild.org/). It just makes it possible to have a simple monorepo with interdependencies.
Una doesn't try to replicate a full build system such as [Bazel](https://bazel.build/) or
[Pants](https://www.pantsbuild.org/).
It just makes it possible to have a simple monorepo with interdependencies.

Una works much like a Rust workspace, with each package having its own pyproject.toml. In general, packages should either be libraries (imported but not run) or apps (run but never imported), but Una will not enforce this.
Una works much like a Rust workspace, with each package having its own pyproject.toml.
In general, packages should either be libraries (imported but not run) or apps (run but never imported), but Una will not enforce this.

Currently it works with the following build backends, but more will follow:

- [Hatch](https://hatch.pypa.io) (used by default and and in all documentation)
- [PDM](https://pdm-project.org/)

All instructions and examples use [uv](https://docs.astral.sh/uv/) for local development.
It only works with [uv](https://docs.astral.sh/uv/) and with the [Hatch](https://hatch.pypa.io) build backend.

## Examples
You can see an example repo here:
Expand Down
2 changes: 1 addition & 1 deletion docs/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ uvx --from build pyproject-build --installer uv apps/printer
```

You'll get some `*.whl` files, which you can then deploy with Docker or whatever you prefer.
They are fully self-contained, so you don't need uv/Hatch/PDM or Una or anything else wherever you want to install them.
They are fully self-contained, so you don't need uv/Hatch or Una or anything else wherever you want to install them.
7 changes: 1 addition & 6 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@ Una doesn't try to replicate a full build system such as [Bazel](https://bazel.b

Una works much like a Rust workspace, with each package having its own pyproject.toml. In general, packages should either be libraries (imported but not run) or apps (run but never imported), but Una will not enforce this.

Currently it works with the following build backends, but more will follow:

- [Hatch](https://hatch.pypa.io) (used by default and and in all documentation)
- [PDM](https://pdm-project.org/)

All instructions and examples use [uv](https://docs.astral.sh/uv/) for local development.
It only works with [uv](https://docs.astral.sh/uv/) and with the [Hatch](https://hatch.pypa.io) build backend.

## Examples
You can see an example repo here:
Expand Down
23 changes: 14 additions & 9 deletions plugins/hatch/hatch_una/hatch_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from hatchling.builders.config import BuilderConfig
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
from hatchling.builders.sdist import SdistBuilder
from hatchling.plugin import hookimpl

from hatch_una import util
Expand All @@ -21,14 +22,10 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None:
# load the config for this package
path = Path(self.root)
conf = util.load_conf(path)
name: str = conf["project"]["name"]

try:
int_deps: dict[str, str] = conf["tool"]["una"]["deps"]
except KeyError as e:
raise KeyError(
f"Package '{name}' is missing '[tool.una.deps]' in pyproject.toml"
) from e
members: list[str] = (
conf.get("tool", {}).get("uv", {}).get("workspace", {}).get("members", []) # pyright:ignore[reportAny]
)
_, int_deps = util.get_dependencies(path)

if not int_deps:
# this is fine, the package doesn't import anything internally
Expand All @@ -39,6 +36,14 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None:
# nothing to do as everything should already be included in sdist...
return

add_dep_files: dict[str, str] = {}
for d in int_deps:
package_dir = util.find_package_dir(d, members)
finder = SdistBuilder(str(package_dir))
files = [Path(f.path) for f in finder.recurse_selected_project_files()]
for f in files:
add_dep_files[str(f)] = str(f.relative_to(package_dir))

# make sure all int_deps exist
found = [Path(k) for k in int_deps if (path / k).exists()]
missing = set(int_deps) - set(str(p) for p in found)
Expand All @@ -53,7 +58,7 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None:

build_data["force_include"] = {
**build_data["force_include"],
**int_deps,
**add_dep_files,
**add_packages_pyproj,
}

Expand Down
21 changes: 3 additions & 18 deletions plugins/hatch/hatch_una/hatch_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,7 @@ def update(self, metadata: dict[str, Any]) -> None:

# load the config for this package
path = Path(self.root)
conf = util.load_conf(path)
name: str = conf["project"]["name"]

try:
int_deps: dict[str, str] = conf["tool"]["una"]["deps"]
except KeyError as e:
raise KeyError(
f"Package '{name}' is missing '[tool.una.deps]' in pyproject.toml"
) from e

project_deps: list[str] = metadata.get("dependencies", [])
project_deps = [d.strip().replace(" ", "") for d in project_deps]
ext_deps, int_deps = util.get_dependencies(path)

add_deps: list[str] = []
for dep_path in int_deps:
Expand All @@ -48,15 +37,11 @@ def update(self, metadata: dict[str, Any]) -> None:

# load all third-party dependencies from this internal dependency into the
# project.dependencies table
dep_conf = util.load_conf(use_path)
try:
dep_deps: list[str] = dep_conf["project"]["dependencies"]
except KeyError as e:
raise KeyError(f"No project.dependencies table for '{use_path}'")
dep_deps, _ = util.get_dependencies(use_path)
dep_deps = [d.strip().replace(" ", "") for d in dep_deps]
add_deps.extend(dep_deps)

metadata["dependencies"] = list(set(project_deps + add_deps))
metadata["dependencies"] = list(set(ext_deps + add_deps))


@hookimpl
Expand Down
44 changes: 44 additions & 0 deletions plugins/hatch/hatch_una/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,47 @@
def load_conf(path: Path) -> dict[str, Any]:
with (path / PYPROJ).open("rb") as fp:
return tomllib.load(fp)


def get_dependencies(path: Path) -> tuple[list[str], list[str]]:
conf = load_conf(path)
all_deps: list[str] = conf["project"].get("dependencies", []) # pyright:ignore[reportAny]
try:
sources: dict[str, dict[str, bool]] = conf["tool"]["uv"]["sources"]
except KeyError as e:
raise KeyError(f"No tool.uv.sources table for '{path}'") from e

ext_deps: list[str] = []
int_deps: list[str] = []
for d in all_deps:
if d in sources:
if sources[d]["workspace"]:
int_deps.append(d)
continue
ext_deps.append(d.replace(" ", ""))
return (ext_deps, int_deps)


def find_package_dir(name: str, members: list[str]) -> Path:
root = _get_workspace_root()
for glob in members:
packages = sorted(root.glob(glob))
for p in packages:
if p.name == name:
return p.resolve()
raise ValueError(f"Couldn't find package '{name}'")


def _get_workspace_root() -> Path:
root = _find_upwards(Path.cwd())
if not root:
raise ValueError("Didn't find the workspace root. Expected to find a .git directory.")
return root


def _find_upwards(cwd: Path) -> Path | None:
if cwd == Path(cwd.root) or cwd == cwd.parent:
return None
elif (cwd / ".git").exists():
return cwd
return _find_upwards(cwd.parent)
5 changes: 0 additions & 5 deletions plugins/pdm/README.md

This file was deleted.

Empty file removed plugins/pdm/pdm_una/__init__.py
Empty file.
9 changes: 0 additions & 9 deletions plugins/pdm/pdm_una/hook.py

This file was deleted.

51 changes: 0 additions & 51 deletions plugins/pdm/pdm_una/include.py

This file was deleted.

51 changes: 0 additions & 51 deletions plugins/pdm/pdm_una/meta.py

This file was deleted.

Empty file removed plugins/pdm/pdm_una/py.typed
Empty file.
38 changes: 0 additions & 38 deletions plugins/pdm/pdm_una/util.py

This file was deleted.

Loading

0 comments on commit 83d3802

Please sign in to comment.