diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 0000000..b7426a1 --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,10 @@ +# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY +_commit: v2024.03.1 +_src_path: gh:astrojuanlu/copier-pylib +author_email: juan_luis_cano@mckinsey.com +author_name: Juan Luis Cano Rodríguez +github_org: astrojuanlu +package_name: kedro_init +project_name: kedro-init +short_description: A simple CLI command that initialises a Kedro project from an existing + Python package diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e519d4e..b528323 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,8 +2,6 @@ name: Publish library on: push: - branches: - - main tags: # Don't try to be smart about PEP 440 compliance, # see https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions @@ -12,6 +10,10 @@ on: jobs: publish: runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + contents: write steps: - uses: actions/checkout@v2 - name: Set up Python @@ -23,11 +25,6 @@ jobs: - name: Build package run: python -m build - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1.4 - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + uses: pypa/gh-action-pypi-publish@release/v1 - name: Create GitHub release uses: softprops/action-gh-release@v1 - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 047393b..04f2989 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -15,7 +15,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions + python -m pip install uv + uv pip install --system tox tox-uv tox-gh-actions - name: Test with tox run: tox diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d12f516..97f2510 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: - id: check-merge-conflict - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 + rev: v0.3.2 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 84c1b2c..ae31699 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ version: 2 build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: python: "3.9" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f14403b..12d6589 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,15 +7,16 @@ make sure that you **read our code of conduct** (`CODE_OF_CONDUCT.md`). ## Contributing code -1. Set up a Python development environment +1. Clone the repository +2. Set up and activate a Python development environment (advice: use [venv](https://docs.python.org/3/library/venv.html), [virtualenv](https://virtualenv.pypa.io/), or [miniconda](https://docs.conda.io/en/latest/miniconda.html)) -2. Install tox: `python -m pip install tox` -3. Clone the repository -4. Start a new branch off master: `git switch -c new-branch master` -5. Make your code changes -6. Check that your code follows the style guidelines of the project: `tox -e reformat && tox -e check` -7. (optional) Build the documentation: `tox -e docs` -8. (optional) Run the tests: `tox -e py39` +3. Install tox: `python -m pip install tox` +4. Make sure the tests run: `tox -e py39` (change the version number according to the Python you are using) -9. Commit, push, and open a pull request! +5. Start a new branch: `git switch -c new-branch main` +6. Make your code changes +7. Check that your code follows the style guidelines of the project: `tox -e reformat && tox -e check` +8. Run the tests again and verify that they pass: `tox -e py39` +9. (optional) Build the documentation: `tox -e docs` +10. Commit, push, and open a pull request! diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d0c3cbf..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source/conf.py b/docs/source/conf.py index b5c6c29..970446c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -8,7 +8,7 @@ project = _metadata["Name"] author = _metadata["Author-email"].split("<", 1)[0].strip() -copyright = f"2022, {author}" +copyright = f"2023, {author}" version = _metadata["Version"] release = ".".join(version.split(".")[:2]) diff --git a/pyproject.toml b/pyproject.toml index 1a2d745..31c2a1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dynamic = ["version"] @@ -56,8 +57,13 @@ doc = [ "sphinx-copybutton", ] +[tool.pdm.version] +source = "scm" + [tool.ruff] show-fixes = true + +[tool.ruff.lint] select = [ "F", # Pyflakes "E", # Pycodestyle @@ -69,17 +75,14 @@ select = [ ] ignore = ["D100", "D103"] -[tool.pdm.version] -source = "scm" +[tool.ruff.lint.pydocstyle] +convention = "pep257" -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "tests/**/*" = ["D", "PLR2004"] -[tool.ruff.pydocstyle] -convention = "pep257" - [tool.mypy] -python_version = "3.7" +python_version = "3.9" warn_redundant_casts = true warn_unused_configs = true pretty = true diff --git a/src/kedro_init/build_config.py b/src/kedro_init/build_config.py index b500e2b..a7f506d 100644 --- a/src/kedro_init/build_config.py +++ b/src/kedro_init/build_config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import tempfile import typing as t import zipfile @@ -9,14 +11,17 @@ try: from importlib.metadata import PackageNotFoundError, version except ModuleNotFoundError: - from importlib_metadata import PackageNotFoundError, version + from importlib_metadata import PackageNotFoundError, version # type: ignore import tomlkit from pygetimportables import _simple_build_wheel, get_top_importables_from_wheel from validate_pyproject import api, errors, plugins +from validate_pyproject.types import Schema -def _get_importables_and_project_name(project_root, outdir): +def _get_importables_and_project_name( + project_root: str | Path, outdir: str | Path +) -> tuple[set[str], str]: wheel_path = _simple_build_wheel(project_root, outdir) with zipfile.ZipFile(wheel_path, "r") as zf: @@ -28,20 +33,22 @@ def _get_importables_and_project_name(project_root, outdir): return package_names, project_name -def kedro_pyproject(tool_name: str) -> dict: - return { - "$id": "https://docs.kedro.org/en/latest/", - "type": "object", - "description": "Kedro project metadata", - "properties": { - "package_name": {"type": "string"}, - "project_name": {"type": "string", "format": "pep508-identifier"}, - "kedro_init_version": {"type": "string", "format": "pep440"}, - "source_dir": {"type": "string"}, - }, - "required": ["package_name", "project_name", "kedro_init_version"], - "additionalProperties": False, - } +def kedro_pyproject(tool_name: str) -> Schema: + return Schema( + { + "$id": "https://docs.kedro.org/en/latest/", + "type": "object", + "description": "Kedro project metadata", + "properties": { + "package_name": {"type": "string"}, + "project_name": {"type": "string", "format": "pep508-identifier"}, + "kedro_init_version": {"type": "string", "format": "pep440"}, + "source_dir": {"type": "string"}, + }, + "required": ["package_name", "project_name", "kedro_init_version"], + "additionalProperties": False, + } + ) def get_or_create_build_config(project_root: Path) -> tuple[bool, t.Any]: @@ -77,10 +84,11 @@ def get_or_create_build_config(project_root: Path) -> tuple[bool, t.Any]: if (project_root / package_name).is_dir(): kedro_config["source_dir"] = "" else: + # FIXME: What happens if package_dir is None? (See type: ignore below) package_dir = next(project_root.glob(f"*/{package_name}"), None) source_dir = package_dir.parent.name if package_dir is not None else None if package_dir is not None and source_dir != "src": - kedro_config["source_dir"] = source_dir + kedro_config["source_dir"] = source_dir # type: ignore return False, kedro_config # Kedro build config might be present, return it if valid @@ -89,10 +97,10 @@ def get_or_create_build_config(project_root: Path) -> tuple[bool, t.Any]: except errors.ValidationError as exc: raise ValueError("Kedro build configuration is invalid") from exc else: - return True, pyproject_toml["tool"]["kedro"] + return True, pyproject_toml["tool"]["kedro"] # type: ignore -def init_build_config(project_root: Path, *, build_config: dict[str, str]): +def init_build_config(project_root: Path, *, build_config: dict[str, str]) -> None: with (project_root / "pyproject.toml").open("r") as fh: pyproject_toml = tomlkit.load(fh) diff --git a/src/kedro_init/cli.py b/src/kedro_init/cli.py index 52a9963..9574a91 100644 --- a/src/kedro_init/cli.py +++ b/src/kedro_init/cli.py @@ -14,7 +14,7 @@ @click.command() @click.argument("project_root", type=click.Path(exists=False)) -def cli(project_root: str): +def cli(project_root: str) -> None: project_root_path = Path(project_root) if rich_available: console = Console() diff --git a/src/kedro_init/config_dirs.py b/src/kedro_init/config_dirs.py index 475711f..02e1280 100644 --- a/src/kedro_init/config_dirs.py +++ b/src/kedro_init/config_dirs.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path CONFIG_DIRS = [ @@ -6,7 +8,9 @@ ] -def get_or_create_config_dirs(project_root: Path, *, expected_config_dirs=None): +def get_or_create_config_dirs( + project_root: Path, *, expected_config_dirs: list[Path] | None = None +) -> dict[Path, tuple[bool, Path]]: if not expected_config_dirs: expected_config_dirs = CONFIG_DIRS @@ -21,5 +25,5 @@ def get_or_create_config_dirs(project_root: Path, *, expected_config_dirs=None): return config_dirs -def init_config_dir(project_root: Path, *, target_config_dir: Path): +def init_config_dir(project_root: Path, *, target_config_dir: Path) -> None: target_config_dir.mkdir(parents=True, exist_ok=True) diff --git a/src/kedro_init/init.py b/src/kedro_init/init.py index 41fb1df..88197bd 100644 --- a/src/kedro_init/init.py +++ b/src/kedro_init/init.py @@ -1,3 +1,4 @@ +import typing as t from pathlib import Path from .build_config import get_or_create_build_config, init_build_config @@ -5,7 +6,7 @@ from .modules import get_or_create_modules, init_module -def init_steps(project_root: Path): +def init_steps(project_root: Path) -> t.Generator[str, None, None]: yield "Looking for existing package directories" existing, build_config = get_or_create_build_config(project_root) @@ -31,6 +32,6 @@ def init_steps(project_root: Path): ) -def init(project_root: Path): +def init(project_root: Path) -> None: for _ in init_steps(project_root): pass diff --git a/src/kedro_init/modules.py b/src/kedro_init/modules.py index eb16b8d..40012c9 100644 --- a/src/kedro_init/modules.py +++ b/src/kedro_init/modules.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path TEMPLATES_PATH = Path(__file__).parent / "templates" @@ -11,29 +13,36 @@ def get_or_create_modules( project_root: Path, *, build_config: dict[str, str], - module_templates: dict[str, str] = None, -): - if not module_templates: - module_templates = MODULE_TEMPLATES + module_templates: dict[str, Path] | None = None, +) -> dict[str, tuple[bool, Path, Path]]: + if module_templates: + module_templates_l = module_templates + else: + module_templates_l = MODULE_TEMPLATES package_name = build_config["package_name"] package_dir = project_root / package_name if not package_dir.is_dir(): - package_dir = next(project_root.glob(f"*/{package_name}"), None) + package_dir = next(project_root.glob(f"*/{package_name}"), None) # type: ignore + + if package_dir is None: + raise ValueError( + f"No suitable directory found for package name '{package_name}'" + ) modules = {} - for module_name in module_templates: + for module_name in module_templates_l: target_module = package_dir / module_name if target_module.exists(): modules[module_name] = True, target_module, target_module else: - modules[module_name] = False, target_module, module_templates[module_name] + modules[module_name] = False, target_module, module_templates_l[module_name] return modules def init_module( project_root: Path, *, target_module_path: Path, module_contents_path: Path -): +) -> None: with target_module_path.open("w") as fh: fh.write(module_contents_path.read_text()) diff --git a/src/kedro_init/templates/pipeline_registry.py b/src/kedro_init/templates/pipeline_registry.py index d3aa4d3..9288fa2 100644 --- a/src/kedro_init/templates/pipeline_registry.py +++ b/src/kedro_init/templates/pipeline_registry.py @@ -11,5 +11,6 @@ def register_pipelines() -> dict[str, Pipeline]: A mapping from pipeline names to ``Pipeline`` objects. """ pipelines = find_pipelines() - pipelines["__default__"] = sum(pipelines.values()) + # https://github.com/kedro-org/kedro/issues/2526 + pipelines["__default__"] = sum(pipelines.values(), start=Pipeline([])) return pipelines diff --git a/tests/test_import.py b/tests/test_import.py deleted file mode 100644 index f130082..0000000 --- a/tests/test_import.py +++ /dev/null @@ -1 +0,0 @@ -import kedro_init diff --git a/tests/test_modules.py b/tests/test_modules.py new file mode 100644 index 0000000..4972185 --- /dev/null +++ b/tests/test_modules.py @@ -0,0 +1,30 @@ +import pytest +from kedro_init.modules import TEMPLATES_PATH, get_or_create_modules + + +@pytest.fixture +def project_setup(tmp_path): + package_name = "test_package" + src_dir = tmp_path / "src" / package_name + src_dir.mkdir(parents=True) + return tmp_path, package_name + + +def test_get_or_create_modules_gets_expected_result(project_setup): + project_root, package_name = project_setup + build_config = {"package_name": package_name} + + result = get_or_create_modules(project_root, build_config=build_config) + + assert result == { + "pipeline_registry.py": ( + False, + project_root / f"src/{package_name}/pipeline_registry.py", + TEMPLATES_PATH / "pipeline_registry.py", + ), + "settings.py": ( + False, + project_root / f"src/{package_name}/settings.py", + TEMPLATES_PATH / "settings.py", + ), + } diff --git a/tox.ini b/tox.ini index 32cf844..c2810e7 100644 --- a/tox.ini +++ b/tox.ini @@ -2,26 +2,25 @@ envlist = check docs - {py37,py38,py39,py310,pypy3}{,-coverage} + {py39,py310,py311,py312,pypy3}{,-coverage} # See https://tox.readthedocs.io/en/latest/example/package.html#flit isolated_build = True isolated_build_env = build [gh-actions] python = - 3.6: py36 - 3.7: py37 - 3.8: py38 3.9: check, py39 3.10: py310 + 3.11: py310 + 3.12: py310 [testenv] basepython = pypy3: pypy3 - py37: python3.7 - py38: python3.8 py39: python3.9 py310: python3.10 + py311: python3.11 + py312: python3.12 # See https://github.com/tox-dev/tox/issues/1548 {check,docs,build}: python3 setenv =