diff --git a/README.md b/README.md index 38c6463..ad76c57 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,19 @@ The Hatchlor integrates the following features: * [EditorConfig]: maintain consistent coding styles for multiple developers, * [src-layout]: the actual Python package is kept under a `src` folder avoiding many common errors. +Please note additional integrations and following amendments: + +* [semantic-release]: local prepare of a commit: create changelog and commit with a version tag +* [bump-my-version] and [generate-changelog]: alternative to [semantic-release] +* [gitlint]: include linting of commit messages +* [GitHub Actions]: reworked workflows - dump context, add test publishing on TestPy, switch to new PyPi mechanism + +The amendments support a local development and commit process while "outsourcing" testing for different OS +and with different Python versions to GitHub. +The local committing includes preparation of a changelog. It is based on semantic-versioning. +Commit-messages are linted to enforce commit messages according to conventional commits format as +basis for a proper and automatically generated changelog. + The template includes a `skeleton.py` with a simple function `fib` that calculates the Fibonacci numbers as demonstration. This is tested with `tests/test_skeleton.py` to demonstrate the corresponding features from above. As an additional tidbit, `skeleton.py` also features [Typer] to show how `fib` can be diff --git a/cookiecutter.json b/cookiecutter.json index 5961b08..3d8ed0d 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -11,6 +11,7 @@ "project_repo": "https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}", "pypi_username": "{{ cookiecutter.github_username }}", "lock_file_support": false, + "gitlint_commitmsg_linter": true, "__prompts__": { "full_name": "Provide your [bold yellow]full name[/]", "email": "Provide your [bold yellow]email[/]", @@ -23,6 +24,7 @@ "open_source_license": "Provide the [bold yellow]license[/] of this project", "pypi_username": "Optionally, provide a [bold yellow]PyPI user name[/]", "github_username": "Optionally, provide a [bold yellow]Github user name[/]", - "lock_file_support": "Activate support for lock files?" + "lock_file_support": "Activate support for lock files?", + "gitlint_commitmsg_linter": "Include gitlint commit message linter?" } } diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 93c01da..524bf48 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -1,4 +1,10 @@ import subprocess +import os + + +if not {{cookiecutter.gitlint_commitmsg_linter}}: + if os.path.exists(".gitlint"): + os.remove(".gitlint") try: subprocess.call(['git', 'init', '--initial-branch', 'main']) diff --git a/{{cookiecutter.project_slug}}/.changelog-config.yaml b/{{cookiecutter.project_slug}}/.changelog-config.yaml new file mode 100644 index 0000000..11d59a1 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.changelog-config.yaml @@ -0,0 +1,123 @@ +# For more configuration information, please see https://callowayproject.github.io/generate-changelog/ + +# User variables for reference in other parts of the configuration. +variables: + changelog_filename: CHANGELOG.md + +# Pipeline to find the most recent tag for incremental changelog generation. +# Leave empty to always start at first commit. +starting_tag_pipeline: + - action: ReadFile + kwargs: + filename: '{% raw %}{{ changelog_filename }}{% endraw %}' + - action: FirstRegExMatch + kwargs: + pattern: (?im)^## (?P\d+\.\d+(?:\.\d+)?)\s+\(\d+-\d{2}-\d{2}\)$ + named_subgroup: rev + +# Used as the version title of the changes since the last valid tag. +unreleased_label: Unreleased + +# Process the commit's first line for use in the changelog. +summary_pipeline: + - action: strip_spaces + - action: Strip + comment: Get rid of any periods so we don't get double periods + kwargs: + chars: . + - action: SetDefault + args: + - no commit message + - action: capitalize + - action: append_dot + +# Process the commit's body for use in the changelog. +body_pipeline: + - action: ParseTrailers + comment: Parse the trailers into metadata. + kwargs: + commit_metadata: save_commit_metadata + +# Process and store the full or partial changelog. +output_pipeline: + - action: IncrementalFileInsert + kwargs: + filename: '{% raw %}{{ changelog_filename }}{% endraw %}' + last_heading_pattern: (?im)^## \d+\.\d+(?:\.\d+)?\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)$ + +# Group the commits within a version by these commit attributes. +group_by: + - metadata.category + +# Only tags matching this regular expression are used for the changelog. +# tag_pattern: ^[0-9]+\.[0-9]+(?:\.[0-9]+)?$ +# tag_pattern: ^v[0-9]+\.[0-9]+(?:\.[0-9]+)?$ +# Version tags RegEx from https://regex101.com/r/vkijKf/1/ preceeded by v +tag_pattern: ^(v0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ + +# Tells ``git-log`` whether to include merge commits in the log. +include_merges: true + +# Ignore commits whose summary line matches any of these regular expression patterns. +ignore_patterns: + - '[@!]minor' + - '[@!]cosmetic' + - '[@!]refactor' + - '[@!]wip' + - ^$ + - ^Merge branch + - ^Merge pull + - ^[0-9]+\.[0-9]+(?:\.[0-9]+)?(?:\.)? # release commit for semantic-release + +# Set the commit's category metadata to the first classifier that returns ``True``. +commit_classifiers: + - action: SummaryRegexMatch + category: New Features + kwargs: + pattern: (?i)^(?:new|add)[^\n]*$ + - action: SummaryRegexMatch + category: Updates + kwargs: + pattern: (?i)^(?:update|change|rename|remove|delete|improve|refactor|chg|modif)[^\n]*$ + - action: SummaryRegexMatch + category: Fixes + kwargs: + pattern: (?i)^(?:fix)[^\n]*$ + - action: + category: Other + +# Tokens in git commit trailers that indicate authorship. +valid_author_tokens: + - author + - based-on-a-patch-by + - based-on-patch-by + - co-authored-by + - co-committed-by + - contributions-by + - from + - helped-by + - improved-by + - original-patch-by + +# Rules applied to commits to determine the type of release to suggest. +release_hint_rules: + - match_result: dev + branch: ^((?!master|main).)*$ + - match_result: patch + grouping: Other + branch: master|main + - match_result: patch + grouping: Fixes + branch: master|main + - match_result: minor + grouping: Updates + branch: master|main + - match_result: minor + grouping: New + branch: master|main + - match_result: minor + grouping: New Features + branch: master|main + - match_result: major + grouping: Breaking Changes + branch: master|main diff --git a/{{cookiecutter.project_slug}}/.github/workflows/build.yml b/{{cookiecutter.project_slug}}/.github/workflows/build.yml index 175d8db..94e2e0e 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/build.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/build.yml @@ -1,75 +1,151 @@ -name: Build +name: Build and Release Package on: [push, pull_request] jobs: - test: - runs-on: {% raw %}${{ matrix.os }}{% endraw %} + dump-context: + runs-on: ubuntu-latest + steps: + - name: Dump context "GitHub" + env: + GITHUB_CONTEXT: {% raw %}${{ toJson(github) }}{% endraw %} + run: echo "$GITHUB_CONTEXT" + + build-test: strategy: matrix: - os: [ubuntu-latest] - python_version: ['{{ cookiecutter.target_python_version }}'] - + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: {% raw %}${{ matrix.os }}{% endraw %} + if: startsWith(github.ref, 'refs/tags/') steps: - - uses: actions/checkout@v4 + - name: Check out the repository + uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: - python-version: {% raw %}${{ matrix.python_version }}{% endraw %} + python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install hatch pre-commit + pip install hatch pre-commit coverage hatch env create - name: Lint and typecheck run: | hatch run lint:all - - name: Run Tests + - name: Run Tests with coverage analysis run: | - hatch run test:pytest + hatch test --cover - uses: codecov/codecov-action@v4 with: token: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %} fail_ci_if_error: true verbose: true - release: + build-distribution: runs-on: ubuntu-latest - environment: release - needs: test - if: startsWith(github.ref, 'refs/tags/') - permissions: - contents: write - id-token: write - + needs: build-test steps: - - uses: actions/checkout@v4 - - name: Set up Python {% raw %}${{ matrix.python_version }}{% endraw %} + - name: Check out the repository + uses: actions/checkout@v4 + - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '{{ cookiecutter.target_python_version }}' + python-version: '3.x' - name: Install dependencies shell: bash run: | python -m pip install --upgrade pip pip install hatch pre-commit - - name: Build + - name: Build with Hatch run: | hatch build + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish-testpypi: + runs-on: ubuntu-latest + needs: build-distribution + environment: + name: pypi-test + url: https://testpypi.org/project/p/{% raw %}${{ github.event.repository.name }}{% endraw %} + permissions: + id-token: write + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ - name: Publish 📦 to Test PyPI - if: startsWith(github.ref, 'refs/heads/main') uses: pypa/gh-action-pypi-publish@release/v1 with: - skip_existing: true - user: __token__ - password: {% raw %}${{ secrets.TEST_PYPI_SECRECT }}{% endraw %} packages-dir: dist/ repository-url: https://test.pypi.org/legacy/ + verbose: true + # skip-existing: true + # user: __token__ + # password: {% raw %}${{ secrets.TEST_PYPI_SECRECT }}{% endraw %} + + publish-pypi: + runs-on: ubuntu-latest + needs: [ build-distribution, publish-testpypi ] + environment: + name: pypi + url: https://pypi.org/project/p/{% raw %}${{ github.event.repository.name }}{% endraw %} + permissions: + id-token: write + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ - name: Publish 📦 to PyPI - if: startsWith(github.ref, 'refs/heads/main') uses: pypa/gh-action-pypi-publish@release/v1 with: - user: __token__ - password: {% raw %}${{ secrets.PYPI_SECRECT }}{% endraw %} packages-dir: dist/ + # repository-url = "https://upload.pypi.org/legacy/" + verbose: true + # user: __token__ + # password: {% raw %}${{ secrets.PYPI_SECRECT }}{% endraw %} + + github-release: + runs-on: ubuntu-latest + needs: publish-pypi + permissions: + contents: write # IMPORTANT: mandatory for making GitHub Releases + id-token: write # IMPORTANT: mandatory for sigstore + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Sign the Python 🐍 distribution 📦 with Sigstore + uses: sigstore/gh-action-sigstore-python@v3.0.0 + with: + inputs: >- + ./dist/*.tar.gz + ./dist/*.whl + - name: Create GitHub Release + env: + GITHUB_TOKEN: {% raw %}${{ github.token }}{% endraw %} + run: >- + gh release create + '{% raw %}${{ github.ref_name }}{% endraw %}' + --repo '{% raw %}${{ github.repository }}{% endraw %}' + --notes "" + - name: Upload artifact signatures to GitHub Release + env: + GITHUB_TOKEN: {% raw %}${{ github.token }}{% endraw %} + # Upload to GitHub Release using the `gh` CLI. + # `dist/` contains the built packages, and the + # sigstore-produced signatures and certificates. + run: >- + gh release upload + '{% raw %}${{ github.ref_name }}{% endraw %}' dist/** + --repo '{% raw %}${{ github.repository }}{% endraw %}' diff --git a/{{cookiecutter.project_slug}}/.github/workflows/documentation.yml b/{{cookiecutter.project_slug}}/.github/workflows/documentation.yml index b685084..a0eedef 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/documentation.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/documentation.yml @@ -22,34 +22,42 @@ defaults: shell: bash jobs: - build: + dump-context: runs-on: ubuntu-latest + steps: + - name: Dump context "GitHub" + env: + GITHUB_CONTEXT: {% raw %}${{ toJson(github) }}{% endraw %} + run: echo "$GITHUB_CONTEXT" + build-documentation: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '{{ cookiecutter.target_python_version }}' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install hatch pre-commit - hatch env create docs - - name: Build - run: hatch run docs:build-check - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./site - - deploy: + - name: Check out the repository + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install hatch pre-commit + hatch env create docs + - name: Build documentation + run: hatch run docs:build-check + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./site + + deploy-documentation: environment: name: github-pages url: {% raw %}${{ steps.deployment.outputs.page_url }}{% endraw %} runs-on: ubuntu-latest - needs: build + needs: build-documentation steps: - name: Deploy to GitHub Pages id: deployment diff --git a/{{cookiecutter.project_slug}}/.github/workflows/draft.yml b/{{cookiecutter.project_slug}}/.github/workflows/draft.yml index 4221061..1110f9c 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/draft.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/draft.yml @@ -6,13 +6,23 @@ on: - main jobs: + + dump-context: + runs-on: ubuntu-latest + steps: + - name: Dump context "GitHub" + env: + GITHUB_CONTEXT: {% raw %}${{ toJson(github) }}{% endraw %} + run: echo "$GITHUB_CONTEXT" + update-draft: runs-on: ubuntu-latest permissions: contents: write steps: # Drafts your next Release notes as Pull Requests are merged into "main" - - uses: release-drafter/release-drafter@v6 + - name: Release draft action + uses: release-drafter/release-drafter@v6 with: disable-autolabeler: true env: diff --git a/{{cookiecutter.project_slug}}/.github/workflows/labeler.yml b/{{cookiecutter.project_slug}}/.github/workflows/labeler.yml index 4d8e86e..d466f47 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/labeler.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/labeler.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - master permissions: actions: read @@ -13,13 +12,22 @@ permissions: pull-requests: write jobs: + + dump-context: + runs-on: ubuntu-latest + steps: + - name: Dump context "GitHub" + env: + GITHUB_CONTEXT: {% raw %}${{ toJson(github) }}{% endraw %} + run: echo "$GITHUB_CONTEXT" + labeler: runs-on: ubuntu-latest steps: - name: Check out the repository uses: actions/checkout@v4 - - name: Run Labeler uses: crazy-max/ghaction-github-labeler@v5.0.0 with: skip-delete: true + github-token: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %} diff --git a/{{cookiecutter.project_slug}}/.github/workflows/tests.yml b/{{cookiecutter.project_slug}}/.github/workflows/tests.yml index 72c87a9..9c08834 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/tests.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/tests.yml @@ -2,9 +2,7 @@ name: Tests on: push: - branches: ["main"] pull_request: - branches: ["main"] concurrency: group: {% raw %}${{ github.workflow }}-${{ github.head_ref || github.run_id }}{% endraw %} @@ -15,19 +13,28 @@ defaults: shell: bash jobs: - tests: - runs-on: {% raw %}${{ matrix.os }}{% endraw %} + dump-context: + runs-on: ubuntu-latest + steps: + - name: Dump context "GitHub" + env: + GITHUB_CONTEXT: {% raw %}${{ toJson(github) }}{% endraw %} + run: echo "$GITHUB_CONTEXT" + + tests: strategy: matrix: - os: [ubuntu-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11"] + os: [ ubuntu-latest, macos-latest, windows-latest ] + python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] + runs-on: {% raw %}${{ matrix.os }}{% endraw %} steps: - - uses: actions/checkout@v4 - - name: Set up Python {% raw %}${{ matrix.python_version }}{% endraw %} + - name: Check out the repository + uses: actions/checkout@v4 + - name: Set up Python {% raw %}${{ matrix.python-version }}{% endraw %} uses: actions/setup-python@v5 with: - python-version: {% raw %}${{ matrix.python_version }}{% endraw %} + python-version: {% raw %}${{ matrix.python-version }}{% endraw %} - name: Install dependencies run: | python -m pip install --upgrade pip @@ -38,4 +45,4 @@ jobs: hatch run lint:all - name: Run Tests run: | - hatch run test:pytest + hatch test diff --git a/{{cookiecutter.project_slug}}/.gitignore b/{{cookiecutter.project_slug}}/.gitignore index c0e87c1..6e5aef3 100644 --- a/{{cookiecutter.project_slug}}/.gitignore +++ b/{{cookiecutter.project_slug}}/.gitignore @@ -101,6 +101,9 @@ ENV/ # mypy .mypy_cache/ +# ruff +.ruff_cache/ + # IDE settings .vscode/ .idea/ diff --git a/{{cookiecutter.project_slug}}/.gitlint b/{{cookiecutter.project_slug}}/.gitlint new file mode 100644 index 0000000..637599d --- /dev/null +++ b/{{cookiecutter.project_slug}}/.gitlint @@ -0,0 +1,33 @@ +[general] +regex-style-search=true +# Ignore rules, reference them by id or name (comma-separated) +ignore=title-trailing-punctuation, T3 + +# Enable specific community contributed rules +contrib=contrib-title-conventional-commits, CT1 + +# Set the extra-path where gitlint will search for user defined rules +# extra-path= + +### Configuring rules ### + +[title-max-length] +line-length=80 + +[title-min-length] +min-length=5 + +[body-max-line-length] +line-length=210 + +### Ignore rules to ignore automated commits by bump-my-version or semantic-release ### + +[ignore-by-title] +# Match commit titles starting with "Release" +regex=^(Bump version: )*[0-9]+\.[0-9]+(?:\.[0-9]+)?(?:\.)? +ignore=all + +[ignore-by-author-name] +# Match commits by author name (e.g. ignore dependabot commits) +regex=semantic-release +ignore=all diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml index 87df95f..364f601 100644 --- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -17,18 +17,28 @@ repos: - id: end-of-file-fixer - id: mixed-line-ending args: ['--fix=auto'] # replace 'auto' with 'lf' to enforce Linux/Mac line endings or 'crlf' for Windows + - id: detect-private-key # Ruff replaces black, flake8, autoflake, isort and more - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.5.7' # make sure this is always consistent with hatch configs + rev: 'v0.8.3' # make sure this is always consistent with hatch configs hooks: - id: ruff - id: ruff-format args: [--check, --config, ./pyproject.toml] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.11.1' # make sure this is always consistent with hatch configs + rev: 'v1.13.0' # make sure this is always consistent with hatch configs hooks: - id: mypy args: ["--install-types", "--non-interactive"] additional_dependencies: [types-tabulate, types-cachetools] + +{% if cookiecutter.gitlint_commitmsg_linter %} +# gitlint for commit message linting +- repo: https://github.com/jorisroovers/gitlint + rev: 'v0.19.1' + hooks: + - id: gitlint # this is the regular commit-msg hook + - id: gitlint-ci # hook for CI environments +{% endif %} diff --git a/{{cookiecutter.project_slug}}/mkdocs.yml b/{{cookiecutter.project_slug}}/mkdocs.yml index 0379659..ab3ad1c 100644 --- a/{{cookiecutter.project_slug}}/mkdocs.yml +++ b/{{cookiecutter.project_slug}}/mkdocs.yml @@ -52,9 +52,12 @@ theme: features: - navigation.sections - navigation.tabs -# - navigation.expand + - navigation.expand - navigation.tabs.sticky - navigation.instant + - content.code.copy + - content.code.select + - content.code.annotate watch: - src/{{ cookiecutter.pkg_name }} @@ -124,6 +127,9 @@ markdown_extensions: guess_lang: false linenums_style: pymdownx-inline use_pygments: true + anchor_linenums: true + line_spans: __span + pygments_lang_class: true - pymdownx.inlinehilite: - pymdownx.keys: - pymdownx.magiclink: diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 14c6118..ba65c1a 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -2,6 +2,7 @@ requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" + #################### # Project Metadata # #################### @@ -26,20 +27,25 @@ authors = [ classifiers = [ # ToDo: Modify according to your needs! "Development Status :: 2 - Pre-Alpha", "Programming Language :: Python", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", ] # direct dependencies of this package, installed when users `pip install {{ cookiecutter.project_slug }}` later. dependencies = [ # ToDo: Modify according to your needs! - "typer", + # "typer", # "numpy", # "matplotlib", # "seaborn", ] dynamic = ["version"] +[project.optional-dependencies] +# n. a. + [project.scripts] # Use `fibonacci` as command-line script, comment or remove this section if not needed. fibonacci = "{{ cookiecutter.pkg_name }}.skeleton:app" @@ -49,8 +55,9 @@ fibonacci = "{{ cookiecutter.pkg_name }}.skeleton:app" # ToDo: Modify according to your needs! Documentation = "{{ cookiecutter.project_repo }}" Source = "{{ cookiecutter.project_repo }}" -# Tracker = "{{ cookiecutter.project_repo }}/issues" -# Sponsor = "https://github.com/sponsors/{{ cookiecutter.github_username }}" +Tracker = "{{ cookiecutter.project_repo }}/issues" +Sponsor = "https://github.com/sponsors/{{ cookiecutter.github_username }}" + ###################### # Build & Versioning # @@ -62,19 +69,117 @@ allow-direct-references = true [tool.hatch.version] source = "vcs" -raw-options = { version_scheme = "no-guess-dev" } -[tool.hatch.build.hooks.vcs] -version-file = "src/{{ cookiecutter.pkg_name }}/_version.py" +[tool.hatch.version.raw-options] +version_scheme = "no-guess-dev" +local_scheme = "dirty-tag" [tool.hatch.build] packages = ["src/{{ cookiecutter.pkg_name }}"] +[tool.hatch.build.hooks.vcs] +version-file = "src/{{ cookiecutter.pkg_name }}/_version.py" + [tool.hatch.build.targets.sdist] +artifacts = ["_version.py"] exclude = [ "/.github", ] +[tool.bumpversion] +current_version = "0.0.1" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" +serialize = ["{major}.{minor}.{patch}"] +search = "{current_version}" +replace = "{new_version}" +ignore_missing_files = true +ignore_missing_version = true +commit = true +commit_args = "" +sign_tags = false +tag = true +tag_name = "v{new_version}" +tag_message = "Bump version: {current_version} → {new_version}" +message = "Bump version: {current_version} → {new_version}" +setup_hooks= ["generate-changelog"] +post_commit_hooks = ["hatch build"] + +[[tool.bumpversion.files]] +filename = "CHANGELOG.md" +search = "Unreleased" + +[[tool.bumpversion.files]] +filename = "CHANGELOG.md" +search = "{current_version}...HEAD" +replace = "{current_version}...{new_version}" + +[tool.semantic_release] +assets = [] +build_command = "pip install hatch && hatch build" +build_command_env = [] +commit_message = "{version}\n\nAutomatically generated by python-semantic-release" +commit_parser = "angular" +logging_use_named_masks = false +major_on_zero = true +allow_zero_version = true +no_git_verify = false +tag_format = "v{version}" +version_variable = [] +version_toml = ["pyproject.toml:tool.bumpversion:current_version"] +version_pattern = [] + +[tool.semantic_release.branches.main] +match = "(main|master)" +prerelease = false +prerelease_token = "rc" + +[tool.semantic_release.changelog] +exclude_commit_patterns = [] +mode = "init" +insertion_flag = "" + +[tool.semantic_release.changelog.default_templates] +changelog_file = "CHANGELOG.md" +output_format = "md" +mask_initial_release = true + +[tool.semantic_release.changelog.environment] +block_start_string = "{% raw %}{%{% endraw %}" +block_end_string = "{% raw %}%}{% endraw %}" +variable_start_string = "{% raw %}{{{% endraw %}" +variable_end_string = "{% raw %}}}{% endraw %}" +comment_start_string = "{% raw %}{#{% endraw %}" +comment_end_string = "{% raw %}#}{% endraw %}" +trim_blocks = false +lstrip_blocks = false +newline_sequence = "\n" +keep_trailing_newline = false +extensions = [] +autoescape = false + +[tool.semantic_release.commit_author] +env = "GIT_COMMIT_AUTHOR" +default = "semantic-release " + +[tool.semantic_release.commit_parser_options] +minor_tags = ["feat"] +patch_tags = ["fix", "perf"] +other_allowed_tags = ["build", "chore", "ci", "docs", "style", "refactor", "test"] +allowed_tags = ["feat", "fix", "perf", "build", "chore", "ci", "docs", "style", "refactor", "test"] +default_bump_level = 0 + +[tool.semantic_release.remote] +name = "origin" +type = "github" +token = { env = "GH_TOKEN" } +ignore_token_for_push = false +insecure = false + +[tool.semantic_release.publish] +dist_glob_patterns = ["dist/*"] +upload_to_vcs_release = true + + ################## # External Tools # ################## @@ -114,15 +219,12 @@ source = [ exclude_lines = [ # Have to re-enable the standard pragma "pragma: no cover", - # Don't complain about missing debug-only code: "def __repr__", "if self\\.debug", - # Don't complain if tests don't hit defensive assertion code: "raise AssertionError", "raise NotImplementedError", - # Don't complain if non-runnable code isn't run: "if 0:", "if __name__ == .__main__.:", @@ -187,13 +289,18 @@ ignore = [ "SIM114", # Combine `if` branches using logical `or` operator "E203", # Whitespace before :, needed for black compatability and also `ruff format` "ISC001", # causes unexpected behaviour with formatter + "I001", # enforce reorganisation of imports + "T201", # print statements + "Q000" # double quotes found but single quotes preferred ] unfixable = [ # "F401", # Don't touch unused imports ] +exclude = ["__init__.py", "_version.py"] [tool.ruff.format] quote-style = "single" # be more like black +exclude = ["__init__.py", "_version.py"] [tool.ruff.lint.isort] known-first-party = ["{{ cookiecutter.pkg_name }}"] @@ -210,6 +317,9 @@ ban-relative-imports = "all" # Tests can use magic values, assertions, and relative imports "tests/**/*" = ["PLR2004", "S101", "TID252"] +[tool.cruft] +skip = ["README.md", "CHANGELOG.MD", "src/", "tests/", "docs/", "notebooks/"] + ##################### # Environment Setup # @@ -220,28 +330,56 @@ ban-relative-imports = "all" requires = ["hatch-pip-compile~=1.11.2"] {%- endif %} -# Default environment with production dependencies +# Hatch default environment with production dependencies [tool.hatch.envs.default] python = "{{ cookiecutter.target_python_version }}" +{%- if cookiecutter.gitlint_commitmsg_linter %} +post-install-commands = ["pre-commit install", "pre-commit install --hook-type commit-msg"] +{%- else %} post-install-commands = ["pre-commit install"] +{%- endif %} {%- if cookiecutter.lock_file_support %} type = "pip-compile" pip-compile-constraint = "default" # keep locks between default & others consistent lock-filename = "locks/{env_name}.lock" pip-compile-hashes = false # no hashes in lock files {%- endif %} -dependencies = [] +{%- if cookiecutter.gitlint_commitmsg_linter %} +dependencies = [ + "pre-commit", + "gitlint" +] +{%- else %} +dependencies = [ + "pre-commit", +] +{%- endif %} {%- if cookiecutter.lock_file_support %} [tool.hatch.envs.default.scripts] upgrade-all = "PIP_COMPILE_UPGRADE=1 hatch env run --env {env_name} -- python --version" upgrade-pkg = "PIP_COMPILE_UPGRADE_PACKAGE='{args}' hatch env run --env {env_name} -- python --version" {%- endif %} +# installer = pip -# Test environment with test-only dependencies +# Hatch default test environment for "hatch test" (also used in GitAction) +[tool.hatch.envs.hatch-test] +dependencies = [ + "pip", + "coverage-enable-subprocess==1.0", + "coverage[toml]~=7.4", + "pytest~=8.1", + "pytest-mock~=3.12", + "pytest-randomly~=3.15", + "pytest-rerunfailures~=14.0", + "pytest-xdist[psutil]~=3.5", +] + +# Test environment with test-only dependencies (do not mix up with default hatch-test environment "hatch-test" !) [tool.hatch.envs.test] dependencies = [ # required test dependencies - "coverage[toml]>=6.2", + "pip", + "coverage[toml]>=7.4", "pytest", "pytest-cov", "pytest-mock", @@ -260,7 +398,48 @@ cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src/{{ no-cov = "cov --no-cov {args}" debug = "cov --no-cov -s --pdb --pdbcls=IPython.core.debugger:Pdb {args}" -# Docs environment +# Hatch testing environment matrix for various Python versions replacing the functionality of tox +[[tool.hatch.envs.test.matrix]] +template = ["test"] +python = ["39", "310", "311", "312", "313"] + +# Hatch default linting environment for "hatch fmt" +[tool.hatch.envs.hatch-static-analysis] +dependencies = [ + "pip", + "mypy==1.13.0", + "ruff==0.8.3" +] + +# Hatch Linting environment for GitAction +[tool.hatch.envs.lint] +template = "lint" # don't inherit from default! +dependencies = [ + "pip", + "mypy==1.13.0", + "ruff==0.8.3" +] +[tool.hatch.envs.lint.scripts] +typing = [ + "echo \"VERSION: `mypy --version`\"", + "mypy --install-types --non-interactive {args}" +] +style = [ + "echo \"VERSION: `ruff --version`\"", + "ruff check {args:.}", + "ruff format --check {args:.}", +] +fix = [ + "ruff format {args:.}", + "ruff check --fix {args:.}", + "style", # feedback on what is not fixable +] +all = [ + "style", + "typing", +] + +# Hatch environment for building documentation [tool.hatch.envs.docs] dependencies = [ "mkdocs~=1.6", @@ -276,13 +455,13 @@ dependencies = [ "mkdocs-redirects~=1.2", "mkdocs-glightbox~=0.4.0", "mike~=2.1", - # Extensions + # Extensions Hatch "pymdown-extensions~=10.9", # Necessary for syntax highlighting in code blocks "pygments~=2.18", # Validation "linkchecker~=10.4", - ] +] [tool.hatch.envs.docs.env-vars] SOURCE_DATE_EPOCH = "1580601600" PYTHONUNBUFFERED = "1" @@ -298,35 +477,3 @@ build-check = [ "build", "validate", ] - -# Lint environment -[tool.hatch.envs.lint] -template = "lint" # don't inherit from default! -dependencies = [ - "mypy==1.11.1", - "ruff==0.5.7", -] -[tool.hatch.envs.lint.scripts] -typing = [ - "echo \"VERSION: `mypy --version`\"", - "mypy --install-types --non-interactive {args}" -] -style = [ - "echo \"VERSION: `ruff --version`\"", - "ruff check {args:.}", - "ruff format --check {args:.}", -] -fix = [ - "ruff format {args:.}", - "ruff check --fix {args:.}", - "style", # feedback on what is not fixable -] -all = [ - "style", - "typing", -] - -# Test matrix for various Python versions replacing the functionality of tox -[[tool.hatch.envs.test.matrix]] -template = ["test"] -python = ["310", "311", "312"] diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.pkg_name}}/__init__.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.pkg_name}}/__init__.py index dc5e376..25bc2c1 100644 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.pkg_name}}/__init__.py +++ b/{{cookiecutter.project_slug}}/src/{{cookiecutter.pkg_name}}/__init__.py @@ -1,13 +1,46 @@ -"""Ultimate Notion provides a pythonic, high-level API for Notion +# version determination -Notion-API: https://developers.notion.com/reference/intro -""" -from importlib.metadata import PackageNotFoundError, version +# original Hatchlor version +# from importlib.metadata import PackageNotFoundError, version +# try: +# __version__ = version('{{ cookiecutter.project_slug }}') +# except PackageNotFoundError: # pragma: no cover +# __version__ = 'unknown' +# finally: +# del version, PackageNotFoundError +# modified version for editable mode installs (if desired +# up-to-date version tag for modules installed in editable mode inspired by +# https://github.com/maresb/hatch-vcs-footgun-example/blob/main/hatch_vcs_footgun_example/__init__.py +# Define the variable '__version__': +# try: +# +# # own developed alternative variant to hatch-vcs-footgun overcoming problem of ignored setuptools_scm settings +# # from hatch-based pyproject.toml libraries +# from hatch.cli import hatch +# from click.testing import CliRunner +# # determine version via hatch +# __version__ = CliRunner().invoke(hatch, ["version"]).output.strip() +# +# except (ImportError, LookupError): +# # As a fallback, use the version that is hard-coded in the file. +# try: +# from ._version import __version__ # noqa: F401 +# except ModuleNotFoundError: +# # The user is probably trying to run this without having installed the +# # package, so complain. +# raise RuntimeError( +# f"Package {__package__} is not correctly installed. Please install it with pip." +# ) + +# simplified version to avoid evaluation of metadata and additional dependencies +# version file is generated during Hatch build process try: - __version__ = version('{{ cookiecutter.project_slug }}') -except PackageNotFoundError: # pragma: no cover - __version__ = 'unknown' -finally: - del version, PackageNotFoundError + from ._version import __version__ # noqa: F401 +except ModuleNotFoundError: + # The user is probably trying to run this without having installed the + # package, so complain. + raise RuntimeError( + f"Package {__package__} is not correctly installed. Please install it with pip." + )