From 29f49c0483f9d4cad90bd00b9c25bf4444eef7e8 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Sun, 21 Jan 2024 12:02:42 +0100 Subject: [PATCH] feat: Hatch build hook for Polylith (#153) * feat(hatch): add hatch build hook for parsing project bricks * fix(cli): correct create project input args * wip(hatch): add build hook project * refactor: extract into new toml brick * fix: check build system (i.e. poetry or hatch) by looking in the build-backend config * refactor: rename hatch and cli project folders * docs: add docs about the Hatch build hook * bump poetry plugin to 1.14.4 * bump cli to 0.5.0 --- bases/polylith/cli/create.py | 5 +- bases/polylith/hatch_hooks/__init__.py | 0 bases/polylith/hatch_hooks/hooks.py | 8 ++ components/polylith/hatch/__init__.py | 3 + components/polylith/hatch/core.py | 41 ++++++++ components/polylith/hatch/hooks/__init__.py | 0 components/polylith/hatch/hooks/bricks.py | 49 ++++++++++ components/polylith/parsing/__init__.py | 4 + components/polylith/parsing/core.py | 24 +++++ components/polylith/parsing/rewrite.py | 60 ++++++++++++ components/polylith/project/__init__.py | 8 +- components/polylith/project/get.py | 76 ++------------- components/polylith/repo/repo.py | 10 +- components/polylith/toml/__init__.py | 13 +++ components/polylith/toml/core.py | 67 +++++++++++++ poetry.lock | 32 ++++++- projects/hatch_polylith_bricks/README.md | 73 ++++++++++++++ projects/hatch_polylith_bricks/poetry.lock | 95 +++++++++++++++++++ projects/hatch_polylith_bricks/pyproject.toml | 32 +++++++ .../poetry_polylith_plugin/pyproject.toml | 3 +- .../{polylith-cli => polylith_cli}/README.md | 0 .../poetry.lock | 0 .../pyproject.toml | 3 +- pyproject.toml | 5 + test/components/polylith/hatch/__init__.py | 0 test/components/polylith/hatch/test_core.py | 12 +++ test/components/polylith/parsing/__init__.py | 0 test/components/polylith/parsing/test_core.py | 14 +++ test/components/polylith/repo/test_repo.py | 2 +- test/components/polylith/sync/test_update.py | 16 ++++ test/components/polylith/toml/__init__.py | 0 .../test_project_get.py => toml/test_core.py} | 26 ++++- 32 files changed, 592 insertions(+), 89 deletions(-) create mode 100644 bases/polylith/hatch_hooks/__init__.py create mode 100644 bases/polylith/hatch_hooks/hooks.py create mode 100644 components/polylith/hatch/__init__.py create mode 100644 components/polylith/hatch/core.py create mode 100644 components/polylith/hatch/hooks/__init__.py create mode 100644 components/polylith/hatch/hooks/bricks.py create mode 100644 components/polylith/parsing/__init__.py create mode 100644 components/polylith/parsing/core.py create mode 100644 components/polylith/parsing/rewrite.py create mode 100644 components/polylith/toml/__init__.py create mode 100644 components/polylith/toml/core.py create mode 100644 projects/hatch_polylith_bricks/README.md create mode 100644 projects/hatch_polylith_bricks/poetry.lock create mode 100644 projects/hatch_polylith_bricks/pyproject.toml rename projects/{polylith-cli => polylith_cli}/README.md (100%) rename projects/{polylith-cli => polylith_cli}/poetry.lock (100%) rename projects/{polylith-cli => polylith_cli}/pyproject.toml (96%) create mode 100644 test/components/polylith/hatch/__init__.py create mode 100644 test/components/polylith/hatch/test_core.py create mode 100644 test/components/polylith/parsing/__init__.py create mode 100644 test/components/polylith/parsing/test_core.py create mode 100644 test/components/polylith/toml/__init__.py rename test/components/polylith/{project/test_project_get.py => toml/test_core.py} (70%) diff --git a/bases/polylith/cli/create.py b/bases/polylith/cli/create.py index cd8fcc14..7008bdfd 100644 --- a/bases/polylith/cli/create.py +++ b/bases/polylith/cli/create.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Union from polylith import project, repo from polylith.bricks import base, component @@ -29,8 +28,10 @@ def component_command( create(name, description, component.create_component) -def _create_project(root: Path, _ns: str, name: str, description: Union[str, None]): +def _create_project(root: Path, options: dict): root_pyproject: dict = project.get_toml(root / repo.default_toml) + name = options["package"] + description = options["description"] if repo.is_poetry(root_pyproject): template = project.templates.poetry_pyproject diff --git a/bases/polylith/hatch_hooks/__init__.py b/bases/polylith/hatch_hooks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bases/polylith/hatch_hooks/hooks.py b/bases/polylith/hatch_hooks/hooks.py new file mode 100644 index 00000000..4cc9749a --- /dev/null +++ b/bases/polylith/hatch_hooks/hooks.py @@ -0,0 +1,8 @@ +from hatchling.plugin import hookimpl + +from polylith.hatch.hooks.bricks import PolylithBricksHook + + +@hookimpl +def hatch_register_build_hook(): + return PolylithBricksHook diff --git a/components/polylith/hatch/__init__.py b/components/polylith/hatch/__init__.py new file mode 100644 index 00000000..b88f76b6 --- /dev/null +++ b/components/polylith/hatch/__init__.py @@ -0,0 +1,3 @@ +from polylith.hatch import hooks + +__all__ = ["hooks"] diff --git a/components/polylith/hatch/core.py b/components/polylith/hatch/core.py new file mode 100644 index 00000000..e7f0114c --- /dev/null +++ b/components/polylith/hatch/core.py @@ -0,0 +1,41 @@ +from pathlib import Path +from typing import List, Union + +from polylith import parsing + + +def get_work_dir(config: dict) -> Path: + work_dir = config.get("work-dir", ".polylith_tmp") + + return Path(work_dir) + + +def parse_namespace(bricks: dict) -> str: + namespaces = parsing.parse_brick_namespace_from_path(bricks) + + return next(namespace for namespace in namespaces) + + +def copy_brick(source: str, brick: str, tmp_dir: Path) -> Path: + destination = Path(tmp_dir / brick).as_posix() + + return parsing.copy_brick(source, destination) + + +def rewrite_module(module: Path, ns: str, top_ns: str) -> Union[str, None]: + was_rewritten = parsing.rewrite_module(module, ns, top_ns) + + return f"{module.parent.name}/{module.name}" if was_rewritten else None + + +def rewrite_modules(path: Path, ns: str, top_ns: str) -> List[str]: + """Rewrite modules in bricks with new top namespace + + returns a list of bricks that was rewritten + """ + + modules = path.glob("**/*.py") + + res = [rewrite_module(module, ns, top_ns) for module in modules] + + return [r for r in res if r] diff --git a/components/polylith/hatch/hooks/__init__.py b/components/polylith/hatch/hooks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/components/polylith/hatch/hooks/bricks.py b/components/polylith/hatch/hooks/bricks.py new file mode 100644 index 00000000..4e1c2e82 --- /dev/null +++ b/components/polylith/hatch/hooks/bricks.py @@ -0,0 +1,49 @@ +import shutil +from pathlib import Path +from typing import Any, Dict + +from hatchling.builders.hooks.plugin.interface import BuildHookInterface +from polylith import repo, toml +from polylith.hatch import core + + +class PolylithBricksHook(BuildHookInterface): + PLUGIN_NAME = "polylith-bricks" + + def initialize(self, _version: str, build_data: Dict[str, Any]) -> None: + top_ns = self.config.get("top-namespace") + work_dir = core.get_work_dir(self.config) + pyproject = Path(f"{self.root}/{repo.default_toml}") + + print(f"Using {pyproject.as_posix()}.") + + data = toml.read_toml_document(pyproject) + bricks = toml.get_project_packages_from_polylith_section(data) + + if not bricks: + print("No bricks found.") + return + + if not top_ns: + build_data["force_include"] = bricks + return + + ns = core.parse_namespace(bricks) + + for source, brick in bricks.items(): + path = core.copy_brick(source, brick, work_dir) + rewritten_bricks = core.rewrite_modules(path, ns, top_ns) + + for item in rewritten_bricks: + print(f"Updated {item} with new top namespace for local imports.") + + key = work_dir.as_posix() + build_data["force_include"][key] = top_ns + + def finalize(self, *args, **kwargs) -> None: + work_dir = core.get_work_dir(self.config) + + if not work_dir.exists() or not work_dir.is_dir(): + return + + shutil.rmtree(work_dir.as_posix()) diff --git a/components/polylith/parsing/__init__.py b/components/polylith/parsing/__init__.py new file mode 100644 index 00000000..67f8b428 --- /dev/null +++ b/components/polylith/parsing/__init__.py @@ -0,0 +1,4 @@ +from polylith.parsing.core import copy_brick, parse_brick_namespace_from_path +from polylith.parsing.rewrite import rewrite_module + +__all__ = ["copy_brick", "parse_brick_namespace_from_path", "rewrite_module"] diff --git a/components/polylith/parsing/core.py b/components/polylith/parsing/core.py new file mode 100644 index 00000000..8700d2a3 --- /dev/null +++ b/components/polylith/parsing/core.py @@ -0,0 +1,24 @@ +import shutil +from pathlib import Path + + +def copy_brick(source: str, destination: str) -> Path: + ignore = shutil.ignore_patterns( + "*.pyc", + "__pycache__", + ".venv", + ".mypy_cache", + ".pytest_cache", + "node_modules", + ".git", + ) + + res = shutil.copytree(source, destination, ignore=ignore, dirs_exist_ok=True) + + return Path(res) + + +def parse_brick_namespace_from_path(bricks: dict) -> set: + parts = {str.split(v, "/")[0] for v in bricks.values()} + + return parts diff --git a/components/polylith/parsing/rewrite.py b/components/polylith/parsing/rewrite.py new file mode 100644 index 00000000..b61d031c --- /dev/null +++ b/components/polylith/parsing/rewrite.py @@ -0,0 +1,60 @@ +import ast +from pathlib import Path + + +def create_namespace_path(top_ns: str, current: str) -> str: + top_ns_module_path = top_ns.replace("/", ".") + return f"{top_ns_module_path}.{current}" + + +def mutate_import(node: ast.Import, ns: str, top_ns: str) -> bool: + did_mutate = False + + for alias in node.names: + if alias.name == ns: + alias.name = create_namespace_path(top_ns, alias.name) + did_mutate = True + + return did_mutate + + +def mutate_import_from(node: ast.ImportFrom, ns: str, top_ns: str) -> bool: + did_mutate = False + + if not node.module or node.level != 0: + return did_mutate + + if node.module == ns or node.module.startswith(f"{ns}."): + node.module = create_namespace_path(top_ns, node.module) + did_mutate = True + + return did_mutate + + +def mutate_imports(node: ast.AST, ns: str, top_ns: str) -> bool: + if isinstance(node, ast.Import): + return mutate_import(node, ns, top_ns) + + if isinstance(node, ast.ImportFrom): + return mutate_import_from(node, ns, top_ns) + + return False + + +def rewrite_module(source: Path, ns: str, top_ns: str) -> bool: + file_path = source.as_posix() + + with open(file_path, "r", encoding="utf-8") as f: + tree = ast.parse(f.read(), source.name) + + res = {mutate_imports(node, ns, top_ns) for node in ast.walk(tree)} + + if True in res: + rewritten_source_code = ast.unparse(tree) # type: ignore[attr-defined] + + with open(file_path, "w", encoding="utf-8", newline="") as f: + f.write(rewritten_source_code) + + return True + + return False diff --git a/components/polylith/project/__init__.py b/components/polylith/project/__init__.py index 7040d729..45309b8e 100644 --- a/components/polylith/project/__init__.py +++ b/components/polylith/project/__init__.py @@ -1,17 +1,11 @@ from polylith.project import templates from polylith.project.create import create_project -from polylith.project.get import ( - get_packages_for_projects, - get_project_dependencies, - get_project_name, - get_toml, -) +from polylith.project.get import get_packages_for_projects, get_project_name, get_toml from polylith.project.parser import parse_package_paths __all__ = [ "create_project", "get_packages_for_projects", - "get_project_dependencies", "get_project_name", "get_toml", "parse_package_paths", diff --git a/components/polylith/project/get.py b/components/polylith/project/get.py index 0079cd61..c770089b 100644 --- a/components/polylith/project/get.py +++ b/components/polylith/project/get.py @@ -1,57 +1,9 @@ -import re from functools import lru_cache from pathlib import Path -from typing import Any, List +from typing import List import tomlkit -from polylith import repo, workspace - - -def transform_to_package(namespace: str, include: str) -> dict: - path, _separator, brick = str.partition(include, f"/{namespace}/") - - return {"include": f"{namespace}/{brick}", "from": path} - - -def find_by_key(data: dict, key: str) -> Any: - if key in data.keys(): - return data[key] - - filtered = {k: v for k, v in data.items() if isinstance(data[k], dict)} - - res = (find_by_key(data[k], key) for k in filtered.keys()) - - return next((r for r in res if r), None) - - -def get_project_packages_from_polylith_section(data) -> dict: - bricks = data["tool"].get("polylith", {}).get("bricks") - - return bricks if isinstance(bricks, dict) else {} - - -def get_hatch_project_packages(data) -> dict: - hatch_data = data["tool"]["hatch"] - build_data = hatch_data.get("build", {}) if isinstance(hatch_data, dict) else {} - - force_included = build_data.get("force-include") - - if force_included: - return force_included - - return get_project_packages_from_polylith_section(data) - - -def get_project_package_includes(namespace: str, data) -> List[dict]: - if repo.is_poetry(data): - return data["tool"]["poetry"].get("packages", []) - - if repo.is_hatch(data): - includes = get_hatch_project_packages(data) - - return [transform_to_package(namespace, key) for key in includes.keys()] - - return [] +from polylith import repo, toml, workspace def get_project_name(data) -> str: @@ -61,23 +13,9 @@ def get_project_name(data) -> str: return data["tool"]["poetry"]["name"] -def get_project_dependencies(data) -> dict: - if repo.is_poetry(data): - deps = data["tool"]["poetry"].get("dependencies", []) - - items = set(deps.keys()) - else: - deps = data["project"].get("dependencies", []) - - items = {re.split(r"[\^~=!<>]", dep)[0] for dep in deps} - - return {"items": items, "source": repo.default_toml} - - @lru_cache def get_toml(path: Path) -> tomlkit.TOMLDocument: - with path.open(encoding="utf-8", errors="ignore") as f: - return tomlkit.loads(f.read()) + return toml.read_toml_document(path) def get_project_files(root: Path) -> dict: @@ -104,16 +42,16 @@ def get_toml_files(root: Path) -> List[dict]: def get_packages_for_projects(root: Path) -> List[dict]: - tomls = get_toml_files(root) + toml_files = get_toml_files(root) namespace = workspace.parser.get_namespace_from_config(root) return [ { "name": get_project_name(d["toml"]), - "packages": get_project_package_includes(namespace, d["toml"]), + "packages": toml.get_project_package_includes(namespace, d["toml"]), "path": d["path"], "type": d["type"], - "deps": get_project_dependencies(d["toml"]), + "deps": toml.get_project_dependencies(d["toml"]), } - for d in tomls + for d in toml_files ] diff --git a/components/polylith/repo/repo.py b/components/polylith/repo/repo.py index 7c2ddc7c..08c30ca0 100644 --- a/components/polylith/repo/repo.py +++ b/components/polylith/repo/repo.py @@ -57,12 +57,18 @@ def get_workspace_root(cwd: Path) -> Path: return root +def has_build_requires(pyproject: dict, value: str) -> bool: + backend = pyproject.get("build-system", {}).get("build-backend", {}) + + return value in backend + + def is_poetry(pyproject: dict) -> bool: - return pyproject.get("tool", {}).get("poetry") is not None + return has_build_requires(pyproject, "poetry") def is_hatch(pyproject: dict) -> bool: - return pyproject.get("tool", {}).get("hatch") is not None + return has_build_requires(pyproject, "hatchling") def is_pep_621_ready(pyproject: dict) -> bool: diff --git a/components/polylith/toml/__init__.py b/components/polylith/toml/__init__.py new file mode 100644 index 00000000..579d3f6e --- /dev/null +++ b/components/polylith/toml/__init__.py @@ -0,0 +1,13 @@ +from polylith.toml.core import ( + get_project_dependencies, + get_project_package_includes, + get_project_packages_from_polylith_section, + read_toml_document, +) + +__all__ = [ + "get_project_dependencies", + "get_project_package_includes", + "get_project_packages_from_polylith_section", + "read_toml_document", +] diff --git a/components/polylith/toml/core.py b/components/polylith/toml/core.py new file mode 100644 index 00000000..e09f0544 --- /dev/null +++ b/components/polylith/toml/core.py @@ -0,0 +1,67 @@ +import re +from pathlib import Path +from typing import List + +import tomlkit +from polylith import repo + + +def transform_to_package(namespace: str, include: str) -> dict: + path, _separator, brick = str.partition(include, f"/{namespace}/") + + return {"include": f"{namespace}/{brick}", "from": path} + + +def get_project_packages_from_polylith_section(data) -> dict: + bricks = data["tool"].get("polylith", {}).get("bricks") + + return bricks if isinstance(bricks, dict) else {} + + +def get_hatch_project_packages(data) -> dict: + hatch_data = data["tool"]["hatch"] + build_data = hatch_data.get("build", {}) if isinstance(hatch_data, dict) else {} + + force_included = build_data.get("force-include") + + if force_included: + return force_included + + return get_project_packages_from_polylith_section(data) + + +def get_project_package_includes(namespace: str, data) -> List[dict]: + if repo.is_poetry(data): + return data["tool"]["poetry"].get("packages", []) + + if repo.is_hatch(data): + includes = get_hatch_project_packages(data) + + return [transform_to_package(namespace, key) for key in includes.keys()] + + return [] + + +def get_project_name(data) -> str: + if repo.is_pep_621_ready(data): + return data["project"]["name"] + + return data["tool"]["poetry"]["name"] + + +def get_project_dependencies(data) -> dict: + if repo.is_poetry(data): + deps = data["tool"]["poetry"].get("dependencies", []) + + items = set(deps.keys()) + else: + deps = data["project"].get("dependencies", []) + + items = {re.split(r"[\^~=!<>]", dep)[0] for dep in deps} + + return {"items": items, "source": repo.default_toml} + + +def read_toml_document(path: Path) -> tomlkit.TOMLDocument: + with path.open(encoding="utf-8", errors="ignore") as f: + return tomlkit.loads(f.read()) diff --git a/poetry.lock b/poetry.lock index 7743811d..b20ff494 100644 --- a/poetry.lock +++ b/poetry.lock @@ -459,6 +459,17 @@ https = ["urllib3 (>=1.24.1)"] paramiko = ["paramiko"] pgp = ["gpg"] +[[package]] +name = "editables" +version = "0.5" +description = "Editable installations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "editables-0.5-py3-none-any.whl", hash = "sha256:61e5ffa82629e0d8bfe09bc44a07db3c1ab8ed1ce78a6980732870f19b5e7d4c"}, + {file = "editables-0.5.tar.gz", hash = "sha256:309627d9b5c4adc0e668d8c6fa7bac1ba7c8c5d415c2d27f60f081f8e80d1de2"}, +] + [[package]] name = "exceptiongroup" version = "1.2.0" @@ -519,6 +530,25 @@ mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.11.0,<2.12.0" pyflakes = ">=3.2.0,<3.3.0" +[[package]] +name = "hatchling" +version = "1.21.0" +description = "Modern, extensible Python build backend" +optional = false +python-versions = ">=3.8" +files = [ + {file = "hatchling-1.21.0-py3-none-any.whl", hash = "sha256:b33ef0ecdee6dbfd28c21ca30df459ba1d566050d033f8b5a1d0e26e5606d26b"}, + {file = "hatchling-1.21.0.tar.gz", hash = "sha256:5c086772357a50723b825fd5da5278ac7e3697cdf7797d07541a6c90b6ff754c"}, +] + +[package.dependencies] +editables = ">=0.3" +packaging = ">=21.3" +pathspec = ">=0.10.1" +pluggy = ">=1.0.0" +tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} +trove-classifiers = "*" + [[package]] name = "idna" version = "3.6" @@ -1477,4 +1507,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "de65e90775c58e5e930efa83146526a3879827f46296afad0d4031081992315a" +content-hash = "28699790093c8c0b68111d1a436c94c72469405980dd2dfc3d57eb4288f61c56" diff --git a/projects/hatch_polylith_bricks/README.md b/projects/hatch_polylith_bricks/README.md new file mode 100644 index 00000000..954ad727 --- /dev/null +++ b/projects/hatch_polylith_bricks/README.md @@ -0,0 +1,73 @@ +# Hatch Build Hook for Polylith + +A plugin for [Hatch](https://github.com/pypa/hatch) and the Polylith Architecture. + +This build hook will look for Polylith `bricks` in `pyproject.toml` and __optionally__ re-write the imports made in the source code. + +## Installation +``` toml +[build-system] +requires = ["hatchling", "hatch-polylith-bricks"] +build-backend = "hatchling.build" +``` + +## But why re-write code? +Building libraries is supported in [the Python tools for the Polylith Architecture](https://davidvujic.github.io/python-polylith-docs), +but you will need to consider that code will share the same top namespace with any other library built from the same monorepo. + +This can be a problem when more than one of your libraries are installed into the same virtual environment. +Python libraries by default are installed in a "flat" folder structure, two libraries with the same top namespace will collide. + +_A Solution_: add a custom top namespace during packaging of the library with Hatch and this build hook plugin. + +## How is this done? +The code in this repo uses __AST__ (Abstract Syntax Tree) parsing to modify source code. +The Python built-in `ast` module is used to parse and un-parse Python code. + + +### What's the output from this plugin? + +Without any custom namespace in the configuration: no changes in the code. Building and packaging as-is. + +#### With a Top Namespace configuration + +``` toml +[tool.hatch.build.hooks.polylith-bricks] +top-namespace = "my_custom_namespace" +``` + +```shell +my_custom_namespace/ + my_namespace/ + /my_package + __init__.py + my_module.py +``` + +Before: +```python +from my_namespace.my_package import my_function +``` + +After: +```python +from my_custom_namespace.my_namespace.my_package import my_function +``` + +## Usage +| Key | Default | Description | +| --- | ------- | ----------- | +| work-dir | .polylith_tmp | The temporary working directory for copying and re-writing source code. | +| top-namespace | None | A custom top namespace. When set, Polylith bricks will be updated using this namespace. | + + +This Plugin expects to find Polylith Bricks in the `pyproject.toml`: + +``` toml +[tool.polylith.bricks] +"../../bases/my_namespace/my_base" = "my_namespace/my_base" +"../../components/my_namespace/my_component" = "my_namespace/my_component +``` + +## Documentation +[the Python tools for the Polylith Architecture](https://davidvujic.github.io/python-polylith-docs) diff --git a/projects/hatch_polylith_bricks/poetry.lock b/projects/hatch_polylith_bricks/poetry.lock new file mode 100644 index 00000000..d2119e67 --- /dev/null +++ b/projects/hatch_polylith_bricks/poetry.lock @@ -0,0 +1,95 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "editables" +version = "0.5" +description = "Editable installations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "editables-0.5-py3-none-any.whl", hash = "sha256:61e5ffa82629e0d8bfe09bc44a07db3c1ab8ed1ce78a6980732870f19b5e7d4c"}, + {file = "editables-0.5.tar.gz", hash = "sha256:309627d9b5c4adc0e668d8c6fa7bac1ba7c8c5d415c2d27f60f081f8e80d1de2"}, +] + +[[package]] +name = "hatchling" +version = "1.21.0" +description = "Modern, extensible Python build backend" +optional = false +python-versions = ">=3.8" +files = [ + {file = "hatchling-1.21.0-py3-none-any.whl", hash = "sha256:b33ef0ecdee6dbfd28c21ca30df459ba1d566050d033f8b5a1d0e26e5606d26b"}, + {file = "hatchling-1.21.0.tar.gz", hash = "sha256:5c086772357a50723b825fd5da5278ac7e3697cdf7797d07541a6c90b6ff754c"}, +] + +[package.dependencies] +editables = ">=0.3" +packaging = ">=21.3" +pathspec = ">=0.10.1" +pluggy = ">=1.0.0" +tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} +trove-classifiers = "*" + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "trove-classifiers" +version = "2024.1.8" +description = "Canonical source for classifiers on PyPI (pypi.org)." +optional = false +python-versions = "*" +files = [ + {file = "trove-classifiers-2024.1.8.tar.gz", hash = "sha256:6e36caf430ff6485c4b57a4c6b364a13f6a898d16b9417c6c37467e59c14b05a"}, + {file = "trove_classifiers-2024.1.8-py3-none-any.whl", hash = "sha256:3c1ff4deb10149c7e39ede6e5bbc107def64362ef1ee7590ec98d71fb92f1b6a"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "27ab6a70560989a8ab2926bac614a4cfab206dfe7c1a61f167b44d5dbe3368be" diff --git a/projects/hatch_polylith_bricks/pyproject.toml b/projects/hatch_polylith_bricks/pyproject.toml new file mode 100644 index 00000000..677e3cf8 --- /dev/null +++ b/projects/hatch_polylith_bricks/pyproject.toml @@ -0,0 +1,32 @@ +[tool.poetry] +name = "hatch-polylith-bricks" +version = "0.0.1" +description = "Hatch build hook plugin for Polylith" +authors = ['David Vujic'] +homepage = "https://davidvujic.github.io/python-polylith-docs/" +repository = "https://github.com/davidvujic/python-polylith" +license = "MIT" +readme = "README.md" + +packages = [ + {include = "polylith/hatch_hooks", from = "../../bases"}, + {include = "polylith/repo",from = "../../components"}, + {include = "polylith/parsing",from = "../../components"}, + {include = "polylith/hatch",from = "../../components"}, + {include = "polylith/toml",from = "../../components"}, +] + +classifiers = [ + "Framework :: Hatch", +] + +[tool.poetry.dependencies] +python = "^3.9" +hatchling = "^1.21.0" + +[tool.poetry.plugins.hatch] +polylith-bricks = "polylith.hatch_hooks.hooks" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/projects/poetry_polylith_plugin/pyproject.toml b/projects/poetry_polylith_plugin/pyproject.toml index b55b20ae..0f35ed4d 100644 --- a/projects/poetry_polylith_plugin/pyproject.toml +++ b/projects/poetry_polylith_plugin/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "poetry-polylith-plugin" -version = "1.14.3" +version = "1.14.4" description = "A Poetry plugin that adds tooling support for the Polylith Architecture" authors = ["David Vujic"] homepage = "https://davidvujic.github.io/python-polylith-docs/" @@ -30,6 +30,7 @@ packages = [ {include = "polylith/reporting",from = "../../components"}, {include = "polylith/sync",from = "../../components"}, {include = "polylith/test",from = "../../components"}, + {include = "polylith/toml",from = "../../components"}, {include = "polylith/workspace",from = "../../components"}, ] diff --git a/projects/polylith-cli/README.md b/projects/polylith_cli/README.md similarity index 100% rename from projects/polylith-cli/README.md rename to projects/polylith_cli/README.md diff --git a/projects/polylith-cli/poetry.lock b/projects/polylith_cli/poetry.lock similarity index 100% rename from projects/polylith-cli/poetry.lock rename to projects/polylith_cli/poetry.lock diff --git a/projects/polylith-cli/pyproject.toml b/projects/polylith_cli/pyproject.toml similarity index 96% rename from projects/polylith-cli/pyproject.toml rename to projects/polylith_cli/pyproject.toml index fd334836..b0a15ba9 100644 --- a/projects/polylith-cli/pyproject.toml +++ b/projects/polylith_cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "polylith-cli" -version = "0.4.0" +version = "0.5.0" description = "Python tooling support for the Polylith Architecture" authors = ['David Vujic'] homepage = "https://davidvujic.github.io/python-polylith-docs/" @@ -29,6 +29,7 @@ packages = [ {include = "polylith/reporting",from = "../../components"}, {include = "polylith/sync",from = "../../components"}, {include = "polylith/test",from = "../../components"}, + {include = "polylith/toml",from = "../../components"}, {include = "polylith/workspace",from = "../../components"}, ] diff --git a/pyproject.toml b/pyproject.toml index e5f1547c..59879341 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ readme = "README.md" packages = [ {include = "polylith/poetry_plugin",from = "bases"}, {include = "polylith/cli",from = "bases"}, + {include = "polylith/hatch_hooks", from = "bases"}, {include = "polylith/alias",from = "components"}, {include = "polylith/bricks",from = "components"}, {include = "polylith/check",from = "components"}, @@ -18,10 +19,12 @@ packages = [ {include = "polylith/dirs",from = "components"}, {include = "polylith/distributions",from = "components"}, {include = "polylith/files",from = "components"}, + {include = "polylith/hatch",from = "components"}, {include = "polylith/imports",from = "components"}, {include = "polylith/info",from = "components"}, {include = "polylith/interface",from = "components"}, {include = "polylith/libs",from = "components"}, + {include = "polylith/parsing",from = "components"}, {include = "polylith/poetry",from = "components"}, {include = "polylith/project",from = "components"}, {include = "polylith/readme",from = "components"}, @@ -29,6 +32,7 @@ packages = [ {include = "polylith/reporting",from = "components"}, {include = "polylith/sync",from = "components"}, {include = "polylith/test",from = "components"}, + {include = "polylith/toml",from = "components"}, {include = "polylith/workspace",from = "components"}, {include = "development"}, ] @@ -40,6 +44,7 @@ tomlkit = "^0.11.5" rich = "^13.6.0" typer = {extras = ["all"], version = "^0.9.0"} cleo = "^2.1.0" +hatchling = "^1.21.0" [tool.poetry.group.dev.dependencies] black = "^23.12.1" diff --git a/test/components/polylith/hatch/__init__.py b/test/components/polylith/hatch/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/components/polylith/hatch/test_core.py b/test/components/polylith/hatch/test_core.py new file mode 100644 index 00000000..0e71ac9f --- /dev/null +++ b/test/components/polylith/hatch/test_core.py @@ -0,0 +1,12 @@ +from polylith.hatch import core + + +def test_parse_namespace(): + expected = "unittest" + + bricks = { + f"../../bases/{expected}/one": f"{expected}/one", + f"../../components/{expected}/two": f"{expected}/two", + } + + assert core.parse_namespace(bricks) == expected diff --git a/test/components/polylith/parsing/__init__.py b/test/components/polylith/parsing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/components/polylith/parsing/test_core.py b/test/components/polylith/parsing/test_core.py new file mode 100644 index 00000000..679d734d --- /dev/null +++ b/test/components/polylith/parsing/test_core.py @@ -0,0 +1,14 @@ +from polylith.parsing import core + + +expected_ns = "unittest" +bricks = { + f"../../bases/{expected_ns}/one": f"{expected_ns}/one", + f"../../components/{expected_ns}/two": f"{expected_ns}/two", +} + + +def test_parse_brick_namespace_from_path(): + res = core.parse_brick_namespace_from_path(bricks) + + assert res == {expected_ns} diff --git a/test/components/polylith/repo/test_repo.py b/test/components/polylith/repo/test_repo.py index cb1cc277..251571d5 100644 --- a/test/components/polylith/repo/test_repo.py +++ b/test/components/polylith/repo/test_repo.py @@ -2,7 +2,7 @@ def test_is_pep_621_ready(): - poetry_section = {"tool": {"poetry": {}}} + poetry_section = {"build-system": {"build-backend": "poetry.core.masonry.api"}} project_section = {"project": {"name": "hello world"}} both = {**poetry_section, **project_section} diff --git a/test/components/polylith/sync/test_update.py b/test/components/polylith/sync/test_update.py index f190682f..7017e78d 100644 --- a/test/components/polylith/sync/test_update.py +++ b/test/components/polylith/sync/test_update.py @@ -76,6 +76,10 @@ def test_generate_updated_poetry_project(): """\ [tool.poetry] packages = [{include = "hello/first", from = "bases"}] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" """ ) @@ -89,6 +93,10 @@ def test_generate_updated_poetry_project(): def test_generate_updated_hatch_project(): data = tomlkit.parse( """\ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [tool.hatch.build.force-include] "bases/hello/first" = "hello/first" """ @@ -104,6 +112,10 @@ def test_generate_updated_hatch_project(): def test_generate_updated_hatch_project_with_missing_build_config(): data = tomlkit.parse( """\ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [tool.hatch] hello = "world" """ @@ -119,6 +131,10 @@ def test_generate_updated_hatch_project_with_missing_build_config(): def test_generate_updated_hatch_project_with_missing_force_include_config(): data = tomlkit.parse( """\ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [tool.hatch.build] hello = "world" """ diff --git a/test/components/polylith/toml/__init__.py b/test/components/polylith/toml/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/components/polylith/project/test_project_get.py b/test/components/polylith/toml/test_core.py similarity index 70% rename from test/components/polylith/project/test_project_get.py rename to test/components/polylith/toml/test_core.py index 76b627f8..29d4e1c8 100644 --- a/test/components/polylith/project/test_project_get.py +++ b/test/components/polylith/toml/test_core.py @@ -1,5 +1,5 @@ import tomlkit -from polylith import project +from polylith import toml namespace = "unittest" @@ -9,15 +9,27 @@ {include = "unittest/one",from = "../../bases"}, {include = "unittest/two",from = "../../components"} ] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" """ hatch_toml = """\ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [tool.hatch.build.force-include] "../../bases/unittest/one" = "unittest/one" "../../components/unittest/two" = "unittest/two" """ hatch_toml_alternative = """\ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [tool.hatch.build] something = "something" @@ -27,6 +39,10 @@ """ hatch_toml_combined = """\ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [tool.hatch.build.force-include] "../../bases/unittest/one" = "unittest/one" "../../components/unittest/two" = "unittest/two" @@ -44,7 +60,7 @@ def test_get_poetry_package_includes(): data = tomlkit.loads(poetry_toml) - res = project.get.get_project_package_includes(namespace, data) + res = toml.get_project_package_includes(namespace, data) assert res == expected @@ -52,7 +68,7 @@ def test_get_poetry_package_includes(): def test_get_hatch_package_includes(): data = tomlkit.loads(hatch_toml) - res = project.get.get_project_package_includes(namespace, data) + res = toml.get_project_package_includes(namespace, data) assert res == expected @@ -60,7 +76,7 @@ def test_get_hatch_package_includes(): def test_get_hatch_package_includes_in_build_hook(): data = tomlkit.loads(hatch_toml_alternative) - res = project.get.get_project_package_includes(namespace, data) + res = toml.get_project_package_includes(namespace, data) assert res == expected @@ -68,6 +84,6 @@ def test_get_hatch_package_includes_in_build_hook(): def test_get_hatch_package_includes_from_default_when_in_both(): data = tomlkit.loads(hatch_toml_combined) - res = project.get.get_project_package_includes(namespace, data) + res = toml.get_project_package_includes(namespace, data) assert res == expected