diff --git a/packages/create-python-app-core/src/create_python_app_core/__init__.py b/packages/create-python-app-core/src/create_python_app_core/__init__.py index 44b63d0..5e21b16 100644 --- a/packages/create-python-app-core/src/create_python_app_core/__init__.py +++ b/packages/create-python-app-core/src/create_python_app_core/__init__.py @@ -25,7 +25,12 @@ read_cache_meta, write_cache_meta, ) -from create_python_app_core.paths import default_cache_dir, resolve_source +from create_python_app_core.loaders import load_layer, merge_layers +from create_python_app_core.paths import ( + default_cache_dir, + get_template_dir_path, + resolve_source, +) __all__ = [ "__version__", @@ -42,6 +47,9 @@ "CacheMeta", "read_cache_meta", "write_cache_meta", + "merge_layers", + "load_layer", + "get_template_dir_path", "CPA_USER_AGENT", "check_for_latest_version", "check_python_version", diff --git a/packages/create-python-app-core/src/create_python_app_core/loaders.py b/packages/create-python-app-core/src/create_python_app_core/loaders.py new file mode 100644 index 0000000..d4e0783 --- /dev/null +++ b/packages/create-python-app-core/src/create_python_app_core/loaders.py @@ -0,0 +1,51 @@ +"""Template/extension file loaders and merge model.""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +from create_python_app_core.errors import ManifestLoadError +from create_python_app_core.paths import ResolvedSource, get_template_dir_path + + +def copy_tree(src: Path, dest: Path, *, overwrite: bool = True) -> list[Path]: + """Copy files from src into dest. Returns list of written paths.""" + written: list[Path] = [] + if not src.is_dir(): + raise ManifestLoadError(f"template directory not found: {src}") + dest.mkdir(parents=True, exist_ok=True) + for path in src.rglob("*"): + if path.is_dir(): + continue + rel = path.relative_to(src) + target = dest / rel + if target.exists() and not overwrite: + continue + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, target) + written.append(target) + return written + + +def load_layer( + source: ResolvedSource, + root: Path, + dest: Path, + *, + overwrite: bool = True, +) -> list[Path]: + """Load one template/extension layer into dest.""" + template_root = get_template_dir_path(source, root) + return copy_tree(template_root, dest, overwrite=overwrite) + + +def merge_layers( + layers: list[tuple[ResolvedSource, Path]], + dest: Path, +) -> list[Path]: + """Apply layers in order: template → addons → extend (later wins).""" + written: list[Path] = [] + for source, root in layers: + written.extend(load_layer(source, root, dest, overwrite=True)) + return written diff --git a/packages/create-python-app-core/tests/test_loaders.py b/packages/create-python-app-core/tests/test_loaders.py new file mode 100644 index 0000000..38299e5 --- /dev/null +++ b/packages/create-python-app-core/tests/test_loaders.py @@ -0,0 +1,24 @@ +from pathlib import Path + +from create_python_app_core.loaders import merge_layers +from create_python_app_core.paths import ResolvedSource + + +def _layer(tmp: Path, name: str, files: dict[str, str]) -> tuple[ResolvedSource, Path]: + root = tmp / name + tpl = root / "template" + for rel, content in files.items(): + path = tpl / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + src = ResolvedSource(kind="file", url=f"file://{root}", local_path=root) + return src, root + + +def test_merge_later_wins(tmp_path: Path) -> None: + a = _layer(tmp_path, "a", {"README.md": "a", "keep.txt": "keep"}) + b = _layer(tmp_path, "b", {"README.md": "b"}) + dest = tmp_path / "out" + merge_layers([a, b], dest) + assert (dest / "README.md").read_text() == "b" + assert (dest / "keep.txt").read_text() == "keep"