From b1d92b70a8c65cc6a088fa99eeac3e028950a650 Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Fri, 14 Jun 2024 12:12:02 +0200 Subject: [PATCH] initial working version --- .github/dependabot.yml | 10 +- .github/workflows/ci.yml | 51 ++++ README.md | 84 +++++- cookiecutter.json | 9 + hooks/post_gen_project.py | 73 +++++ noxfile.py | 251 ++++++++++++++++++ pyproject.toml | 93 +++++++ .../.github/dependabot.yml | 12 + .../.github/workflows/ci.yml | 69 +++++ .../.github/workflows/publish.yml | 95 +++++++ {{cookiecutter.package_name}}/CHANGELOG.md | 10 + {{cookiecutter.package_name}}/README.md | 43 +++ .../cookiecutter-rt-pkg/CHANGELOG.md | 15 ++ .../3rd_party/cookiecutter-rt-pkg/USAGE.md | 3 + .../noxfile.py | 88 ++++-- {{cookiecutter.package_name}}/pyproject.toml | 134 ++++++++++ .../{{cookiecutter.package_name}}/__init__.py | 0 .../_internal/__init__.py | 5 + .../{{cookiecutter.package_name}}/py.typed | 0 .../v0/__init__.py | 3 + .../tests/__init__.py | 0 .../tests/conftest.py | 3 + .../tests/django_fixtures.py | 16 ++ .../tests/settings.py | 22 ++ .../tests/unit/__init__.py | 3 + .../tests/unit/api/__init__.py | 5 + .../tests/unit/api/test_setup.py | 4 + .../tests/unit/internal/__init__.py | 5 + .../tests/unit/internal/test_django_setup.py | 5 + .../tests/unit/internal/test_setup.py | 17 ++ .../.github/dependabot.yml | 12 - .../.github/workflows/ci.yml | 43 --- .../pyproject.toml | 50 ---- 33 files changed, 1094 insertions(+), 139 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 cookiecutter.json create mode 100644 hooks/post_gen_project.py create mode 100644 noxfile.py create mode 100644 pyproject.toml create mode 100644 {{cookiecutter.package_name}}/.github/dependabot.yml create mode 100644 {{cookiecutter.package_name}}/.github/workflows/ci.yml create mode 100644 {{cookiecutter.package_name}}/.github/workflows/publish.yml create mode 100644 {{cookiecutter.package_name}}/CHANGELOG.md create mode 100644 {{cookiecutter.package_name}}/README.md create mode 100644 {{cookiecutter.package_name}}/docs/3rd_party/cookiecutter-rt-pkg/CHANGELOG.md create mode 100644 {{cookiecutter.package_name}}/docs/3rd_party/cookiecutter-rt-pkg/USAGE.md rename {{{cookiecutter.repostory_name}} => {{cookiecutter.package_name}}}/noxfile.py (56%) create mode 100644 {{cookiecutter.package_name}}/pyproject.toml create mode 100644 {{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/__init__.py create mode 100644 {{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/_internal/__init__.py create mode 100644 {{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/py.typed create mode 100644 {{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/v0/__init__.py create mode 100644 {{cookiecutter.package_name}}/tests/__init__.py create mode 100644 {{cookiecutter.package_name}}/tests/conftest.py create mode 100644 {{cookiecutter.package_name}}/tests/django_fixtures.py create mode 100644 {{cookiecutter.package_name}}/tests/settings.py create mode 100644 {{cookiecutter.package_name}}/tests/unit/__init__.py create mode 100644 {{cookiecutter.package_name}}/tests/unit/api/__init__.py create mode 100644 {{cookiecutter.package_name}}/tests/unit/api/test_setup.py create mode 100644 {{cookiecutter.package_name}}/tests/unit/internal/__init__.py create mode 100644 {{cookiecutter.package_name}}/tests/unit/internal/test_django_setup.py create mode 100644 {{cookiecutter.package_name}}/tests/unit/internal/test_setup.py delete mode 100644 {{cookiecutter.repostory_name}}/.github/dependabot.yml delete mode 100644 {{cookiecutter.repostory_name}}/.github/workflows/ci.yml delete mode 100644 {{cookiecutter.repostory_name}}/pyproject.toml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dce75dd..ad4e545 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,14 +1,12 @@ version: 2 updates: - package-ecosystem: "pip" - directory: "/{{cookiecutter.repostory_name}}/app/src" + directory: "/{{cookiecutter.package_name}}/" schedule: interval: "daily" + open-pull-requests-limit: 0 # Only security updates will be opened as PRs - package-ecosystem: "docker" - directory: "/{{cookiecutter.repostory_name}}/app/envs/prod" - schedule: - interval: "weekly" - - package-ecosystem: "docker" - directory: "/docker" + directory: "/{{cookiecutter.package_name}}/" # specify the directory to scan for dependency files, e.g., "/" schedule: interval: "weekly" + open-pull-requests-limit: 0 # Only security updates will be opened as PRs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0342936 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: Continuous Integration + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +env: + PYTHON_DEFAULT_VERSION: "3.12" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Python ${{ env.PYTHON_DEFAULT_VERSION }} + uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_DEFAULT_VERSION }} + cache: 'pip' + cache-dependency-path: | + **/pyproject.toml + **/pdm.lock + - name: Install dependencies + run: python -m pip install --upgrade nox pdm + - name: Run linters + run: nox -vs lint + check_crufted_project: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cruft_config: ["default", "django"] + env: + CRUFT_TESTED_CONFIGS: ${{ matrix.cruft_config }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Python ${{ env.PYTHON_DEFAULT_VERSION }} + uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_DEFAULT_VERSION }} + cache: 'pip' + - name: Install dependencies + run: python -m pip install --upgrade nox pdm + - name: Run checks on project created from template + run: nox -vt crufted_project diff --git a/README.md b/README.md index ca250d5..d8f95bc 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,81 @@ # cookiecutter-rt-pkg -Cookiecutter for python packages +Opinionated cookiecutter for Python packages. -### TODO: +# Features -Must haves: -* [ ] towncrier for changelog -* [ ] CD (pypi) that runs CI workflow first -* [ ] CI for cookiecutter itself (see https://github.com/reef-technologies/cookiecutter-rt-django/blob/master/.github/workflows/ci.yml) -* [ ] rudimentary template readme +Packages created from this template get following features: + +- CI with GitHub Actions +- Trusted Publishing to PyPI with GitHub Actions +- Signed release files using sigstore +- Pre-configured nox for testing (pytest), autoformatting and auto linting (ruff) +- Testing against multiple Python versions, operating systems, and optionally, Django versions +- Package layout and release process with SemVer & ApiVer in mind +- Towncrier for conflict free changelogs + +## Template features + +- CI for cookiecutter template itself + +## Planned features + +High priority: + +- [ ] testing utils for testing ApiVer modules Nice to haves: -* [] Contributing guidelines -* [] PR templates \ No newline at end of file + +- [ ] CD should require CI tests to pass first +- [ ] excluding some django-python combos in nox test matrix (allow to test of Django5+Python3.12 and Django4+Python3.9 but not Django5+Python3.9) +- [ ] Contributing guidelines +- [ ] PR templates +- [ ] ability to build binary & python version-specific wheels + +## Usage + +[cruft](https://github.com/cruft/cruft) is used to manage the template, you can install it with: + +```sh +pip install cruft +``` + +### To generate a new package: + +1. Setup empty repository on GitHub +2. Run: + +```sh +cruft create https://github.com/reef-technologies/cookiecutter-rt-pkg +``` + +3. Configure Trusted Publishers to allow GitHub Actions to publish to PyPI, follow instructions in [{{cookiecutter.package\_name}}/.github/workflows/publish.yml](.github/workflows/publish.yml) + +### When you wish to update your project to the latest version of the template: + +```sh +cruft update +``` + +Before committing make sure to review changes listed in [docs/3rd\_party/cookiecutter-rt-pkg/CHANGELOG.md](docs/3rd_party/cookiecutter-rt-pkg/CHANGELOG.md). + +### Linking an existing repository + +If you have an existing repo, you can link it to the template by running: + +```sh +cruft link https://github.com/reef-technologies/cookiecutter-rt-django +``` + +# Contributing + +When proposing new features or changes, please make sure there are also applicable to the application template [cookiecutter-rt-django](https://github.com/reef-technologies/cookiecutter-rt-django). +This is to ensure we do not try to solve the same problem in two different ways. + +## License + +This project is licensed under the terms of the [BSD-3 License](/LICENSE) + +## Changelog + +Breaking changes are documented in the [CHANGELOG]({{cookiecutter.package_name}}/docs/3rd_party/cookiecutter-rt-django/CHANGELOG.md) diff --git a/cookiecutter.json b/cookiecutter.json new file mode 100644 index 0000000..b1c8a31 --- /dev/null +++ b/cookiecutter.json @@ -0,0 +1,9 @@ +{ + "package_name": "xxx", + "repository_github_url": "https://github.com/reef-technologies/xxx", + "is_django_package": "n", + "_jinja2_env_vars": { + "block_start_string": "# COOKIECUTTER{%", + "block_end_string": "%}" + } +} diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py new file mode 100644 index 0000000..657b8f5 --- /dev/null +++ b/hooks/post_gen_project.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +import contextlib +import importlib.util +import os +import pathlib +import re +import subprocess + +PROJECT_ROOT = pathlib.Path().resolve() +NOXFILE_PATH = PROJECT_ROOT / "noxfile.py" + + +def is_django_support_enabled(): + """ + Check if DJANGO SUPPORT is enabled. + """ + # Since .cruft.json is not available at this point, we need to check from noxfile.py. + spec = importlib.util.spec_from_file_location("noxfile", NOXFILE_PATH) + noxfile = importlib.util.module_from_spec(spec) + spec.loader.exec_module(noxfile) + + return hasattr(noxfile, "DJANGO_VERSIONS") + + +_DJANGO_ONL_FILE_MARKER = re.compile( + r"^# cookiecutter-rt-pkg macro: requires cookiecutter.is_django_package$", + re.MULTILINE, +) + + +def get_django_specific_files(): + """ + Find all files with `# cookiecutter-rt-pkg macro: requires cookiecutter.is_django_package` line. + """ + for file_path in PROJECT_ROOT.rglob("*.py"): + with file_path.open() as f: + try: + content = f.read() + except UnicodeDecodeError: + continue + without_marker_content = _DJANGO_ONL_FILE_MARKER.sub("", content) + if content != without_marker_content: + yield file_path, content + + +@contextlib.contextmanager +def working_directory(path): + """Changes working directory and returns to previous on exit.""" + prev_cwd = pathlib.Path.cwd() + os.chdir(path) + try: + yield + finally: + os.chdir(prev_cwd) + + +def main(): + django_support = is_django_support_enabled() + print(f"{django_support=!r}") + for filepath, new_content in get_django_specific_files(): + if django_support: + with filepath.open("w") as f: + f.write(new_content) + else: + print(f"Removing django specific {filepath}") + filepath.unlink() + + with working_directory(PROJECT_ROOT): + subprocess.check_call(["pdm", "lock", "--update-reuse"]) + + +if __name__ == "__main__": + main() diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..bd58abf --- /dev/null +++ b/noxfile.py @@ -0,0 +1,251 @@ +""" +nox configuration for cookiecutter project template. +""" + +from __future__ import annotations + +import contextlib +import functools +import hashlib +import json +import os +import subprocess +import tempfile +from pathlib import Path + +import nox + +CI = os.environ.get("CI") is not None + +ROOT = Path(".") +PYTHON_VERSIONS = ["3.11"] +PYTHON_DEFAULT_VERSION = PYTHON_VERSIONS[-1] + +# tested default config overrides +CRUFT_TESTED_CONFIG_MATRIX = { + "default": {}, + "django": {"is_django_package": "y"}, +} +CRUFT_TESTED_CONFIGS = os.getenv("CRUFT_TESTED_CONFIGS", ",".join(CRUFT_TESTED_CONFIG_MATRIX)).split(",") + +nox.options.default_venv_backend = "venv" +nox.options.reuse_existing_virtualenvs = True + + +# In CI, use Python interpreter provided by GitHub Actions +if CI: + nox.options.force_venv_backend = "none" + + +MD_PATHS = ["*.md"] + + +def get_cruft_config(config_name="default", **kw): + with (Path(__file__).parent / "cookiecutter.json").open() as f: + cruft_config = json.load(f) + overrides = CRUFT_TESTED_CONFIG_MATRIX[config_name] + complete_config = {**cruft_config, **overrides, **kw} + config_hash = hashlib.sha256(json.dumps(complete_config, sort_keys=True).encode()).hexdigest() + complete_config["package_name"] = f"{config_name}_{config_hash[:8]}" + return complete_config + + +@contextlib.contextmanager +def with_dirty_commit(session): + """ + Returned context manager will commit changes to the git repository if it is dirty. + + This is needed because tools like `cruft` only use committed changes. + """ + is_dirty = not CI and subprocess.run(["git", "diff", "--quiet"], check=False).returncode + if is_dirty: + with tempfile.TemporaryDirectory(prefix="rt_tmpl_repo") as tmpdir: + session.log(f"Found dirty git repository, temporarily committing changes in {tmpdir}") + subprocess.run(["cp", "-r", ".", tmpdir], check=True) + with session.chdir(tmpdir): + subprocess.run(["git", "add", "-A"], check=True) + subprocess.run(["git", "commit", "-m", "nox: dirty commit"], check=True) + yield + else: + yield + + +@functools.lru_cache +def _list_files() -> list[Path]: + file_list = [] + for cmd in ( + ["git", "ls-files"], + ["git", "ls-files", "--others", "--exclude-standard"], + ): + cmd_result = subprocess.run(cmd, check=True, text=True, capture_output=True) + file_list.extend(cmd_result.stdout.splitlines()) + return [Path(p) for p in file_list] + + +def list_files(suffix: str | None = None) -> list[Path]: + """List all non-files not-ignored by git.""" + file_paths = _list_files() + if suffix is not None: + file_paths = [p for p in file_paths if p.suffix == suffix] + return file_paths + + +def run_readable(session, mode="check"): + session.run( + "docker", + "run", + "--platform", + "linux/amd64", + "--rm", + "-v", + f"{ROOT.absolute()}:/data", + "-w", + "/data", + "ghcr.io/bobheadxi/readable:v0.5.0@sha256:423c133e7e9ca0ac20b0ab298bd5dbfa3df09b515b34cbfbbe8944310cc8d9c9", + mode, + *MD_PATHS, + external=True, + ) + + +def run_shellcheck(session, mode="check"): + shellcheck_cmd = [ + "docker", + "run", + "--platform", + "linux/amd64", # while this image is multi-arch, we cannot use digest with multi-arch images + "--rm", + "-v", + f"{ROOT.absolute()}:/mnt", + "-w", + "/mnt", + "-q", + "koalaman/shellcheck:0.9.0@sha256:a527e2077f11f28c1c1ad1dc784b5bc966baeb3e34ef304a0ffa72699b01ad9c", + ] + + files = list_files(suffix=".sh") + if not files: + session.log("No shell files found") + return + shellcheck_cmd.extend(files) + + if mode == "fmt": + with tempfile.NamedTemporaryFile(mode="w+") as diff_file: + session.run( + *shellcheck_cmd, + "--format=diff", + external=True, + stdout=diff_file, + success_codes=[0, 1], + ) + diff_file.seek(0) + diff = diff_file.read() + if len(diff.splitlines()) > 1: # ignore single-line message + session.log("Applying shellcheck patch:\n%s", diff) + subprocess.run( + ["patch", "-p1"], + input=diff, + text=True, + check=True, + ) + + session.run(*shellcheck_cmd, external=True) + + +@nox.session(name="format", python=PYTHON_DEFAULT_VERSION) +def format_(session): + """Lint the code and apply fixes in-place whenever possible.""" + session.run("pip", "install", "-e", ".[format]") + session.run("ruff", "check", "--fix", ".") + run_shellcheck(session, mode="fmt") + run_readable(session, mode="fmt") + session.run("ruff", "format", ".") + + +@nox.session(python=PYTHON_DEFAULT_VERSION) +def lint(session): + """Run linters in readonly mode.""" + session.run("pip", "install", "-e", ".[lint]") + session.run("ruff", "check", "--diff", "--unsafe-fixes", ".") + session.run("codespell", ".") + run_shellcheck(session, mode="check") + run_readable(session, mode="check") + session.run("ruff", "format", "--diff", ".") + + +@contextlib.contextmanager +def crufted_project(session, cruft_config): + session.run("pip", "install", "-e", ".") + + tmpdir = crufted_project.tmpdir + if not tmpdir: + session.notify("cleanup_crufted_project") + crufted_project.tmpdir = tmpdir = tempfile.TemporaryDirectory(prefix="rt_crufted_") + tmpdir_path = Path(tmpdir.name) + tmpdir_path.mkdir(exist_ok=True) + + project_path = tmpdir_path / cruft_config["package_name"] + if not project_path.exists(): + session.log("Creating project in %s", tmpdir.name) + # project_path.mkdir(parents=True) + with with_dirty_commit(session): + session.run( + "cruft", + "create", + ".", + "--output-dir", + str(tmpdir_path), + "--no-input", + "--extra-context", + json.dumps(cruft_config), + ) + with session.chdir(project_path): + session.run("git", "init", external=True) + + with session.chdir(project_path): + yield project_path + + +crufted_project.tmpdir = None + + +def rm_root_owned(session, dirpath): + assert not ROOT.is_relative_to(dirpath) # sanity check before we nuke dirpath + children = sorted(dirpath.iterdir()) + session.run( + "docker", + "run", + "--rm", + "-v", + f"{dirpath}:/tmpdir/", + "alpine:3.18.0", + "rm", + "-rf", + *[f"/tmpdir/{f.name}" for f in children], + external=True, + ) + + +@nox.session(python=PYTHON_DEFAULT_VERSION, tags=["crufted_project"]) +@nox.parametrize("cruft_config_name", CRUFT_TESTED_CONFIGS) +def lint_crufted_project(session, cruft_config_name): + cruft_config = get_cruft_config(cruft_config_name) + with crufted_project(session, cruft_config): + session.run("nox", "-t", "lint") + + +@nox.session(python=PYTHON_DEFAULT_VERSION, tags=["crufted_project"]) +@nox.parametrize("cruft_config_name", CRUFT_TESTED_CONFIGS) +def test_crufted_project(session, cruft_config_name): + cruft_config = get_cruft_config(cruft_config_name) + with crufted_project(session, cruft_config): + session.run("nox", "-t", "test") + + +@nox.session(python=PYTHON_DEFAULT_VERSION) +def cleanup_crufted_project(session): + if crufted_project.tmpdir: + # workaround for docker compose creating root-owned files + rm_root_owned(session, Path(crufted_project.tmpdir.name)) + crufted_project.tmpdir.cleanup() + crufted_project.tmpdir = None diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..75399bb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,93 @@ +[project] +name = "cookiecutter-rt-pkg" +description = "RT pkg template - don't install try to install this as package, use it as cruft template" +version = "0" +license = {file = "LICENSE"} +authors = [ + {name = "Reef Technologies", email = "opensource@reef.pl"}, +] +requires-python = ">=3.11" + +dependencies = [ + "codespell", + "cruft", + "nox", + "ruff", +] + +[tool.ruff] +line-length = 120 +exclude = [ + "\\{\\{cookiecutter.package_name\\}\\}", +] + +[tool.ruff.lint] +select = [ + "D", + "E", "F", "I", "UP", + "TCH005", +] +ignore = [ + "D100", "D105", "D107", "D200", "D202", "D203", "D205", "D212", "D400", "D401", "D415", + "D101", "D102", "D103", "D104", # TODO remove once we have docstring for all public methods +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] +"test/**" = ["D", "F403", "F405"] + +[tool.codespell] +skip = "*.min.js,pdm.lock" +ignore-words-list = "datas" + +[tool.towncrier] +directory = "changelog.d" +filename = "\\{\\{cookiecutter.repostory_name\\}\\}/docs/3rd_party/cookiecutter-rt-pkg/CHANGELOG.md" +start_string = "\n" +underlines = ["", "", ""] +title_format = "## [{version}](https://github.com/reef-technologies/cookiecutter-rt-django/releases/tag/v{version}) - {project_date}" +issue_format = "[#{issue}](https://github.com/reef-technologies/cookiecutter-rt-django/issues/{issue})" + +[[tool.towncrier.type]] +directory = "removed" +name = "Removed" +showcontent = true + +[[tool.towncrier.type]] +directory = "changed" +name = "Changed" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixed" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecated" +name = "Deprecated" +showcontent = true + +[[tool.towncrier.type]] +directory = "added" +name = "Added" +showcontent = true + +[[tool.towncrier.type]] +directory = "doc" +name = "Doc" +showcontent = true + +[[tool.towncrier.type]] +directory = "infrastructure" +name = "Infrastructure" +showcontent = true + +[tool.mypy] + +[[tool.mypy.overrides]] +module = [ + "nox", + "pytest", +] +ignore_missing_imports = true diff --git a/{{cookiecutter.package_name}}/.github/dependabot.yml b/{{cookiecutter.package_name}}/.github/dependabot.yml new file mode 100644 index 0000000..41f5a4b --- /dev/null +++ b/{{cookiecutter.package_name}}/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" # specify the directory to scan for dependency files, e.g., "/" + schedule: + interval: "daily" + open-pull-requests-limit: 0 # Only security updates will be opened as PRs + - package-ecosystem: "docker" + directory: "/" # specify the directory to scan for dependency files, e.g., "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 0 # Only security updates will be opened as PRs diff --git a/{{cookiecutter.package_name}}/.github/workflows/ci.yml b/{{cookiecutter.package_name}}/.github/workflows/ci.yml new file mode 100644 index 0000000..7d69668 --- /dev/null +++ b/{{cookiecutter.package_name}}/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: Continuous Integration + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +env: + PYTHON_DEFAULT_VERSION: "3.12" + +jobs: + linter: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 +# COOKIECUTTER{%- raw %} + - name: Set up Python ${{ env.PYTHON_DEFAULT_VERSION }} + uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_DEFAULT_VERSION }} + cache: "pip" + cache-dependency-path: 'pdm.lock' + - uses: actions/cache@v3 + with: + path: ~/.cache/pdm + key: ${{ env.PYTHON_DEFAULT_VERSION }}-pdm-${{ hashFiles('pdm.lock') }} + restore-keys: ${{ env.PYTHON_DEFAULT_VERSION }}-pdm- +# COOKIECUTTER{%- endraw %} + - name: Install dependencies + run: python -m pip install --upgrade nox 'pdm>=2.12,<3' + - name: Run linters + run: nox -vs lint + test: + timeout-minutes: 10 +# COOKIECUTTER{%- raw %} + runs-on: ${{ matrix.os }} +# COOKIECUTTER{%- endraw %} + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest"] + python-version: ["3.11", "3.12"] + # COOKIECUTTER{%- if cookiecutter.is_django_package == "y" %} + django-version: ["3.2", "4.2"] + # COOKIECUTTER{%- endif %} +# COOKIECUTTER{%- raw %} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: 'pdm.lock' + - uses: actions/cache@v3 + with: + path: ~/.cache/pdm + key: ${{ matrix.python-version }}-pdm-${{ hashFiles('pdm.lock') }} + restore-keys: ${{ matrix.python-version }}-pdm- + - name: Install dependencies + run: python -m pip install --upgrade 'nox==2024.3.2' 'pdm==2.13.2' + - name: Run unit tests + run: nox -vs test +# COOKIECUTTER{%- endraw %} diff --git a/{{cookiecutter.package_name}}/.github/workflows/publish.yml b/{{cookiecutter.package_name}}/.github/workflows/publish.yml new file mode 100644 index 0000000..57c8690 --- /dev/null +++ b/{{cookiecutter.package_name}}/.github/workflows/publish.yml @@ -0,0 +1,95 @@ +name: "Continuous Deployment" + +# This workflow requires https://docs.pypi.org/trusted-publishers/ to be enabled for the repository. +# Follow instructions from this link to enable it. +# Use this workflow (`publish.yml`) in the configuration. +# Please note this process has to be repeated for Test PyPI and PyPI separately. + +on: + push: + tags: + - 'v*' # push events to matching v*, i.e. v1.0, v20.15.10 + - 'draft/v*' + +env: + PYTHON_DEFAULT_VERSION: "3.12" + +jobs: +# COOKIECUTTER{%- raw %} + publish: + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + permissions: + id-token: write # allows publishing to PyPI + contents: write # allows uploading a GitHub release + runs-on: ubuntu-latest + steps: + - name: Get version from tag + id: get-version + run: | + if [[ ${{ github.ref }} == refs/tags/v* ]]; then + echo "draft=false" >> "$GITHUB_OUTPUT" + echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" + else + echo "draft=true" >> "$GITHUB_OUTPUT" + echo "version=${GITHUB_REF#refs/tags/draft/v}" >> "$GITHUB_OUTPUT" + fi + export IS_PRERELEASE=$([[ ${{ github.ref }} =~ [^0-9]$ ]] && echo true || echo false) + echo "prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python ${{ env.PYTHON_DEFAULT_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_DEFAULT_VERSION }} + + - name: Install dependencies + run: python -m pip install --upgrade nox 'pdm>=2.12,<3' + + - name: Read the Changelog + id: read-changelog + uses: mindsers/changelog-reader-action@v2 + with: + version: ${{ steps.get-version.outputs.version }} + path: ./CHANGELOG.md + continue-on-error: ${{ fromJSON(steps.get-version.outputs.draft) }} + + - name: Build + run: pdm build + + - name: Sign distribution + uses: sigstore/gh-action-sigstore-python@v2.1.1 + with: + inputs: >- + dist/*.tar.gz + dist/*.whl + + - name: Create GitHub release + id: create-release + uses: softprops/action-gh-release@v1 + with: + name: ${{ steps.get-version.outputs.version }} + body: ${{ steps.read-changelog.outputs.changes }} + draft: ${{ fromJSON(steps.get-version.outputs.draft)}} + prerelease: ${{ fromJSON(steps.get-version.outputs.prerelease) }} + files: >- + dist/*.tar.gz + dist/*.whl + dist/*.sigstore + + - name: Remove signature files as pypa/gh-action-pypi-publish does not support them + run: rm -f dist/*.sigstore + + - name: Publish distribution 📦 to TestPyPI + if: ${{ steps.get-version.outputs.draft == 'true' }} + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + - name: Publish distribution 📦 to PyPI + if: ${{ steps.get-version.outputs.draft == 'false' }} + uses: pypa/gh-action-pypi-publish@release/v1 +# COOKIECUTTER{%- endraw %} diff --git a/{{cookiecutter.package_name}}/CHANGELOG.md b/{{cookiecutter.package_name}}/CHANGELOG.md new file mode 100644 index 0000000..e3f36bc --- /dev/null +++ b/{{cookiecutter.package_name}}/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the changes for the +upcoming release can be found in [changelog.d](changelog.d). + + diff --git a/{{cookiecutter.package_name}}/README.md b/{{cookiecutter.package_name}}/README.md new file mode 100644 index 0000000..fd0cfe6 --- /dev/null +++ b/{{cookiecutter.package_name}}/README.md @@ -0,0 +1,43 @@ +# {{cookiecutter.package_name}} + + +## Usage + +> [!IMPORTANT] +> This package uses [ApiVer](#versioning), make sure to import `{{cookiecutter.package_name}}.v1`. + + +## Versioning + +This package uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +TL;DR you are safe to use [compatible release version specifier](https://packaging.python.org/en/latest/specifications/version-specifiers/#compatible-release) `~=MAJOR.MINOR` in your `pyproject.toml` or `requirements.txt`. + +Additionally, this package uses [ApiVer](https://www.youtube.com/watch?v=FgcoAKchPjk) to further reduce the risk of breaking changes. +This means, the public API of this package is explicitly versioned, e.g. `{{cookiecutter.package_name}}.v1`, and will not change in a backwards-incompatible way even when `{{cookiecutter.package_name}}.v2` is released. + +Internal packages, i.e. prefixed by `{{cookiecutter.package_name}}._` do not share these guarantees and may change in a backwards-incompatible way at any time even in patch releases. + + +## Development + + +Pre-requisites: +- [pdm](https://pdm.fming.dev/) +- [nox](https://nox.thea.codes/en/stable/) +- [docker](https://www.docker.com/) and [docker compose plugin](https://docs.docker.com/compose/) + + +Ideally, you should run `nox -t format lint` before every commit to ensure that the code is properly formatted and linted. +Before submitting a PR, make sure that tests pass as well, you can do so using: +``` +nox -t check # equivalent to `nox -t format lint test` +``` + +If you wish to install dependencies into `.venv` so your IDE can pick them up, you can do so using: +``` +pdm install --dev +``` + +### Release process + +Run `nox -s make_release -- X.Y.Z` where `X.Y.Z` is the version you're releasing and follow the printed instructions. diff --git a/{{cookiecutter.package_name}}/docs/3rd_party/cookiecutter-rt-pkg/CHANGELOG.md b/{{cookiecutter.package_name}}/docs/3rd_party/cookiecutter-rt-pkg/CHANGELOG.md new file mode 100644 index 0000000..f995542 --- /dev/null +++ b/{{cookiecutter.package_name}}/docs/3rd_party/cookiecutter-rt-pkg/CHANGELOG.md @@ -0,0 +1,15 @@ +# cookiecutter-rt-pkg Changelog + +Main purpose of this file is to provide a changelog for the template itself. +It is not intended to be used as a changelog for the generated project. + +This changelog will document any know **BREAKING** changes between versions of the template. +Please review this new entries carefully after applying `cruft update` before committing the changes. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +Currently, `cookiecutter-rt-pkg` has no explicit versioning amd we purely rely on `cruft` diff. + +## [Unreleased] + +* **BREAKING** Introduced cookiecutter-rt-pkg template diff --git a/{{cookiecutter.package_name}}/docs/3rd_party/cookiecutter-rt-pkg/USAGE.md b/{{cookiecutter.package_name}}/docs/3rd_party/cookiecutter-rt-pkg/USAGE.md new file mode 100644 index 0000000..c9efaba --- /dev/null +++ b/{{cookiecutter.package_name}}/docs/3rd_party/cookiecutter-rt-pkg/USAGE.md @@ -0,0 +1,3 @@ +# Reef Technologies package cookiecutter template usage + + diff --git a/{{cookiecutter.repostory_name}}/noxfile.py b/{{cookiecutter.package_name}}/noxfile.py similarity index 56% rename from {{cookiecutter.repostory_name}}/noxfile.py rename to {{cookiecutter.package_name}}/noxfile.py index 45d81db..525f0b1 100644 --- a/{{cookiecutter.repostory_name}}/noxfile.py +++ b/{{cookiecutter.package_name}}/noxfile.py @@ -1,5 +1,9 @@ +from __future__ import annotations + +import argparse import functools import os +import re import subprocess import sys import tempfile @@ -12,24 +16,38 @@ CI = os.environ.get("CI") is not None ROOT = Path(".") -PYTHON_VERSIONS = ["3.9", "3.11", "3.12"] +MAIN_BRANCH_NAME = "master" +PYTHON_VERSIONS = ["3.11", "3.12"] PYTHON_DEFAULT_VERSION = PYTHON_VERSIONS[-1] +# COOKIECUTTER{%- if cookiecutter.is_django_package == "y" %} +DJANGO_VERSIONS = ["3.2", "4.2"] +DEMO_APP_DIR = ROOT / "demo" +# COOKIECUTTER{%- endif %} + nox.options.default_venv_backend = "venv" nox.options.stop_on_first_error = True nox.options.reuse_existing_virtualenvs = not CI -# In CI, use Python interpreter provided by GitHub Actions if CI: - PYTHON_VERSIONS = [sys.executable] # we don't disable venv, since we use it with pdm anyhow + # In CI, use Python interpreter provided by GitHub Actions + PYTHON_VERSIONS = [sys.executable] -def install(session: nox.Session, *args): - groups = [] - for group in args: - groups.extend(["--group", group]) - session.run("pdm", "install", "--check", *groups, external=True) +def install(session: nox.Session, *groups, dev: bool = True, editable: bool = False, no_self=False, no_default=False): + other_args = [] + if not dev: + other_args.append("--prod") + if not editable: + other_args.append("--no-editable") + if no_self: + other_args.append("--no-self") + if no_default: + other_args.append("--no-default") + for group in groups: + other_args.extend(["--group", group]) + session.run("pdm", "install", "--check", *other_args, external=True) @functools.lru_cache @@ -41,8 +59,7 @@ def _list_files() -> list[Path]: ): cmd_result = subprocess.run(cmd, check=True, text=True, capture_output=True) file_list.extend(cmd_result.stdout.splitlines()) - file_paths = [Path(p) for p in file_list] - return file_paths + return [Path(p) for p in file_list] def list_files(suffix: str | None = None) -> list[Path]: @@ -115,30 +132,63 @@ def run_shellcheck(session, mode="check"): session.run(*shellcheck_cmd, external=True) -@nox.session(name="format", python=PYTHON_DEFAULT_VERSION) +@nox.session(name="format", python=PYTHON_DEFAULT_VERSION, tags=["format", "check"]) def format_(session): """Lint the code and apply fixes in-place whenever possible.""" - install(session, "lint") + install(session, "lint", no_self=True, no_default=True) session.run("ruff", "check", "--fix", ".") run_shellcheck(session, mode="fmt") run_readable(session, mode="fmt") session.run("ruff", "format", ".") -@nox.session(python=PYTHON_DEFAULT_VERSION) +@nox.session(python=PYTHON_DEFAULT_VERSION, tags=["lint", "check"]) def lint(session): """Run linters in readonly mode.""" install(session, "lint") - session.run("ruff", "check", "--diff", ".") + session.run("ruff", "check", "--diff", "--unsafe-fixes", ".") + session.run("ruff", "format", "--diff", ".") + session.run("mypy", ".") session.run("codespell", ".") run_shellcheck(session, mode="check") run_readable(session, mode="check") - session.run("ruff", "format", "--diff", ".") - session.run("bandit", "--ini", "bandit.ini", "-r", ".", *session.posargs) - session.run("mypy", "--config-file", "mypy.ini", ".", *session.posargs) -@nox.session(python=PYTHON_VERSIONS) +@nox.session(python=PYTHON_VERSIONS, tags=["test", "check"]) +# COOKIECUTTER{%- if cookiecutter.is_django_package == "y" %} +@nox.parametrize("django", DJANGO_VERSIONS) +def test(session, django: str): + install(session, "test") + session.run("pip", "install", f"django~={django}.0") +# COOKIECUTTER{%- else %} def test(session): install(session, "test") - session.run("pytest", "-vv", "-n", "auto", *session.posargs) +# COOKIECUTTER{%- endif %} + session.run("pytest", "-vv", "-n", "auto", *session.posargs) + + +@nox.session(python=PYTHON_DEFAULT_VERSION) +def make_release(session): + parser = argparse.ArgumentParser() + + def version(value): + if not re.match(r"\d+\.\d+\.\d+", value): + raise argparse.ArgumentTypeError("Invalid version format") + return value + + parser.add_argument( + "release_version", + help="Release version in semver format (e.g. 1.2.3)", + type=version, + ) + parsed_args = parser.parse_args(session.posargs) + + local_changes = subprocess.check_output(["git", "diff", "--stat"]) + if local_changes: + session.error("Uncommitted changes detected") + + current_branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], text=True) + if current_branch != MAIN_BRANCH_NAME: + session.error(f"WARNING: releasing from a branch different than {MAIN_BRANCH_NAME!r}") + + session.run("towncrier", "build", "--yes", "--version", parsed_args.release_version) diff --git a/{{cookiecutter.package_name}}/pyproject.toml b/{{cookiecutter.package_name}}/pyproject.toml new file mode 100644 index 0000000..e69c7c5 --- /dev/null +++ b/{{cookiecutter.package_name}}/pyproject.toml @@ -0,0 +1,134 @@ +[project] +name = "{{cookiecutter.package_name}}" +version = "0.0.1" +requires-python = ">=3.11" +keywords = [] +license = {text = "MIT"} +readme = "README.md" +authors = [ + {name = "Reef Technologies", email = "opensource@reef.pl"}, +] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +[project.urls] +"Source" = "{{cookiecutter.repository_github_url}}" +"Issue Tracker" = "{{cookiecutter.repository_github_url}}/issues" + +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" + +[tool.pytest.ini_options] +pythonpath = ["."] # allow for `import tests` in test files + +# COOKIECUTTER{%- if cookiecutter.is_django_package == "y" %} +DJANGO_SETTINGS_MODULE="tests.settings" +# COOKIECUTTER{%- endif %} + +[tool.pdm.dev-dependencies] +test = [ + "freezegun", + "pytest", + "pytest-asyncio", +# COOKIECUTTER{%- if cookiecutter.is_django_package == "y" %} + "pytest-django", +# COOKIECUTTER{%- endif %} + "pytest-xdist", +] +lint = [ + "codespell[toml]", +# COOKIECUTTER{%- if cookiecutter.is_django_package == "y" %} + "django-stubs>=1.12.0", +# COOKIECUTTER{%- endif %} + "mypy>=1.8", + "ruff", + "types-freezegun", + "types-python-dateutil", + "types-requests", +] +release = [ + "towncrier", +] + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = [ + "D", + "E", "F", "I", "UP", + "TCH005", +] +ignore = [ + "D100", "D105", "D107", "D200", "D202", "D203", "D205", "D212", "D400", "D401", "D415", + "D101", "D102", "D103", "D104", # TODO remove once we have docstring for all public methods +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] +"test/**" = ["D", "F403", "F405"] + +[tool.codespell] +skip = "*.min.js,pdm.lock" +ignore-words-list = "datas" + +[tool.towncrier] +directory = "changelog.d" +filename = "CHANGELOG.md" +start_string = "\n" +underlines = ["", "", ""] +title_format = "## [{version}]({{cookiecutter.repository_github_url}}/releases/tag/v{version}) - {project_date}" +issue_format = "[#{issue}]({{cookiecutter.repository_github_url}}/issues/{issue})" + +[[tool.towncrier.type]] +directory = "removed" +name = "Removed" +showcontent = true + +[[tool.towncrier.type]] +directory = "changed" +name = "Changed" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixed" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecated" +name = "Deprecated" +showcontent = true + +[[tool.towncrier.type]] +directory = "added" +name = "Added" +showcontent = true + +[[tool.towncrier.type]] +directory = "doc" +name = "Doc" +showcontent = true + +[[tool.towncrier.type]] +directory = "infrastructure" +name = "Infrastructure" +showcontent = true + +[tool.mypy] + +[[tool.mypy.overrides]] +module = [ + "nox", + "pytest", + "tests.*", +] +ignore_missing_imports = true diff --git a/{{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/__init__.py b/{{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/_internal/__init__.py b/{{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/_internal/__init__.py new file mode 100644 index 0000000..42d609f --- /dev/null +++ b/{{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/_internal/__init__.py @@ -0,0 +1,5 @@ +""" +Internal implementation of {{cookiecutter.package_name}}. + +This project uses ApiVer, and as such, all imports should be done from v* submodules. +""" diff --git a/{{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/py.typed b/{{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/v0/__init__.py b/{{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/v0/__init__.py new file mode 100644 index 0000000..2644275 --- /dev/null +++ b/{{cookiecutter.package_name}}/src/{{cookiecutter.package_name}}/v0/__init__.py @@ -0,0 +1,3 @@ +""" +Public interface of the {{cookiecutter.package_name}} package. +""" diff --git a/{{cookiecutter.package_name}}/tests/__init__.py b/{{cookiecutter.package_name}}/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.package_name}}/tests/conftest.py b/{{cookiecutter.package_name}}/tests/conftest.py new file mode 100644 index 0000000..4b91d6b --- /dev/null +++ b/{{cookiecutter.package_name}}/tests/conftest.py @@ -0,0 +1,3 @@ +# COOKIECUTTER{%- if cookiecutter.is_django_package == "y" -%} +from .django_fixtures import * # noqa +# COOKIECUTTER{%- endif %} diff --git a/{{cookiecutter.package_name}}/tests/django_fixtures.py b/{{cookiecutter.package_name}}/tests/django_fixtures.py new file mode 100644 index 0000000..bd44d55 --- /dev/null +++ b/{{cookiecutter.package_name}}/tests/django_fixtures.py @@ -0,0 +1,16 @@ +# cookiecutter-rt-pkg macro: requires cookiecutter.is_django_package +from __future__ import annotations + +import pytest +from django.contrib.auth import get_user_model +from django.test import RequestFactory + + +@pytest.fixture +def request_factory() -> RequestFactory: + return RequestFactory() + + +@pytest.fixture +def user_model(): + return get_user_model() diff --git a/{{cookiecutter.package_name}}/tests/settings.py b/{{cookiecutter.package_name}}/tests/settings.py new file mode 100644 index 0000000..7fa5de5 --- /dev/null +++ b/{{cookiecutter.package_name}}/tests/settings.py @@ -0,0 +1,22 @@ +""" +Django settings used in tests. +""" +# cookiecutter-rt-pkg macro: requires cookiecutter.is_django_package + +DEBUG = True +SECRET_KEY = "DUMMY" + +INSTALLED_APPS = [ + "django.contrib.auth", + "django.contrib.contenttypes", +] + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + } +} + +ROOT_URLCONF = __name__ +urlpatterns = [] # type: ignore diff --git a/{{cookiecutter.package_name}}/tests/unit/__init__.py b/{{cookiecutter.package_name}}/tests/unit/__init__.py new file mode 100644 index 0000000..c2cf7c1 --- /dev/null +++ b/{{cookiecutter.package_name}}/tests/unit/__init__.py @@ -0,0 +1,3 @@ +""" +Tests for internals +""" diff --git a/{{cookiecutter.package_name}}/tests/unit/api/__init__.py b/{{cookiecutter.package_name}}/tests/unit/api/__init__.py new file mode 100644 index 0000000..0cdb696 --- /dev/null +++ b/{{cookiecutter.package_name}}/tests/unit/api/__init__.py @@ -0,0 +1,5 @@ +""" +Tests covering the package API versioned with ApiVer. + +These tests will be run per each version of the API. +""" diff --git a/{{cookiecutter.package_name}}/tests/unit/api/test_setup.py b/{{cookiecutter.package_name}}/tests/unit/api/test_setup.py new file mode 100644 index 0000000..544eeb6 --- /dev/null +++ b/{{cookiecutter.package_name}}/tests/unit/api/test_setup.py @@ -0,0 +1,4 @@ +def test_v0_exports(): + import {{cookiecutter.package_name}}.v0 as api # noqa + + assert sorted(name for name in dir(api) if not name.startswith("_")) == [] diff --git a/{{cookiecutter.package_name}}/tests/unit/internal/__init__.py b/{{cookiecutter.package_name}}/tests/unit/internal/__init__.py new file mode 100644 index 0000000..0cdb696 --- /dev/null +++ b/{{cookiecutter.package_name}}/tests/unit/internal/__init__.py @@ -0,0 +1,5 @@ +""" +Tests covering the package API versioned with ApiVer. + +These tests will be run per each version of the API. +""" diff --git a/{{cookiecutter.package_name}}/tests/unit/internal/test_django_setup.py b/{{cookiecutter.package_name}}/tests/unit/internal/test_django_setup.py new file mode 100644 index 0000000..cc26614 --- /dev/null +++ b/{{cookiecutter.package_name}}/tests/unit/internal/test_django_setup.py @@ -0,0 +1,5 @@ +# cookiecutter-rt-pkg macro: requires cookiecutter.is_django_package + + +def test__setup(db): + pass diff --git a/{{cookiecutter.package_name}}/tests/unit/internal/test_setup.py b/{{cookiecutter.package_name}}/tests/unit/internal/test_setup.py new file mode 100644 index 0000000..0eaff0e --- /dev/null +++ b/{{cookiecutter.package_name}}/tests/unit/internal/test_setup.py @@ -0,0 +1,17 @@ +""" +This test file is here always to indicate that everything was installed and the CI was able to run tests. +It should always pass as long as all dependencies are properly installed. +""" + +from datetime import datetime, timedelta + +import pytest +from freezegun import freeze_time + + +def test__setup(): + with freeze_time(datetime.now() - timedelta(days=1)): + pass + + with pytest.raises(ZeroDivisionError): + 1 / 0 diff --git a/{{cookiecutter.repostory_name}}/.github/dependabot.yml b/{{cookiecutter.repostory_name}}/.github/dependabot.yml deleted file mode 100644 index 550071a..0000000 --- a/{{cookiecutter.repostory_name}}/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "pip" - schedule: - interval: "daily" - directory: / - open-pull-requests-limit: 0 - - package-ecosystem: "docker" - schedule: - interval: "weekly" - directory: / - open-pull-requests-limit: 0 diff --git a/{{cookiecutter.repostory_name}}/.github/workflows/ci.yml b/{{cookiecutter.repostory_name}}/.github/workflows/ci.yml deleted file mode 100644 index 74e48de..0000000 --- a/{{cookiecutter.repostory_name}}/.github/workflows/ci.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Run linter and tests - -on: - push: - branches: [master, main] - pull_request: - branches: [master, main] - -env: - PYTHON_DEFAULT_VERSION: "3.11" - -jobs: - linter: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up Python {% raw %}${{ env.PYTHON_DEFAULT_VERSION }}{% endraw %} - uses: actions/setup-python@v4 - with: - python-version: {% raw %}${{ env.PYTHON_DEFAULT_VERSION }}{% endraw %} - cache: "pip" - - name: Install dependencies - run: python -m pip install --upgrade nox 'pdm>=2.12,<3' - - name: Run linters - run: nox -vs lint - test: - timeout-minutes: 10 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up Python {% raw %}${{ env.PYTHON_DEFAULT_VERSION }}{% endraw %} - uses: actions/setup-python@v4 - with: - python-version: {% raw %}${{ env.PYTHON_DEFAULT_VERSION }}{% endraw %} - cache: "pip" - - name: Install dependencies - run: python -m pip install --upgrade 'nox==2024.3.2' 'pdm==2.13.2' - - name: Run unit tests - run: nox -vs test diff --git a/{{cookiecutter.repostory_name}}/pyproject.toml b/{{cookiecutter.repostory_name}}/pyproject.toml deleted file mode 100644 index ad34a2d..0000000 --- a/{{cookiecutter.repostory_name}}/pyproject.toml +++ /dev/null @@ -1,50 +0,0 @@ -[project] -name = "{{cookiecutter.repostory_name}}" -requires-python = ">=3.9" - -[build-system] -requires = ["pdm-backend"] -build-backend = "pdm.backend" - - -[tool.pdm.dev-dependencies] -test = [ - 'pytest', - 'pytest-django', - 'pytest-xdist', - 'ipdb', - 'freezegun', -] -lint = [ - "ruff", - "codespell[toml]", - "bandit>=1.7.7", - "mypy", - "types-freezegun", - "types-python-dateutil", - "types-requests", -] - -[tool.ruff] -line-length = 120 - -[tool.ruff.lint] -# TODO add D -select = [ - "E", "F", "I", "UP", - "TCH005", -] -# TODO: remove E501 once docstrings are formatted -ignore = [ - "D100", "D105", "D107", "D200", "D202", "D203", "D205", "D212", "D400", "D401", "D415", - "D101", "D102","D103", "D104", # TODO remove once we have docstring for all public methods - "E501", # TODO: remove E501 once docstrings are formatted -] - -[tool.ruff.lint.per-file-ignores] -"__init__.py" = ["F401"] -"test/**" = ["D", "F403", "F405"] - -[tool.codespell] -skip = '*.min.js,pdm.lock' -ignore-words-list = 'datas'