From 86b6f8ffe9afcf83d6d69e8697daf870e94e325d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 25 Nov 2024 23:27:24 +0100 Subject: [PATCH] chore: Template upgrade --- .copier-answers.yml | 2 +- .../{bug_report.md => 1-bug.md} | 2 +- .../{feature_request.md => 2-feature.md} | 0 .github/ISSUE_TEMPLATE/3-docs.md | 16 ++ .github/ISSUE_TEMPLATE/4-change.md | 18 ++ .github/workflows/ci.yml | 48 ++-- .github/workflows/release.yml | 17 +- .gitignore | 1 + .gitpod.dockerfile | 6 - .gitpod.yml | 13 -- CONTRIBUTING.md | 17 +- Makefile | 5 +- README.md | 10 +- config/pytest.ini | 2 - config/ruff.toml | 2 +- config/vscode/tasks.json | 6 - devdeps.txt | 27 --- docs/.overrides/main.html | 4 +- docs/.overrides/partials/comments.html | 57 +++++ docs/css/mkdocstrings.css | 2 +- docs/index.md | 5 + docs/js/feedback.js | 14 ++ docs/license.md | 5 + duties.py | 215 ++++++------------ mkdocs.yml | 21 +- pyproject.toml | 65 +++++- scripts/gen_credits.py | 18 +- scripts/gen_ref_nav.py | 2 +- scripts/make | 160 +------------ scripts/make.py | 193 ++++++++++++++++ 30 files changed, 532 insertions(+), 421 deletions(-) rename .github/ISSUE_TEMPLATE/{bug_report.md => 1-bug.md} (98%) rename .github/ISSUE_TEMPLATE/{feature_request.md => 2-feature.md} (100%) create mode 100644 .github/ISSUE_TEMPLATE/3-docs.md create mode 100644 .github/ISSUE_TEMPLATE/4-change.md delete mode 100644 .gitpod.dockerfile delete mode 100644 .gitpod.yml delete mode 100644 devdeps.txt create mode 100644 docs/.overrides/partials/comments.html create mode 100644 docs/js/feedback.js mode change 100755 => 120000 scripts/make create mode 100755 scripts/make.py diff --git a/.copier-answers.yml b/.copier-answers.yml index ebe9bf2..efbda51 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 1.1.0 +_commit: 1.5.4 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothée Mazzucotelli diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/1-bug.md similarity index 98% rename from .github/ISSUE_TEMPLATE/bug_report.md rename to .github/ISSUE_TEMPLATE/1-bug.md index 049d186..5d70bcc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/1-bug.md @@ -53,7 +53,7 @@ PASTE TRACEBACK HERE git-changelog --debug-info # | xclip -selection clipboard ``` -PASTE OUTPUT HERE +PASTE MARKDOWN OUTPUT HERE ### Additional context + +### Relevant code snippets + + +### Link to the relevant documentation section + diff --git a/.github/ISSUE_TEMPLATE/4-change.md b/.github/ISSUE_TEMPLATE/4-change.md new file mode 100644 index 0000000..dc9a8f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-change.md @@ -0,0 +1,18 @@ +--- +name: Change request +about: Suggest any other kind of change for this project. +title: "change: " +assignees: pawamoy +--- + +### Is your change request related to a problem? Please describe. + + +### Describe the solution you'd like + + +### Describe alternatives you've considered + + +### Additional context + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d3fbb9..de566ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,17 +25,20 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - - name: Fetch all tags - run: git fetch --depth=1 --tags - - - name: Set up Python + - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - - name: Install uv - run: pip install uv + - name: Setup uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml - name: Install dependencies run: make setup @@ -49,9 +52,6 @@ jobs: - name: Check if the code is correctly typed run: make check-types - - name: Check for vulnerabilities in dependencies - run: make check-dependencies - - name: Check for breaking changes in the API run: make check-api @@ -64,28 +64,46 @@ jobs: - macos-latest - windows-latest python-version: - - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" + - "3.13" + - "3.14" + resolution: + - highest + - lowest-direct + exclude: + - os: macos-latest + resolution: lowest-direct + - os: windows-latest + resolution: lowest-direct runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.python-version == '3.12' }} + continue-on-error: ${{ matrix.python-version == '3.14' }} steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - - name: Set up Python + - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - - name: Install uv - run: pip install uv + - name: Setup uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml + cache-suffix: py${{ matrix.python-version }} - name: Install dependencies + env: + UV_RESOLUTION: ${{ matrix.resolution }} run: make setup - name: Run the test suite diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07d2809..d09c514 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,15 +11,18 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Fetch all tags - run: git fetch --depth=1 --tags + with: + fetch-depth: 0 + fetch-tags: true - name: Setup Python - uses: actions/setup-python@v4 - - name: Install git-changelog - run: pip install git-changelog + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Setup uv + uses: astral-sh/setup-uv@v3 - name: Prepare release notes - run: git-changelog --release-notes > release-notes.md + run: uv tool run git-changelog --release-notes > release-notes.md - name: Create release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: body_path: release-notes.md diff --git a/.gitignore b/.gitignore index 41fee62..9fea047 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ /.pdm-build/ /htmlcov/ /site/ +uv.lock # cache .cache/ diff --git a/.gitpod.dockerfile b/.gitpod.dockerfile deleted file mode 100644 index 1590b41..0000000 --- a/.gitpod.dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM gitpod/workspace-full -USER gitpod -ENV PIP_USER=no -RUN pip3 install pipx; \ - pipx install uv; \ - pipx ensurepath diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 23a3c2b..0000000 --- a/.gitpod.yml +++ /dev/null @@ -1,13 +0,0 @@ -vscode: - extensions: - - ms-python.python - -image: - file: .gitpod.dockerfile - -ports: -- port: 8000 - onOpen: notify - -tasks: -- init: make setup diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb6682d..93cd90e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,12 +23,11 @@ make setup > You can install it with: > > ```bash -> python3 -m pip install --user pipx -> pipx install uv +> curl -LsSf https://astral.sh/uv/install.sh | sh > ``` > > Now you can try running `make setup` again, -> or simply `uv install`. +> or simply `uv sync`. You now have the dependencies installed. @@ -38,13 +37,11 @@ Run `make help` to see all the available actions! ## Tasks -This project uses [duty](https://github.com/pawamoy/duty) to run tasks. -A Makefile is also provided. The Makefile will try to run certain tasks -on multiple Python versions. If for some reason you don't want to run the task -on multiple Python versions, you run the task directly with `make run duty TASK`. - -The Makefile detects if a virtual environment is activated, -so `make` will work the same with the virtualenv activated or not. +The entry-point to run commands and tasks is the `make` Python script, +located in the `scripts` directory. Try running `make` to show the available commands and tasks. +The *commands* do not need the Python dependencies to be installed, +while the *tasks* do. +The cross-platform tasks are written in Python, thanks to [duty](https://github.com/pawamoy/duty). If you work in VSCode, we provide [an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup) diff --git a/Makefile b/Makefile index 7badd23..9afdaf9 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,10 @@ # This Makefile is just here to allow auto-completion in the terminal. actions = \ + allrun \ changelog \ check \ check-api \ - check-dependencies \ check-docs \ check-quality \ check-types \ @@ -16,6 +16,7 @@ actions = \ docs-deploy \ format \ help \ + multirun \ profile \ release \ run \ @@ -25,4 +26,4 @@ actions = \ .PHONY: $(actions) $(actions): - @bash scripts/make "$@" + @python scripts/make "$@" diff --git a/README.md b/README.md index 7039563..d051a75 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # git-changelog [![ci](https://github.com/pawamoy/git-changelog/workflows/ci/badge.svg)](https://github.com/pawamoy/git-changelog/actions?query=workflow%3Aci) -[![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://pawamoy.github.io/git-changelog/) +[![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://pawamoy.github.io/git-changelog/) [![pypi version](https://img.shields.io/pypi/v/git-changelog.svg)](https://pypi.org/project/git-changelog/) -[![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/git-changelog) [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#git-changelog:gitter.im) Automatic Changelog generator using Jinja2 templates. From git logs to change logs. @@ -50,17 +49,14 @@ Automatic Changelog generator using Jinja2 templates. From git logs to change lo ## Installation -With `pip`: - ```bash pip install git-changelog ``` -With [`pipx`](https://github.com/pipxproject/pipx): +With [`uv`](https://docs.astral.sh/uv/): ```bash -python3.8 -m pip install --user pipx -pipx install git-changelog +uv tool install git-changelog ``` ## Usage diff --git a/config/pytest.ini b/config/pytest.ini index ebdeb48..052a2f1 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -1,8 +1,6 @@ [pytest] python_files = test_*.py - *_test.py - tests.py addopts = --cov --cov-config config/coverage.ini diff --git a/config/ruff.toml b/config/ruff.toml index af8a2eb..ccc0f2a 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -1,4 +1,4 @@ -target-version = "py38" +target-version = "py39" line-length = 120 [lint] diff --git a/config/vscode/tasks.json b/config/vscode/tasks.json index 30008cf..73145ee 100644 --- a/config/vscode/tasks.json +++ b/config/vscode/tasks.json @@ -31,12 +31,6 @@ "command": "scripts/make", "args": ["check-docs"] }, - { - "label": "check-dependencies", - "type": "process", - "command": "scripts/make", - "args": ["check-dependencies"] - }, { "label": "check-api", "type": "process", diff --git a/devdeps.txt b/devdeps.txt deleted file mode 100644 index c615ff9..0000000 --- a/devdeps.txt +++ /dev/null @@ -1,27 +0,0 @@ -build>=1.0 -duty>=0.10 -black>=23.9 -markdown-callouts>=0.3 -markdown-exec>=1.7 -mkdocs>=1.5 -mkdocs-coverage>=1.0 -mkdocs-gen-files>=0.5 -mkdocs-git-committers-plugin-2>=1.2 -mkdocs-literate-nav>=0.6 -mkdocs-material>=9.4 -mkdocs-minify-plugin>=0.7 -mkdocstrings[python]>=0.23 -tomli>=2.0; python_version < '3.11' -black>=23.9 -blacken-docs>=1.16 -ruff>=0.0 -pytest>=7.4 -pytest-cov>=4.1 -pytest-randomly>=3.15 -pytest-xdist>=3.3 -mypy>=1.5 -tomli-w>=1.0 -types-markdown>=3.5 -types-pyyaml>=6.0 -safety>=2.3 -twine>=5.0 diff --git a/docs/.overrides/main.html b/docs/.overrides/main.html index fcad323..75c5edf 100644 --- a/docs/.overrides/main.html +++ b/docs/.overrides/main.html @@ -2,11 +2,13 @@ {% block announce %} - For updates follow @pawamoy on + Follow + @pawamoy on {% include ".icons/fontawesome/brands/mastodon.svg" %} Fosstodon + for updates {% endblock %} diff --git a/docs/.overrides/partials/comments.html b/docs/.overrides/partials/comments.html new file mode 100644 index 0000000..e18bf1b --- /dev/null +++ b/docs/.overrides/partials/comments.html @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 7ed2331..7bf351a 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -18,7 +18,7 @@ a.autorefs-external::after { height: 1em; width: 1em; - background-color: var(--md-typeset-a-color); + background-color: currentColor; } a.external:hover::after, diff --git a/docs/index.md b/docs/index.md index 612c7a5..8e6f2fb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1,6 @@ +--- +hide: +- feedback +--- + --8<-- "README.md" diff --git a/docs/js/feedback.js b/docs/js/feedback.js new file mode 100644 index 0000000..f97321a --- /dev/null +++ b/docs/js/feedback.js @@ -0,0 +1,14 @@ +const feedback = document.forms.feedback; +feedback.hidden = false; + +feedback.addEventListener("submit", function(ev) { + ev.preventDefault(); + const commentElement = document.getElementById("feedback"); + commentElement.style.display = "block"; + feedback.firstElementChild.disabled = true; + const data = ev.submitter.getAttribute("data-md-value"); + const note = feedback.querySelector(".md-feedback__note [data-md-value='" + data + "']"); + if (note) { + note.hidden = false; + } +}) diff --git a/docs/license.md b/docs/license.md index a873d2b..e81c0ed 100644 --- a/docs/license.md +++ b/docs/license.md @@ -1,3 +1,8 @@ +--- +hide: +- feedback +--- + # License ``` diff --git a/duties.py b/duties.py index 461658c..cf75879 100644 --- a/duties.py +++ b/duties.py @@ -4,18 +4,20 @@ import os import sys +from collections.abc import Iterator from contextlib import contextmanager from cProfile import Profile from importlib.metadata import version as pkgversion from pathlib import Path from pstats import SortKey, Stats from tempfile import TemporaryDirectory -from typing import TYPE_CHECKING, Iterator +from typing import TYPE_CHECKING -from duty import duty -from duty.callables import coverage, lazy, mkdocs, mypy, pytest, ruff, safety +from duty import duty, tools if TYPE_CHECKING: + from collections.abc import Iterator + from duty.context import Context @@ -48,142 +50,71 @@ def material_insiders() -> Iterator[bool]: # noqa: D103 @duty -def changelog(ctx: Context) -> None: +def changelog(ctx: Context, bump: str = "") -> None: """Update the changelog in-place with latest commits. Parameters: - ctx: The context instance (passed automatically). + bump: Bump option passed to git-changelog. """ - from git_changelog.cli import main as git_changelog - - ctx.run(git_changelog, args=[[]], title="Updating changelog") - + ctx.run(tools.git_changelog(bump=bump or None), title="Updating changelog") -@duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies", "check-api"]) -def check(ctx: Context) -> None: # noqa: ARG001 - """Check it all! - Parameters: - ctx: The context instance (passed automatically). - """ +@duty(pre=["check-quality", "check-types", "check-docs", "check-api"]) +def check(ctx: Context) -> None: + """Check it all!""" @duty def check_quality(ctx: Context) -> None: - """Check the code quality. - - Parameters: - ctx: The context instance (passed automatically). - """ + """Check the code quality.""" ctx.run( - ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), + tools.ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), title=pyprefix("Checking code quality"), - command=f"ruff check --config config/ruff.toml {PY_SRC}", - ) - - -@duty -def check_dependencies(ctx: Context) -> None: - """Check for vulnerabilities in dependencies. - - Parameters: - ctx: The context instance (passed automatically). - """ - # retrieve the list of dependencies - requirements = ctx.run( - ["uv", "pip", "freeze"], - silent=True, - allow_overrides=False, - ) - - ctx.run( - safety.check(requirements), - title="Checking dependencies", - command="uv pip freeze | safety check --stdin", ) @duty def check_docs(ctx: Context) -> None: - """Check if the documentation builds correctly. - - Parameters: - ctx: The context instance (passed automatically). - """ + """Check if the documentation builds correctly.""" Path("htmlcov").mkdir(parents=True, exist_ok=True) Path("htmlcov/index.html").touch(exist_ok=True) with material_insiders(): ctx.run( - mkdocs.build(strict=True, verbose=True), + tools.mkdocs.build(strict=True, verbose=True), title=pyprefix("Building documentation"), - command="mkdocs build -vs", ) @duty def check_types(ctx: Context) -> None: - """Check that the code is correctly typed. - - Parameters: - ctx: The context instance (passed automatically). - """ + """Check that the code is correctly typed.""" ctx.run( - mypy.run(*PY_SRC_LIST, config_file="config/mypy.ini"), + tools.mypy(*PY_SRC_LIST, config_file="config/mypy.ini"), title=pyprefix("Type-checking"), - command=f"mypy --config-file config/mypy.ini {PY_SRC}", ) @duty -def check_api(ctx: Context) -> None: - """Check for API breaking changes. - - Parameters: - ctx: The context instance (passed automatically). - """ - from griffe.cli import check as g_check - - griffe_check = lazy(g_check, name="griffe.check") +def check_api(ctx: Context, *cli_args: str) -> None: + """Check for API breaking changes.""" ctx.run( - griffe_check("git_changelog", search_paths=["src"], color=True), + tools.griffe.check("git_changelog", search=["src"], color=True).add_args(*cli_args), title="Checking for API breaking changes", - command="griffe check -ssrc git_changelog", nofail=True, ) -@duty(silent=True) -def clean(ctx: Context) -> None: - """Delete temporary files. - - Parameters: - ctx: The context instance (passed automatically). - """ - - def _rm(*targets: str) -> None: - for target in targets: - ctx.run(f"rm -rf {target}") - - def _find_rm(*targets: str) -> None: - for target in targets: - ctx.run(f"find . -type d -name '{target}' | xargs rm -rf") - - _rm("build", "dist", ".coverage*", "htmlcov", "site", ".pdm-build") - _find_rm(".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__") - - @duty -def docs(ctx: Context, host: str = "127.0.0.1", port: int = 8000) -> None: +def docs(ctx: Context, *cli_args: str, host: str = "127.0.0.1", port: int = 8000) -> None: """Serve the documentation (localhost:8000). Parameters: - ctx: The context instance (passed automatically). host: The host to serve the docs from. port: The port to serve the docs on. """ with material_insiders(): ctx.run( - mkdocs.serve(dev_addr=f"{host}:{port}"), + tools.mkdocs.serve(dev_addr=f"{host}:{port}").add_args(*cli_args), title="Serving documentation", capture=False, ) @@ -191,103 +122,91 @@ def docs(ctx: Context, host: str = "127.0.0.1", port: int = 8000) -> None: @duty def docs_deploy(ctx: Context) -> None: - """Deploy the documentation on GitHub pages. - - Parameters: - ctx: The context instance (passed automatically). - """ + """Deploy the documentation to GitHub pages.""" os.environ["DEPLOY"] = "true" with material_insiders() as insiders: if not insiders: ctx.run(lambda: False, title="Not deploying docs without Material for MkDocs Insiders!") - ctx.run(mkdocs.gh_deploy(), title="Deploying documentation") + ctx.run(tools.mkdocs.gh_deploy(), title="Deploying documentation") @duty def format(ctx: Context) -> None: - """Run formatting tools on the code. - - Parameters: - ctx: The context instance (passed automatically). - """ + """Run formatting tools on the code.""" ctx.run( - ruff.check(*PY_SRC_LIST, config="config/ruff.toml", fix_only=True, exit_zero=True), + tools.ruff.check(*PY_SRC_LIST, config="config/ruff.toml", fix_only=True, exit_zero=True), title="Auto-fixing code", ) - ctx.run(ruff.format(*PY_SRC_LIST, config="config/ruff.toml"), title="Formatting code") + ctx.run(tools.ruff.format(*PY_SRC_LIST, config="config/ruff.toml"), title="Formatting code") + + +@duty +def build(ctx: Context) -> None: + """Build source and wheel distributions.""" + ctx.run( + tools.build(), + title="Building source and wheel distributions", + pty=PTY, + ) + + +@duty +def publish(ctx: Context) -> None: + """Publish source and wheel distributions to PyPI.""" + if not Path("dist").exists(): + ctx.run("false", title="No distribution files found") + dists = [str(dist) for dist in Path("dist").iterdir()] + ctx.run( + tools.twine.upload(*dists, skip_existing=True), + title="Publishing source and wheel distributions to PyPI", + pty=PTY, + ) -@duty(post=["docs-deploy"]) -def release(ctx: Context, version: str) -> None: +@duty(post=["build", "publish", "docs-deploy"]) +def release(ctx: Context, version: str = "") -> None: """Release a new Python package. Parameters: - ctx: The context instance (passed automatically). version: The new version number to use. """ + if not (version := (version or input("> Version to release: ")).strip()): + ctx.run("false", title="A version must be provided") ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY) ctx.run(["git", "commit", "-m", f"chore: Prepare release {version}"], title="Committing changes", pty=PTY) ctx.run(f"git tag {version}", title="Tagging commit", pty=PTY) ctx.run("git push", title="Pushing commits", pty=False) ctx.run("git push --tags", title="Pushing tags", pty=False) - ctx.run("pyproject-build", title="Building dist/wheel", pty=PTY) - ctx.run("twine upload --skip-existing dist/*", title="Publishing version", pty=PTY) - -@duty(silent=True, aliases=["coverage"]) -def cov(ctx: Context) -> None: - """Report coverage as text and HTML. - Parameters: - ctx: The context instance (passed automatically). - """ - ctx.run(coverage.combine, nofail=True) - ctx.run(coverage.report(rcfile="config/coverage.ini"), capture=False) - ctx.run(coverage.html(rcfile="config/coverage.ini")) +@duty(silent=True, aliases=["cov"]) +def coverage(ctx: Context) -> None: + """Report coverage as text and HTML.""" + ctx.run(tools.coverage.combine(), nofail=True) + ctx.run(tools.coverage.report(rcfile="config/coverage.ini"), capture=False) + ctx.run(tools.coverage.html(rcfile="config/coverage.ini")) @duty -def test(ctx: Context, match: str = "") -> None: +def test(ctx: Context, *cli_args: str, match: str = "") -> None: """Run the test suite. Parameters: - ctx: The context instance (passed automatically). match: A pytest expression to filter selected tests. """ py_version = f"{sys.version_info.major}{sys.version_info.minor}" os.environ["COVERAGE_FILE"] = f".coverage.{py_version}" ctx.run( - pytest.run("-n", "auto", "tests", config_file="config/pytest.ini", select=match, color="yes"), + tools.pytest( + "tests", + config_file="config/pytest.ini", + select=match, + color="yes", + ).add_args("-n", "auto", *cli_args), title=pyprefix("Running tests"), - command=f"pytest -c config/pytest.ini -n auto -k{match!r} --color=yes tests", ) -@duty -def vscode(ctx: Context) -> None: - """Configure VSCode. - - This task will overwrite the following files, - so make sure to back them up: - - - `.vscode/launch.json` - - `.vscode/settings.json` - - `.vscode/tasks.json` - - Parameters: - ctx: The context instance (passed automatically). - """ - - def update_config(filename: str) -> None: - source_file = Path("config", "vscode", filename) - target_file = Path(".vscode", filename) - target_file.parent.mkdir(exist_ok=True) - target_file.write_text(source_file.read_text()) - - for filename in ("launch.json", "settings.json", "tasks.json"): - ctx.run(update_config, args=[filename], title=f"Update .vscode/{filename}") - - @duty def profile(ctx: Context, merge: int = 15) -> None: """Profile the parsing and grouping of commits. diff --git a/mkdocs.yml b/mkdocs.yml index e99502c..777b9bb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,6 +75,9 @@ extra_css: - css/material.css - css/mkdocstrings.css +extra_javascript: +- js/feedback.js + markdown_extensions: - attr_list - admonition @@ -126,13 +129,15 @@ plugins: show_root_heading: true show_root_full_path: false show_signature_annotations: true + show_source: true show_symbol_type_heading: true show_symbol_type_toc: true signature_crossrefs: true summary: true -- git-committers: +- git-revision-date-localized: enabled: !ENV [DEPLOY, false] - repository: pawamoy/git-changelog + enable_creation_date: true + type: timeago - minify: minify_html: !ENV [DEPLOY, false] - group: @@ -152,3 +157,15 @@ extra: link: https://gitter.im/git-changelog/community - icon: fontawesome/brands/python link: https://pypi.org/project/git-changelog/ + analytics: + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: Let us know how we can improve this page. diff --git a/pyproject.toml b/pyproject.toml index 12d06b3..c01ba8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "Automatic Changelog generator using Jinja2 templates." authors = [{name = "Timothée Mazzucotelli", email = "dev@pawamoy.fr"}] license = {text = "ISC"} readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" keywords = [ "git", "changelog", @@ -23,10 +23,12 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "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 :: 3.14", "Topic :: Documentation", "Topic :: Software Development", "Topic :: Software Development :: Documentation", @@ -60,3 +62,62 @@ version = {source = "scm"} [tool.pdm.build] package-dir = "src" editable-backend = "editables" + +# Include as much as possible in the source distribution, to help redistributors. +excludes = ["**/.pytest_cache"] +source-includes = [ + "config", + "docs", + "scripts", + "share", + "tests", + "duties.py", + "mkdocs.yml", + "*.md", + "LICENSE", +] + +[tool.pdm.build.wheel-data] +# Manual pages can be included in the wheel. +# Depending on the installation tool, they will be accessible to users. +# pipx supports it, uv does not yet, see https://github.com/astral-sh/uv/issues/4731. +data = [ + {path = "share/**/*", relative-to = "."}, +] + +[dependency-groups] +dev = [ + # dev + "editables>=0.5", + + # maintenance + "build>=1.2", + "git-changelog>=2.5", + "twine>=5.1", + + # ci + "duty>=1.4", + "ruff>=0.4", + "pytest>=8.2", + "pytest-cov>=5.0", + "pytest-randomly>=3.15", + "pytest-xdist>=3.6", + "mypy>=1.10", + "types-markdown>=3.6", + "types-pyyaml>=6.0", + + # docs + "black>=24.4", + "markdown-callouts>=0.4", + "markdown-exec>=1.8", + "mkdocs>=1.6", + "mkdocs-coverage>=1.0", + "mkdocs-gen-files>=0.5", + "mkdocs-git-revision-date-localized-plugin>=1.2", + "mkdocs-literate-nav>=0.6", + "mkdocs-material>=9.5", + "mkdocs-minify-plugin>=0.8", + "mkdocstrings[python]>=0.25", + # YORE: EOL 3.10: Remove line. + "tomli>=2.0; python_version < '3.11'", +] \ No newline at end of file diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index 27f94d6..2e5d492 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -5,17 +5,18 @@ import os import sys from collections import defaultdict +from collections.abc import Iterable from importlib.metadata import distributions from itertools import chain from pathlib import Path from textwrap import dedent -from typing import Dict, Iterable, Union +from typing import Union from jinja2 import StrictUndefined from jinja2.sandbox import SandboxedEnvironment from packaging.requirements import Requirement -# TODO: Remove once support for Python 3.10 is dropped. +# YORE: EOL 3.10: Replace block with line 2. if sys.version_info >= (3, 11): import tomllib else: @@ -26,11 +27,10 @@ pyproject = tomllib.load(pyproject_file) project = pyproject["project"] project_name = project["name"] -with project_dir.joinpath("devdeps.txt").open() as devdeps_file: - devdeps = [line.strip() for line in devdeps_file if not line.startswith("-e")] +devdeps = [dep for dep in pyproject["dependency-groups"]["dev"] if not dep.startswith("-e")] -PackageMetadata = Dict[str, Union[str, Iterable[str]]] -Metadata = Dict[str, PackageMetadata] +PackageMetadata = dict[str, Union[str, Iterable[str]]] +Metadata = dict[str, PackageMetadata] def _merge_fields(metadata: dict) -> PackageMetadata: @@ -88,7 +88,7 @@ def _set_license(metadata: PackageMetadata) -> None: def _get_deps(base_deps: dict[str, Requirement], metadata: Metadata) -> Metadata: deps = {} for dep_name, dep_req in base_deps.items(): - if dep_name not in metadata: + if dep_name not in metadata or dep_name == "git-changelog": continue metadata[dep_name]["spec"] |= {str(spec) for spec in dep_req.specifier} # type: ignore[operator] metadata[dep_name]["extras"] |= dep_req.extras # type: ignore[operator] @@ -131,8 +131,8 @@ def _render_credits() -> str: template_data = { "project_name": project_name, - "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: str(dep["name"])), - "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: str(dep["name"])), + "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: str(dep["name"]).lower()), + "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: str(dep["name"]).lower()), "more_credits": "http://pawamoy.github.io/credits/", } template_text = dedent( diff --git a/scripts/gen_ref_nav.py b/scripts/gen_ref_nav.py index b369536..6939e86 100644 --- a/scripts/gen_ref_nav.py +++ b/scripts/gen_ref_nav.py @@ -29,7 +29,7 @@ with mkdocs_gen_files.open(full_doc_path, "w") as fd: ident = ".".join(parts) - fd.write(f"::: {ident}") + fd.write(f"---\ntitle: {ident}\n---\n\n::: {ident}") mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path.relative_to(root)) diff --git a/scripts/make b/scripts/make deleted file mode 100755 index 9a8bce9..0000000 --- a/scripts/make +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env bash - -set -e -export PYTHON_VERSIONS=${PYTHON_VERSIONS-3.8 3.9 3.10 3.11 3.12} - -exe="" -prefix="" - - -# Install runtime and development dependencies, -# as well as current project in editable mode. -uv_install() { - uv pip compile pyproject.toml devdeps.txt | uv pip install -r - - if [ -z "${CI}" ]; then - uv pip install -e . - else - uv pip install "git-changelog @ ." - fi -} - - -# Setup the development environment by installing dependencies -# in multiple Python virtual environments with uv: -# one venv per Python version in `.venvs/$py`, -# and an additional default venv in `.venv`. -setup() { - if ! command -v uv &>/dev/null; then - echo "make: setup: uv must be installed, see https://github.com/astral-sh/uv" >&2 - return 1 - fi - - if [ -n "${PYTHON_VERSIONS}" ]; then - for version in ${PYTHON_VERSIONS}; do - if [ ! -d ".venvs/${version}" ]; then - uv venv --python "${version}" ".venvs/${version}" - fi - VIRTUAL_ENV="${PWD}/.venvs/${version}" uv_install - done - fi - - if [ ! -d .venv ]; then uv venv --python python; fi - uv_install -} - - -# Activate a Python virtual environments. -# The annoying operating system also requires -# that we set some global variables to help it find commands... -activate() { - local path - if [ -f "$1/bin/activate" ]; then - source "$1/bin/activate" - return 0 - fi - if [ -f "$1/Scripts/activate.bat" ]; then - "$1/Scripts/activate.bat" - exe=".exe" - prefix="$1/Scripts/" - return 0 - fi - echo "run: Cannot activate venv $1" >&2 - return 1 -} - - -# Run a command in all configured Python virtual environments. -# We handle the case when the `PYTHON_VERSIONS` environment variable -# is unset or empty, for robustness. -multirun() { - local cmd="$1" - shift - - if [ -n "${PYTHON_VERSIONS}" ]; then - for version in ${PYTHON_VERSIONS}; do - (activate ".venvs/${version}" && MULTIRUN=1 "${prefix}${cmd}${exe}" "$@") - done - else - (activate .venv && "${prefix}${cmd}${exe}" "$@") - fi -} - - -# Run a command in the default Python virtual environment. -# We rely on `multirun`'s handling of empty `PYTHON_VERSIONS`. -singlerun() { - PYTHON_VERSIONS= multirun "$@" -} - - -# Record options following a command name, -# until a non-option argument is met or there are no more arguments. -# Output each option on a new line, so the parent caller can store them in an array. -# Return the number of times the parent caller must shift arguments. -options() { - local shift_count=0 - for arg in "$@"; do - if [[ "${arg}" =~ ^- || "${arg}" =~ ^.+= ]]; then - echo "${arg}" - ((shift_count++)) - else - break - fi - done - return ${shift_count} -} - - -# Main function. -main() { - local cmd - while [ $# -ne 0 ]; do - cmd="$1" - shift - - # Handle `run` early to simplify `case` below. - if [ "${cmd}" = "run" ]; then - singlerun "$@" - exit $? - fi - - # Handle `multirun` early to simplify `case` below. - if [ "${cmd}" = "multirun" ]; then - multirun "$@" - exit $? - fi - - # All commands except `run` and `multirun` can be chained on a single line. - # Some of them accept options in two formats: `-f`, `--flag` and `param=value`. - # Some of them don't, and will print warnings/errors if options were given. - opts=("$(options "$@")") && opts=() || shift $? - - case "${cmd}" in - # The following commands require special handling. - help|"") - singlerun duty --list ;; - setup) - setup ;; - check) - multirun duty check-quality check-types check-docs - singlerun duty check-dependencies check-api - ;; - - # The following commands run in all venvs. - check-quality|\ - check-docs|\ - check-types|\ - test) - multirun duty "${cmd}" "${opts[@]}" ;; - - # The following commands run in the default venv only. - *) - singlerun duty "${cmd}" "${opts[@]}" ;; - esac - done -} - - -# Execute the main function. -main "$@" diff --git a/scripts/make b/scripts/make new file mode 120000 index 0000000..c2eda0d --- /dev/null +++ b/scripts/make @@ -0,0 +1 @@ +make.py \ No newline at end of file diff --git a/scripts/make.py b/scripts/make.py new file mode 100755 index 0000000..a9f9f8f --- /dev/null +++ b/scripts/make.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +"""Management commands.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from contextlib import contextmanager +from pathlib import Path +from textwrap import dedent +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Iterator + + +PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split() + + +def shell(cmd: str, *, capture_output: bool = False, **kwargs: Any) -> str | None: + """Run a shell command.""" + if capture_output: + return subprocess.check_output(cmd, shell=True, text=True, **kwargs) # noqa: S602 + subprocess.run(cmd, shell=True, check=True, stderr=subprocess.STDOUT, **kwargs) # noqa: S602 + return None + + +@contextmanager +def environ(**kwargs: str) -> Iterator[None]: + """Temporarily set environment variables.""" + original = dict(os.environ) + os.environ.update(kwargs) + try: + yield + finally: + os.environ.clear() + os.environ.update(original) + + +def uv_install(venv: Path) -> None: + """Install dependencies using uv.""" + with environ(UV_PROJECT_ENVIRONMENT=str(venv), PYO3_USE_ABI3_FORWARD_COMPATIBILITY="1"): + if "CI" in os.environ: + shell("uv sync --no-editable") + else: + shell("uv sync") + + +def setup() -> None: + """Setup the project.""" + if not shutil.which("uv"): + raise ValueError("make: setup: uv must be installed, see https://github.com/astral-sh/uv") + + print("Installing dependencies (default environment)") + default_venv = Path(".venv") + if not default_venv.exists(): + shell("uv venv") + uv_install(default_venv) + + if PYTHON_VERSIONS: + for version in PYTHON_VERSIONS: + print(f"\nInstalling dependencies (python{version})") + venv_path = Path(f".venvs/{version}") + if not venv_path.exists(): + shell(f"uv venv --python {version} {venv_path}") + with environ(UV_PROJECT_ENVIRONMENT=str(venv_path.resolve())): + uv_install(venv_path) + + +def run(version: str, cmd: str, *args: str, no_sync: bool = False, **kwargs: Any) -> None: + """Run a command in a virtual environment.""" + kwargs = {"check": True, **kwargs} + uv_run = ["uv", "run"] + if no_sync: + uv_run.append("--no-sync") + if version == "default": + with environ(UV_PROJECT_ENVIRONMENT=".venv"): + subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510 + else: + with environ(UV_PROJECT_ENVIRONMENT=f".venvs/{version}", MULTIRUN="1"): + subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510 + + +def multirun(cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command for all configured Python versions.""" + if PYTHON_VERSIONS: + for version in PYTHON_VERSIONS: + run(version, cmd, *args, **kwargs) + else: + run("default", cmd, *args, **kwargs) + + +def allrun(cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command in all virtual environments.""" + run("default", cmd, *args, **kwargs) + if PYTHON_VERSIONS: + multirun(cmd, *args, **kwargs) + + +def clean() -> None: + """Delete build artifacts and cache files.""" + paths_to_clean = ["build", "dist", "htmlcov", "site", ".coverage*", ".pdm-build"] + for path in paths_to_clean: + shutil.rmtree(path, ignore_errors=True) + + cache_dirs = {".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"} + for dirpath in Path(".").rglob("*/"): + if dirpath.parts[0] not in (".venv", ".venvs") and dirpath.name in cache_dirs: + shutil.rmtree(dirpath, ignore_errors=True) + + +def vscode() -> None: + """Configure VSCode to work on this project.""" + shutil.copytree("config/vscode", ".vscode", dirs_exist_ok=True) + + +def main() -> int: + """Main entry point.""" + args = list(sys.argv[1:]) + if not args or args[0] == "help": + if len(args) > 1: + run("default", "duty", "--help", args[1]) + else: + print( + dedent( + """ + Available commands + help Print this help. Add task name to print help. + setup Setup all virtual environments (install dependencies). + run Run a command in the default virtual environment. + multirun Run a command for all configured Python versions. + allrun Run a command in all virtual environments. + 3.x Run a command in the virtual environment for Python 3.x. + clean Delete build artifacts and cache files. + vscode Configure VSCode to work on this project. + """, + ), + flush=True, + ) + if os.path.exists(".venv"): + print("\nAvailable tasks", flush=True) + run("default", "duty", "--list", no_sync=True) + return 0 + + while args: + cmd = args.pop(0) + + if cmd == "run": + run("default", *args) + return 0 + + if cmd == "multirun": + multirun(*args) + return 0 + + if cmd == "allrun": + allrun(*args) + return 0 + + if cmd.startswith("3."): + run(cmd, *args) + return 0 + + opts = [] + while args and (args[0].startswith("-") or "=" in args[0]): + opts.append(args.pop(0)) + + if cmd == "clean": + clean() + elif cmd == "setup": + setup() + elif cmd == "vscode": + vscode() + elif cmd == "check": + multirun("duty", "check-quality", "check-types", "check-docs") + run("default", "duty", "check-api") + elif cmd in {"check-quality", "check-docs", "check-types", "test"}: + multirun("duty", cmd, *opts) + else: + run("default", "duty", cmd, *opts) + + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except subprocess.CalledProcessError as process: + if process.output: + print(process.output, file=sys.stderr) + sys.exit(process.returncode)