From 83d380289d50bac09e385dc81012bca8c5be7cb6 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Tue, 27 Aug 2024 17:35:03 +0100 Subject: [PATCH] use uv.sources table for internal deps (#4) --- .github/workflows/release.yml | 1 - README.md | 17 +++---- docs/build.md | 2 +- docs/index.md | 7 +-- plugins/hatch/hatch_una/hatch_build.py | 23 +++++---- plugins/hatch/hatch_una/hatch_meta.py | 21 ++------ plugins/hatch/hatch_una/util.py | 44 +++++++++++++++++ plugins/pdm/README.md | 5 -- plugins/pdm/pdm_una/__init__.py | 0 plugins/pdm/pdm_una/hook.py | 9 ---- plugins/pdm/pdm_una/include.py | 51 ------------------- plugins/pdm/pdm_una/meta.py | 51 ------------------- plugins/pdm/pdm_una/py.typed | 0 plugins/pdm/pdm_una/util.py | 38 -------------- plugins/pdm/pyproject.toml | 52 -------------------- pyproject.toml | 2 +- tests/test_distributions.py | 2 + tests/test_sync.py | 57 --------------------- una/una/cli.py | 13 +++-- una/una/config.py | 23 ++++----- una/una/files.py | 34 +++++++------ una/una/package_deps.py | 68 ++++++++++---------------- una/una/sync.py | 44 ++++------------- una/una/types.py | 35 +++++++------ uv.lock | 25 +--------- 25 files changed, 166 insertions(+), 458 deletions(-) delete mode 100644 plugins/pdm/README.md delete mode 100644 plugins/pdm/pdm_una/__init__.py delete mode 100644 plugins/pdm/pdm_una/hook.py delete mode 100644 plugins/pdm/pdm_una/include.py delete mode 100644 plugins/pdm/pdm_una/meta.py delete mode 100644 plugins/pdm/pdm_una/py.typed delete mode 100644 plugins/pdm/pdm_una/util.py delete mode 100644 plugins/pdm/pyproject.toml delete mode 100644 tests/test_sync.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60819bc..75eb283 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/README.md b/README.md index 7c5e9f4..75353c3 100644 --- a/README.md +++ b/README.md @@ -20,21 +20,20 @@ -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: diff --git a/docs/build.md b/docs/build.md index 0c8d758..4782993 100644 --- a/docs/build.md +++ b/docs/build.md @@ -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. diff --git a/docs/index.md b/docs/index.md index 428e4f2..1b220f9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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: diff --git a/plugins/hatch/hatch_una/hatch_build.py b/plugins/hatch/hatch_una/hatch_build.py index d0140da..1425e9b 100644 --- a/plugins/hatch/hatch_una/hatch_build.py +++ b/plugins/hatch/hatch_una/hatch_build.py @@ -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 @@ -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 @@ -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) @@ -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, } diff --git a/plugins/hatch/hatch_una/hatch_meta.py b/plugins/hatch/hatch_una/hatch_meta.py index 2d2aa07..fc7ac2d 100644 --- a/plugins/hatch/hatch_una/hatch_meta.py +++ b/plugins/hatch/hatch_una/hatch_meta.py @@ -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: @@ -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 diff --git a/plugins/hatch/hatch_una/util.py b/plugins/hatch/hatch_una/util.py index 5f9482a..5249328 100644 --- a/plugins/hatch/hatch_una/util.py +++ b/plugins/hatch/hatch_una/util.py @@ -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) diff --git a/plugins/pdm/README.md b/plugins/pdm/README.md deleted file mode 100644 index dce2894..0000000 --- a/plugins/pdm/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# pdm-una - -This is the PDM plugin for [una](https://github.com/carderne/una). - -Read the full README there. diff --git a/plugins/pdm/pdm_una/__init__.py b/plugins/pdm/pdm_una/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/pdm/pdm_una/hook.py b/plugins/pdm/pdm_una/hook.py deleted file mode 100644 index 1201dfd..0000000 --- a/plugins/pdm/pdm_una/hook.py +++ /dev/null @@ -1,9 +0,0 @@ -from pdm.backend.hooks import Context - -from pdm_una.include import force_include -from pdm_una.meta import add_dependencies - - -def pdm_build_initialize(context: Context): - add_dependencies(context) - force_include(context) diff --git a/plugins/pdm/pdm_una/include.py b/plugins/pdm/pdm_una/include.py deleted file mode 100644 index 6b25d48..0000000 --- a/plugins/pdm/pdm_una/include.py +++ /dev/null @@ -1,51 +0,0 @@ -from pathlib import Path - -from pdm.backend.hooks import Context - -from pdm_una import util - - -def force_include(context: Context) -> None: - """ - Force-include all needed internal monorepo dependencies. - """ - print("una: Injecting internal dependencies") - context.ensure_build_dir() - - build_dir = context.build_dir - - # load the config for this package - path = Path(context.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 - - if not int_deps: - # this is fine, the package doesn't import anything internally - return - - via_sdist = Path("PKG-INFO").exists() - if via_sdist: - # nothing to do as everything should already be included in sdist... - return - - # 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) - if len(missing) > 0: - missing_str = ", ".join(missing) - raise ValueError(f"Could not find these paths: {missing_str}") - - for src_str, dst_str in int_deps.items(): - src = Path(src_str) - destination = build_dir / dst_str - util.copy_tree(src, destination) - # need these so src->sdist->wheel builds can access them for external deps - util.copy_file( - src.parents[1] / util.PYPROJ, - build_dir / util.EXTRA_PYPROJ / src.name / util.PYPROJ, - ) diff --git a/plugins/pdm/pdm_una/meta.py b/plugins/pdm/pdm_una/meta.py deleted file mode 100644 index 29ee91f..0000000 --- a/plugins/pdm/pdm_una/meta.py +++ /dev/null @@ -1,51 +0,0 @@ -from pathlib import Path - -from pdm.backend.hooks import Context - -from pdm_una import util - - -def add_dependencies(context: Context): - """ - Inject needed third-party dependencies into project.dependencies. - """ - print("una: Injecting transitive external dependencies") - metadata = context.config.metadata - path = context.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] - - add_deps: list[str] = [] - for dep_path in int_deps: - # In builds that do src -> sdist -> wheel, the needed pyproject.toml files - # will have been copied into the sdist so they're available for the wheel build. - # Here we check for both in order. - dep_project_path = Path(dep_path).parents[1] - extra_path = util.EXTRA_PYPROJ / Path(dep_path).name - if dep_project_path.exists(): - use_path = dep_project_path - elif extra_path.exists(): - use_path = extra_path - else: - raise ValueError(f"Could not find internal dependency at '{dep_path}'") - - # 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.dependcies table for '{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)) diff --git a/plugins/pdm/pdm_una/py.typed b/plugins/pdm/pdm_una/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/pdm/pdm_una/util.py b/plugins/pdm/pdm_una/util.py deleted file mode 100644 index 6a45922..0000000 --- a/plugins/pdm/pdm_una/util.py +++ /dev/null @@ -1,38 +0,0 @@ -import shutil -import tomllib -from pathlib import Path -from typing import Any - -PYPROJ = "pyproject.toml" -EXTRA_PYPROJ = Path("_extra_pyproj") - - -def load_conf(path: Path) -> dict[str, Any]: - with (path / PYPROJ).open("rb") as fp: - return tomllib.load(fp) - - -def copy_file(src: Path, dst: Path) -> Path: - dst.parents[0].mkdir(parents=True, exist_ok=True) - return shutil.copyfile(src, dst) - - -def copy_tree(src: Path, dst: Path) -> Path: - ignore = shutil.ignore_patterns( - "*.pyc", - "__pycache__", - ".venv", - "__pypackages__", - ".mypy_cache", - ".pytest_cache", - "node_modules", - ".git", - ) - - res: Path = shutil.copytree( # return might not actually be a Path - src, - dst, - ignore=ignore, - dirs_exist_ok=True, - ) - return Path(res) diff --git a/plugins/pdm/pyproject.toml b/plugins/pdm/pyproject.toml deleted file mode 100644 index dfc9f89..0000000 --- a/plugins/pdm/pyproject.toml +++ /dev/null @@ -1,52 +0,0 @@ -[project] -name = "pdm-una" -dynamic = ["version"] -description = "Python monorepo tooling" -authors = [ - { name = "Chris Arderne", email = "chris@rdrn.me" } -] -readme = "README.md" -license = {text = "MIT License"} -requires-python = ">= 3.11" -keywords = ["uv", "pdm", "monorepo", "build", "python"] - -classifiers = [ - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: Unix", - "Programming Language :: Python", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", -] - -dependencies = ["pdm-backend"] - -[project.urls] -homepage = "https://github.com/carderne/una" -repository = "https://github.com/carderne/una" - -[project.entry-points."pdm.build.hook"] -una = "pdm_una.hook" - -[tool.uv] -dev-dependencies = [] - -[build-system] -requires = ["hatchling", "hatch-vcs"] -build-backend = "hatchling.build" - -[tool.hatch.version] -source = "vcs" -raw-options = { root = "../.." } - -[tool.basedpyright] -venvPath = "../.." -venv = ".venv" -pythonVersion = "3.11" -strict = ["**/*.py"] -reportUnnecessaryTypeIgnoreComment = true -reportImplicitOverride = false -reportUnusedCallResult = false -enableTypeIgnoreComments = true diff --git a/pyproject.toml b/pyproject.toml index c713142..5690465 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ dev-dependencies = [ ] [tool.uv.workspace] -members = ["una", "plugins/*"] +members = ["una", "plugins/hatch"] [tool.ruff] target-version = "py311" diff --git a/tests/test_distributions.py b/tests/test_distributions.py index 8dd253a..903982e 100644 --- a/tests/test_distributions.py +++ b/tests/test_distributions.py @@ -1,3 +1,5 @@ +"""Code from https://github.com/DavidVujic/python-polylith""" + # pyright: reportPrivateUsage=false import importlib.metadata diff --git a/tests/test_sync.py b/tests/test_sync.py deleted file mode 100644 index bd86a92..0000000 --- a/tests/test_sync.py +++ /dev/null @@ -1,57 +0,0 @@ -# pyright: reportPrivateUsage=false - -from una import config, sync -from una.types import Include - -INCLUDES = [ - Include(src="apps/hello/first", dst="hello/first"), - Include(src="libs/hello/second", dst="hello/second"), - Include(src="libs/hello/third", dst="hello/third"), -] - -EXPEXTED_HATCH_PACKAGES = { - "apps/hello/first": "hello/first", - "libs/hello/second": "hello/second", - "libs/hello/third": "hello/third", -} - -BASE_PYPROJECT = """ -[project] -name = "" -version = "" -description = "" -authors = [] -dependencies = [] -readme = "" -requires-python = "" -""" - - -def test_generate_updated_hatch_project_with_existing_una_sections(): - pyproj = f""" -{BASE_PYPROJECT} -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" -[tool.hatch.build] -[tool.una.deps] -"apps/hello/first" = "hello/first" -""" - conf = config.load_conf_from_str(pyproj) - updated = str(sync._generate_updated_package(conf, INCLUDES[1:])) - res = config.load_conf_from_str(updated).tool.una.deps - assert res == EXPEXTED_HATCH_PACKAGES - - -def test_generate_updated_hatch_project_with_missing_int_dep_config(): - pyproj = f""" -{BASE_PYPROJECT} -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" -[tool.hatch.build] - """ - conf = config.load_conf_from_str(pyproj) - updated = str(sync._generate_updated_package(conf, INCLUDES)) - res = config.load_conf_from_str(updated).tool.una.deps - assert res == EXPEXTED_HATCH_PACKAGES diff --git a/una/una/cli.py b/una/una/cli.py index e41fcad..4cec71b 100644 --- a/una/una/cli.py +++ b/una/una/cli.py @@ -31,6 +31,7 @@ def sync_command( ] = "", ): """Update packages with missing dependencies.""" + console = rich_console() root = config.get_workspace_root() ns = config.get_ns(root) alias_list = alias.split(",") if alias else [] @@ -41,7 +42,6 @@ def sync_command( d = check.check_package_deps(root, ns, p, alias_list) diffs.append(d) - console = rich_console() if check_only: for d in diffs: if d.ext_dep_diff: @@ -71,9 +71,10 @@ def create_package_command( path: Annotated[str, Argument(help="Where to place the package.")], ): """Creates an Una package.""" - root = config.get_workspace_root() - files.create_package(root, name, path, "", "", "") console = rich_console() + root = config.get_workspace_root() + ns = config.get_ns(root) + files.create_package(root, ns, name, path, "", "", "") console.print("Success!") console.print(f"Created package {name}") @@ -81,11 +82,9 @@ def create_package_command( @create.command("workspace") def create_workspace_command(): """Creates an Una workspace in the current directory.""" - path = Path.cwd() - root = config.get_workspace_root() - ns = config.get_ns(root) - files.create_workspace(path, ns) console = rich_console() + path = Path.cwd() + files.create_workspace(path) console.print("Success!") console.print("Set up workspace in current directory.") console.print("Remember to delete the src/ directory") diff --git a/una/una/config.py b/una/una/config.py index 6c5b391..287da8b 100644 --- a/una/una/config.py +++ b/una/una/config.py @@ -21,30 +21,27 @@ def load_conf(path: Path) -> Conf: return _load_conf(fullpath) -def sanitise_name(name: str) -> str: - return name.replace("-", "_") - - def get_ns(path: Path) -> str: - return sanitise_name(load_conf(path).project.name) + ns = load_conf(path).tool.una.namespace + if ns is None: + raise ValueError("No namespace set in pyproject.toml") + return ns def get_members(path: Path) -> list[str]: - return load_conf(path).tool.uv.members + return load_conf(path).tool.uv.workspace.members def get_workspace_root() -> Path: - root = _find_upwards(Path.cwd(), consts.ROOT_FILE) + root = _find_upwards(Path.cwd()) if not root: raise ValueError("Didn't find the workspace root. Expected to find a .git directory.") - return root.parent + return root -def _find_upwards(cwd: Path, name: str) -> Path | None: +def _find_upwards(cwd: Path) -> Path | None: if cwd == Path(cwd.root) or cwd == cwd.parent: return None - elif (fullpath := cwd / name).exists(): - return fullpath elif (cwd / consts.ROOT_FILE).exists(): - return None - return _find_upwards(cwd.parent, name) + return cwd + return _find_upwards(cwd.parent) diff --git a/una/una/files.py b/una/una/files.py index 801b5cb..2668f92 100644 --- a/una/una/files.py +++ b/una/una/files.py @@ -29,16 +29,14 @@ [tool.uv] dev-dependencies = [] -[tool.hatch.build.hooks.una-build] -[tool.hatch.metadata.hooks.una-meta] +[tool.uv.sources] +{sources} -[tool.una.deps] -{internal_deps}\ +[tool.hatch.build.hooks.una-build] +[tool.hatch.metadata.hooks.una-meta]\ """ -_EXAMPLE_INTERNAL_DEPS = """\ -"../../libs/{dep_name}/{ns}/{dep_name}" = "{ns}/{dep_name}" -""" +_EXAMPLE_INTERNAL_DEPS = """{dep_name} = {{ workspace = true }}""" _EXAMPLE_APP_CODE = """\ from {ns} import {lib_name} @@ -65,21 +63,24 @@ def test_import(): """ -def create_workspace(path: Path, ns: str) -> None: +def create_workspace(path: Path) -> None: + ns = _update_root_pyproj(path, _EXAMPLE_IMPORT) + app_content = _EXAMPLE_APP_CODE.format(ns=ns, lib_name=_EXAMPLE_LIB_NAME) + app_deps = _EXAMPLE_INTERNAL_DEPS.format(dep_name=_EXAMPLE_LIB_NAME) lib_content = _EXAMPLE_LIB_CODE - - _update_root_pyproj(path, ns, _EXAMPLE_IMPORT) create_package( path, + ns, _EXAMPLE_APP_NAME, "apps", app_content, - "", - _EXAMPLE_INTERNAL_DEPS.format(ns=ns, dep_name=_EXAMPLE_LIB_NAME), + f'"{_EXAMPLE_LIB_NAME}"', + app_deps, ) create_package( path, + ns, _EXAMPLE_LIB_NAME, "libs", lib_content, @@ -90,6 +91,7 @@ def create_workspace(path: Path, ns: str) -> None: def create_package( path: Path, + ns: str, name: str, top_dir: str, content: str, @@ -98,7 +100,6 @@ def create_package( ) -> None: conf = config.load_conf(path) python_version = conf.project.requires_python - ns = config.get_ns(path) package_dir = _create_dir(path, f"{top_dir}/{name}") ns_dir = _create_dir(path, f"{top_dir}/{name}/{ns}") @@ -117,7 +118,7 @@ def create_package( name=name, python_version=python_version, dependencies=dependencies, - internal_deps=internal_deps, + sources=internal_deps, ) _create_file( package_dir, @@ -144,13 +145,16 @@ def _create_dir(path: Path, dir_name: str, keep: bool = False) -> Path: return d -def _update_root_pyproj(path: Path, ns: str, dependencies: str) -> None: +def _update_root_pyproj(path: Path, dependencies: str) -> str: pyproj = path / consts.PYPROJ_FILE with pyproj.open() as f: toml = tomlkit.parse(f.read()) + ns: str = toml["project"]["name"] # pyright:ignore[reportIndexIssue,reportAssignmentType] toml.pop("project") # pyright:ignore[reportUnknownMemberType] toml.pop("build-system") # pyright:ignore[reportUnknownMemberType] toml["tool"]["uv"]["workspace"] = {"members": _EXAMPLE_MEMBERS} # pyright:ignore[reportIndexIssue] + toml["tool"]["una"] = {"namespace": ns} # pyright:ignore[reportIndexIssue] with pyproj.open("w") as f: f.write(tomlkit.dumps(toml)) # pyright:ignore[reportUnknownMemberType] + return ns diff --git a/una/una/package_deps.py b/una/una/package_deps.py index e0c8f57..b32ff13 100644 --- a/una/una/package_deps.py +++ b/una/una/package_deps.py @@ -2,23 +2,13 @@ from pathlib import Path from una import config -from una.types import Conf, ConfWrapper, ExtDep, Include, IntDep, PackageDeps +from una.types import ConfWrapper, ExtDep, IntDep, PackageDeps def get_packages(root: Path, ns: str) -> list[PackageDeps]: - root_conf = config.load_conf(root) - ns = root_conf.project.name confs = get_package_confs(root) - packages = [ - PackageDeps( - name=c.conf.project.name, - path=c.path, - ext_deps=_get_package_ext_deps(c.conf), - int_deps=_get_package_int_deps(c, confs, ns), - ) - for c in confs - ] - return [p for p in packages if Path.cwd().name in p.path.as_posix()] + packages = [_get_package_deps(c) for c in confs if Path.cwd().name in c.path.as_posix()] + return packages def get_package_confs(root: Path) -> list[ConfWrapper]: @@ -30,34 +20,26 @@ def get_package_confs(root: Path) -> list[ConfWrapper]: return packages -def _parse_deps_table(dep: str) -> ExtDep | None: - parts = re.split(r"[\^~=!<>]", dep) +def _parse_deps_table(dep: str) -> ExtDep: + parts: list[str] = re.split(r"[\^~=!<>]", dep) name, *_ = parts if parts else [""] - version = str.replace(dep, name, "") - if name: - return ExtDep(name, version) - return None - - -def _get_package_ext_deps(conf: Conf) -> list[ExtDep]: - deps = conf.project.dependencies - items = [_parse_deps_table(dep) for dep in deps] - items_filt = [it for it in items if it] - return items_filt - - -def _get_package_int_deps( - conf: ConfWrapper, - all_confs: list[ConfWrapper], - namespace: str, -) -> list[IntDep]: - packages = [Include(src=k, dst=v) for k, v in conf.conf.tool.una.deps.items()] - paths = [(conf.path / p.src).parents[1].resolve() for p in packages] - - all_paths = {Path(c.path) for c in all_confs} - pkg_deps_paths = sorted(all_paths.intersection(paths)) - pkg_deps = [IntDep(path=Path(p), name=Path(p).name) for p in pkg_deps_paths] - - # add self - pkg_deps.append(IntDep(path=conf.path, name=conf.conf.project.name)) - return pkg_deps + version = dep.replace(name, "") + return ExtDep(name, version) + + +def _get_package_deps(conf: ConfWrapper) -> PackageDeps: + items = [_parse_deps_table(dep) for dep in conf.conf.project.dependencies] + ext_deps: list[ExtDep] = [] + int_deps: list[IntDep] = [] + for it in items: + if it.name in conf.conf.tool.uv.sources: + if conf.conf.tool.uv.sources[it.name].workspace: + int_deps.append(IntDep(name=it.name)) + continue + ext_deps.append(it) + return PackageDeps( + name=conf.conf.project.name, + path=conf.path, + ext_deps=ext_deps, + int_deps=int_deps, + ) diff --git a/una/una/sync.py b/una/una/sync.py index cc48353..c7b5904 100644 --- a/una/una/sync.py +++ b/una/una/sync.py @@ -1,30 +1,22 @@ +from collections.abc import Iterable from pathlib import Path -from typing import cast -from una import config, consts, package_deps -from una.types import CheckDiff, Conf, Include +from una import config, consts +from una.types import CheckDiff, Conf, UvSourceIsWorkspace def sync_package(root: Path, diff: CheckDiff, ns: str): - all_confs = package_deps.get_package_confs(root) - pkg_map = {c.path.name: c.path for c in all_confs} - packages = [_to_package(pkg_map, ns, name, diff.package.path) for name in diff.int_dep_diff] - _rewrite_package_pyproj(diff.package.path, packages) + _rewrite_package_pyproj(diff.package.path, diff.int_dep_diff) -def _to_package(pkg_map: dict[str, Path], ns: str, name: str, caller_path: Path) -> Include: - dst = Path(ns) / name - src = _path_relative_to(pkg_map[name] / dst, caller_path) - return Include(src=str(src), dst=str(dst)) - - -def _generate_updated_package(conf: Conf, packages: list[Include]) -> str | None: - for inc in packages: - conf.tool.una.deps[inc.src] = inc.dst +def _generate_updated_package(conf: Conf, packages: Iterable[str]) -> str | None: + for p in packages: + conf.project.dependencies.append(p) + conf.tool.uv.sources[p] = UvSourceIsWorkspace(workspace=True) return conf.to_str() -def _rewrite_package_pyproj(path: Path, packages: list[Include]): +def _rewrite_package_pyproj(path: Path, packages: Iterable[str]): conf = config.load_conf(path) generated = _generate_updated_package(conf, packages) if not generated: @@ -32,21 +24,3 @@ def _rewrite_package_pyproj(path: Path, packages: list[Include]): fullpath = path / consts.PYPROJ_FILE with fullpath.open("w", encoding="utf-8") as f: f.write(generated) - - -def _path_relative_to(p: Path, other: Path) -> Path: - """ - Return relative path between paths. - - Added here since the walk_up parameter isn't available in Python 3.11 - https://github.com/python/cpython/blob/33d9e27b2b26d5434d654ef8e5fae560beb68b1b/Lib/pathlib.py#L663 - """ - for step, path in enumerate([other] + list(other.parents)): - if p.is_relative_to(path): - break - elif path.name == "..": - raise ValueError(f"'..' segment in {str(other)!r} cannot be walked") - else: - raise ValueError(f"{str(p)!r} and {str(other)!r} have different anchors") - parts = cast(str, [".."] * step + p._tail[len(path._tail) :]) # pyright:ignore[reportUnknownMemberType,reportAttributeAccessIssue,reportUnknownArgumentType] - return Path(*parts) diff --git a/una/una/types.py b/una/una/types.py index 57e6c02..d3610af 100644 --- a/una/una/types.py +++ b/una/una/types.py @@ -11,12 +11,6 @@ Imports: TypeAlias = dict[str, set[str]] -@dataclass -class Include: - src: str - dst: str - - @dataclass(frozen=True) class ExtDep: name: str @@ -25,8 +19,8 @@ class ExtDep: @dataclass(frozen=True) class IntDep: - path: Path name: str + version: str = "" @dataclass(frozen=False) @@ -60,8 +54,8 @@ def rename(d: Json) -> None: @dataclass_json @dataclass(frozen=True) class Project: - name: str - dependencies: list[str] + name: str = "" + dependencies: list[str] = field(default_factory=list) version: str | None = None requires_python: str = ">= 3.8" @@ -72,14 +66,27 @@ def _default_members() -> list[str]: @dataclass_json @dataclass(frozen=True) -class Uv: +class UvWorkspace: members: list[str] = field(default_factory=_default_members) +@dataclass_json +@dataclass(frozen=True) +class UvSourceIsWorkspace: + workspace: bool = False + + +@dataclass_json +@dataclass(frozen=True) +class Uv: + workspace: UvWorkspace = field(default_factory=UvWorkspace) + sources: dict[str, UvSourceIsWorkspace] = field(default_factory=dict) + + @dataclass_json @dataclass(frozen=True) class Una: - deps: dict[str, str] = field(default_factory=dict) + namespace: str | None = None @dataclass_json @@ -102,8 +109,8 @@ class Conf: See the caveats on `to_str()`. """ - project: Project tool: Tool + project: Project = field(default_factory=Project) _tomldoc: tomlkit.TOMLDocument | None = field(default=None) if TYPE_CHECKING: @@ -134,7 +141,7 @@ def to_tomldoc(self) -> tomlkit.TOMLDocument: # deal with a a non-existent tool.una.deps try: - tomldoc["tool"]["una"]["deps"].update(self.tool.una.deps) # type: ignore[reportIndexIssues] + tomldoc["tool"]["uv"]["sources"].update(self.tool.uv.sources) # type: ignore[reportIndexIssues] except KeyError: una = tomlkit.table(True) deps = tomlkit.table() @@ -155,7 +162,7 @@ def to_str(self) -> str: To preserve the original formatting and make my life easy, this function will currently only modify the following fields: - project.dependencies - - tool.una.deps + - tool.uv.sources - tool.hatch.build.hooks.una-build - tool.hatch.meta.hooks.una-meta diff --git a/uv.lock b/uv.lock index 23b511b..4abd3f2 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,6 @@ requires-python = ">=3.11" [manifest] members = [ "hatch-una", - "pdm-una", "una", ] requirements = [ @@ -131,7 +130,7 @@ wheels = [ [[package]] name = "hatch-una" -version = "0.2.1.dev18+gb206798.d20240827" +version = "0.3.2.dev2+ga35a5a9" source = { editable = "plugins/hatch" } dependencies = [ { name = "hatchling" }, @@ -383,26 +382,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, ] -[[package]] -name = "pdm-backend" -version = "2.3.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/2b/0be2d0f2eba3a4acb755fd2b0e442ef67770b2ef6c75fd646d49f20968fa/pdm_backend-2.3.3.tar.gz", hash = "sha256:a8616f628ec84353d7a0ba86b228dcf01bab5debc9e4d1a29e5311a52425d594", size = 109222 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/fe/483cf0918747a32800795f430319ec292f833eb871ba6da3ebed4553a575/pdm_backend-2.3.3-py3-none-any.whl", hash = "sha256:db6e86de8ca84e4264c35a620877b14aba8092ad7a34de395715553144668823", size = 104947 }, -] - -[[package]] -name = "pdm-una" -version = "0.2.1.dev21+gd9be7e8.d20240827" -source = { editable = "plugins/pdm" } -dependencies = [ - { name = "pdm-backend" }, -] - -[package.metadata] -requires-dist = [{ name = "pdm-backend" }] - [[package]] name = "platformdirs" version = "4.2.2" @@ -683,7 +662,7 @@ wheels = [ [[package]] name = "una" -version = "0.2.1.dev23+g1380e02.d20240827" +version = "0.3.2.dev2+ga35a5a9" source = { editable = "una" } dependencies = [ { name = "dataclasses-json" },