From d687a805783bcfcbd5e67bea56a816816be4060f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 16 May 2024 12:37:32 +0200 Subject: [PATCH] chore: Switch to Copier UV template --- .copier-answers.yml | 4 +- .envrc | 1 + .github/FUNDING.yml | 5 + .github/workflows/ci.yml | 51 +++++--- .gitignore | 34 ++--- .gitpod.dockerfile | 2 +- CONTRIBUTING.md | 10 +- Makefile | 59 +++------ README.md | 4 +- config/black.toml | 3 - config/coverage.ini | 3 +- config/git-changelog.toml | 1 + config/pytest.ini | 6 - config/ruff.toml | 68 ++++------ config/vscode/launch.json | 11 ++ config/vscode/settings.json | 23 +--- config/vscode/tasks.json | 80 +++++++----- devdeps.txt | 35 +++++ docs/css/mkdocstrings.css | 2 +- duties.py | 156 ++++++++-------------- pyproject.toml | 49 +------ scripts/gen_credits.py | 146 ++++++++++++++------- scripts/gen_ref_nav.py | 2 +- scripts/make | 242 +++++++++++++++++++++++++++++++++++ scripts/setup.sh | 25 ---- src/mkdocs_autorefs/debug.py | 5 +- 26 files changed, 603 insertions(+), 424 deletions(-) create mode 100644 .envrc create mode 100644 .github/FUNDING.yml delete mode 100644 config/black.toml create mode 100644 devdeps.txt create mode 100755 scripts/make delete mode 100755 scripts/setup.sh diff --git a/.copier-answers.yml b/.copier-answers.yml index 8073f79..3a9ffe1 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,6 +1,6 @@ # Changes here will be overwritten by Copier -_commit: 1.2.6 -_src_path: gh:pawamoy/copier-pdm +_commit: 1.2.4 +_src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothée Mazzucotelli author_username: pawamoy diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..f9d77ee --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +PATH_add scripts diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..a502284 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +github: pawamoy +ko_fi: pawamoy +polar: pawamoy +custom: +- https://www.paypal.me/pawamoy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80e7915..51cea12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ env: LANG: en_US.utf-8 LC_ALL: en_US.utf-8 PYTHONIOENCODING: UTF-8 + PYTHON_VERSIONS: "" jobs: @@ -28,36 +29,35 @@ jobs: - name: Fetch all tags run: git fetch --depth=1 --tags - - name: Set up PDM - uses: pdm-project/setup-pdm@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.11" - - name: Resolving dependencies - run: pdm lock -v --no-cross-platform -G ci-quality + - name: Install uv + run: pip install uv - name: Install dependencies - run: pdm install -G ci-quality + run: make setup - name: Check if the documentation builds correctly - run: pdm run duty check-docs + run: make check-docs - name: Check the code quality - run: pdm run duty check-quality + run: make check-quality - name: Check if the code is correctly typed - run: pdm run duty check-types + run: make check-types - name: Check for vulnerabilities in dependencies - run: pdm run duty check-dependencies + run: make check-dependencies - name: Check for breaking changes in the API - run: pdm run duty check-api + run: make check-api tests: strategy: - max-parallel: 4 matrix: os: - ubuntu-latest @@ -69,24 +69,35 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" + 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.13' }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up PDM - uses: pdm-project/setup-pdm@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - allow-python-prereleases: true + allow-prereleases: true - - name: Resolving dependencies - run: pdm lock -v --no-cross-platform -G ci-tests + - name: Install uv + run: pip install uv - name: Install dependencies - run: pdm install --no-editable -G ci-tests + env: + UV_RESOLUTION: ${{ matrix.resolution }} + run: make setup - name: Run the test suite - run: pdm run duty test + run: make test diff --git a/.gitignore b/.gitignore index f59144c..41fee62 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,24 @@ +# editors .idea/ .vscode/ -__pycache__/ -*.py[cod] -dist/ + +# python *.egg-info/ -build/ -htmlcov/ -.coverage* -pip-wheel-metadata/ -.pytest_cache/ -.python-version -site/ -pdm.lock -pdm.toml -.pdm-plugins/ -.pdm-python -__pypackages__/ +*.py[cod] .venv/ +.venvs/ +/build/ +/dist/ + +# tools +.coverage* +/.pdm-build/ +/htmlcov/ +/site/ + +# cache .cache/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +__pycache__/ diff --git a/.gitpod.dockerfile b/.gitpod.dockerfile index 0e6d9d3..1590b41 100644 --- a/.gitpod.dockerfile +++ b/.gitpod.dockerfile @@ -2,5 +2,5 @@ FROM gitpod/workspace-full USER gitpod ENV PIP_USER=no RUN pip3 install pipx; \ - pipx install pdm; \ + pipx install uv; \ pipx ensurepath diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c243dd4..d162b4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,18 +17,18 @@ make setup > NOTE: > If it fails for some reason, > you'll need to install -> [PDM](https://github.com/pdm-project/pdm) +> [uv](https://github.com/astral-sh/uv) > manually. > > You can install it with: > > ```bash > python3 -m pip install --user pipx -> pipx install pdm +> pipx install uv > ``` > > Now you can try running `make setup` again, -> or simply `pdm install`. +> or simply `uv install`. You now have the dependencies installed. @@ -39,13 +39,13 @@ Run `make help` to see all the available actions! 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 `pdm run duty 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. If you work in VSCode, we provide -[an action to configure VSCode](https://pawamoy.github.io/copier-pdm/work/#vscode-setup) +[an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup) for the project. ## Development diff --git a/Makefile b/Makefile index bd432a4..aede0fe 100644 --- a/Makefile +++ b/Makefile @@ -1,54 +1,29 @@ -.DEFAULT_GOAL := help -SHELL := bash -DUTY := $(if $(VIRTUAL_ENV),,pdm run) duty -export PDM_MULTIRUN_VERSIONS ?= 3.8 3.9 3.10 3.11 3.12 -export PDM_MULTIRUN_USE_VENVS ?= $(if $(shell pdm config python.use_venv | grep True),1,0) +# If you have `direnv` loaded in your shell, and allow it in the repository, +# the `make` command will point at the `scripts/make` shell script. +# This Makefile is just here to allow auto-completion in the terminal. -args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)")) -check_quality_args = files -docs_args = host port -release_args = version -test_args = cleancov match - -BASIC_DUTIES = \ +actions = \ + allrun \ changelog \ + check \ check-api \ check-dependencies \ + check-docs \ + check-quality \ + check-types \ clean \ coverage \ docs \ docs-deploy \ format \ + help \ + multirun \ release \ + run \ + setup \ + test \ vscode -QUALITY_DUTIES = \ - check-quality \ - check-docs \ - check-types \ - test - -.PHONY: help -help: - @$(DUTY) --list - -.PHONY: lock -lock: - @pdm lock -G:all - -.PHONY: setup -setup: - @bash scripts/setup.sh - -.PHONY: check -check: - @pdm multirun duty check-quality check-types check-docs - @$(DUTY) check-dependencies check-api - -.PHONY: $(BASIC_DUTIES) -$(BASIC_DUTIES): - @$(DUTY) $@ $(call args,$@) - -.PHONY: $(QUALITY_DUTIES) -$(QUALITY_DUTIES): - @pdm multirun duty $@ $(call args,$@) +.PHONY: $(actions) +$(actions): + @bash scripts/make "$@" diff --git a/README.md b/README.md index 7eebcd7..c315800 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # mkdocs-autorefs [![ci](https://github.com/mkdocstrings/autorefs/workflows/ci/badge.svg)](https://github.com/mkdocstrings/autorefs/actions?query=workflow%3Aci) -[![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://mkdocstrings.github.io/autorefs/) +[![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://mkdocstrings.github.io/autorefs/) [![pypi version](https://img.shields.io/pypi/v/mkdocs-autorefs.svg)](https://pypi.org/project/mkdocs-autorefs/) [![conda version](https://img.shields.io/conda/vn/conda-forge/mkdocs-autorefs.svg)](https://anaconda.org/conda-forge/mkdocs-autorefs) -[![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/autorefs) +[![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/autorefs) [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#autorefs:gitter.im) Automatically link across pages in MkDocs. diff --git a/config/black.toml b/config/black.toml deleted file mode 100644 index d24affe..0000000 --- a/config/black.toml +++ /dev/null @@ -1,3 +0,0 @@ -[tool.black] -line-length = 120 -exclude = "tests/fixtures" diff --git a/config/coverage.ini b/config/coverage.ini index fde9d55..cdc6e59 100644 --- a/config/coverage.ini +++ b/config/coverage.ini @@ -8,7 +8,8 @@ source = [coverage:paths] equivalent = src/ - __pypackages__/ + .venv/lib/*/site-packages/ + .venvs/*/lib/*/site-packages/ [coverage:report] ignore_errors = True diff --git a/config/git-changelog.toml b/config/git-changelog.toml index 44e2b1f..57114e0 100644 --- a/config/git-changelog.toml +++ b/config/git-changelog.toml @@ -6,3 +6,4 @@ parse-refs = false parse-trailers = true sections = ["build", "deps", "feat", "fix", "refactor"] template = "keepachangelog" +versioning = "pep440" diff --git a/config/pytest.ini b/config/pytest.ini index e2c7163..796da7d 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -1,10 +1,4 @@ [pytest] -norecursedirs = - .git - .tox - .env - dist - build python_files = test_*.py *_test.py diff --git a/config/ruff.toml b/config/ruff.toml index 02cf84e..2ced7b4 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -1,53 +1,26 @@ target-version = "py38" line-length = 120 + +[lint] exclude = [ - "fixtures", - "site", + "tests/fixtures/*.py", ] select = [ - "A", - "ANN", - "ARG", - "B", - "BLE", - "C", - "C4", + "A", "ANN", "ARG", + "B", "BLE", + "C", "C4", "COM", - "D", - "DTZ", - "E", - "ERA", - "EXE", - "F", - "FBT", + "D", "DTZ", + "E", "ERA", "EXE", + "F", "FBT", "G", - "I", - "ICN", - "INP", - "ISC", + "I", "ICN", "INP", "ISC", "N", - "PGH", - "PIE", - "PL", - "PLC", - "PLE", - "PLR", - "PLW", - "PT", - "PYI", + "PGH", "PIE", "PL", "PLC", "PLE", "PLR", "PLW", "PT", "PYI", "Q", - "RUF", - "RSE", - "RET", - "S", - "SIM", - "SLF", - "T", - "T10", - "T20", - "TCH", - "TID", - "TRY", + "RUF", "RSE", "RET", + "S", "SIM", "SLF", + "T", "T10", "T20", "TCH", "TID", "TRY", "UP", "W", "YTT", @@ -73,7 +46,7 @@ ignore = [ "TRY003", # Avoid specifying long messages outside the exception class ] -[per-file-ignores] +[lint.per-file-ignores] "src/*/cli.py" = [ "T201", # Print statement ] @@ -91,18 +64,21 @@ ignore = [ "S101", # Use of assert detected ] -[flake8-quotes] +[lint.flake8-quotes] docstring-quotes = "double" -[flake8-tidy-imports] +[lint.flake8-tidy-imports] ban-relative-imports = "all" -[isort] +[lint.isort] known-first-party = ["mkdocs_autorefs"] -[pydocstyle] +[lint.pydocstyle] convention = "google" [format] +exclude = [ + "tests/fixtures/*.py", +] docstring-code-format = true docstring-code-line-length = 80 diff --git a/config/vscode/launch.json b/config/vscode/launch.json index d056cce..e328838 100644 --- a/config/vscode/launch.json +++ b/config/vscode/launch.json @@ -9,6 +9,17 @@ "console": "integratedTerminal", "justMyCode": false }, + { + "name": "docs", + "type": "debugpy", + "request": "launch", + "module": "mkdocs", + "justMyCode": false, + "args": [ + "serve", + "-v" + ] + }, { "name": "test", "type": "debugpy", diff --git a/config/vscode/settings.json b/config/vscode/settings.json index 17beee4..949856d 100644 --- a/config/vscode/settings.json +++ b/config/vscode/settings.json @@ -1,29 +1,9 @@ { "files.watcherExclude": { - "**/__pypackages__/**": true, "**/.venv*/**": true, + "**/.venvs*/**": true, "**/venv*/**": true }, - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" - }, - "python.autoComplete.extraPaths": [ - "__pypackages__/3.8/lib", - "__pypackages__/3.9/lib", - "__pypackages__/3.10/lib", - "__pypackages__/3.11/lib", - "__pypackages__/3.12/lib" - ], - "python.analysis.extraPaths": [ - "__pypackages__/3.8/lib", - "__pypackages__/3.9/lib", - "__pypackages__/3.10/lib", - "__pypackages__/3.11/lib", - "__pypackages__/3.12/lib" - ], - "black-formatter.args": [ - "--config=config/black.toml" - ], "mypy-type-checker.args": [ "--config-file=config/mypy.ini" ], @@ -32,6 +12,7 @@ "python.testing.pytestArgs": [ "--config-file=config/pytest.ini" ], + "ruff.enable": true, "ruff.format.args": [ "--config=config/ruff.toml" ], diff --git a/config/vscode/tasks.json b/config/vscode/tasks.json index 80cd13d..30008cf 100644 --- a/config/vscode/tasks.json +++ b/config/vscode/tasks.json @@ -3,84 +3,94 @@ "tasks": [ { "label": "changelog", - "type": "shell", - "command": "pdm run duty changelog" + "type": "process", + "command": "scripts/make", + "args": ["changelog"] }, { "label": "check", - "type": "shell", - "command": "pdm run duty check" + "type": "process", + "command": "scripts/make", + "args": ["check"] }, { "label": "check-quality", - "type": "shell", - "command": "pdm run duty check-quality" + "type": "process", + "command": "scripts/make", + "args": ["check-quality"] }, { "label": "check-types", - "type": "shell", - "command": "pdm run duty check-types" + "type": "process", + "command": "scripts/make", + "args": ["check-types"] }, { "label": "check-docs", - "type": "shell", - "command": "pdm run duty check-docs" + "type": "process", + "command": "scripts/make", + "args": ["check-docs"] }, { "label": "check-dependencies", - "type": "shell", - "command": "pdm run duty check-dependencies" + "type": "process", + "command": "scripts/make", + "args": ["check-dependencies"] }, { "label": "check-api", - "type": "shell", - "command": "pdm run duty check-api" + "type": "process", + "command": "scripts/make", + "args": ["check-api"] }, { "label": "clean", - "type": "shell", - "command": "pdm run duty clean" + "type": "process", + "command": "scripts/make", + "args": ["clean"] }, { "label": "docs", - "type": "shell", - "command": "pdm run duty docs" + "type": "process", + "command": "scripts/make", + "args": ["docs"] }, { "label": "docs-deploy", - "type": "shell", - "command": "pdm run duty docs-deploy" + "type": "process", + "command": "scripts/make", + "args": ["docs-deploy"] }, { "label": "format", - "type": "shell", - "command": "pdm run duty format" - }, - { - "label": "lock", - "type": "shell", - "command": "pdm lock -G:all" + "type": "process", + "command": "scripts/make", + "args": ["format"] }, { "label": "release", - "type": "shell", - "command": "pdm run duty release ${input:version}" + "type": "process", + "command": "scripts/make", + "args": ["release", "${input:version}"] }, { "label": "setup", - "type": "shell", - "command": "bash scripts/setup.sh" + "type": "process", + "command": "scripts/make", + "args": ["setup"] }, { "label": "test", - "type": "shell", - "command": "pdm run duty test coverage", + "type": "process", + "command": "scripts/make", + "args": ["test", "coverage"], "group": "test" }, { "label": "vscode", - "type": "shell", - "command": "pdm run duty vscode" + "type": "process", + "command": "scripts/make", + "args": ["vscode"] } ], "inputs": [ diff --git a/devdeps.txt b/devdeps.txt new file mode 100644 index 0000000..7f44351 --- /dev/null +++ b/devdeps.txt @@ -0,0 +1,35 @@ +# dev +editables>=0.5 + +# maintenance +build>=1.0 +git-changelog>=2.3 +twine>=5.0 + +# ci +duty>=0.10 +ruff>=0.0 +pygments>=2.16 +pymdown-extensions>=10.0 +pytest>=7.4 +pytest-cov>=4.1 +pytest-randomly>=3.15 +pytest-xdist>=3.3 +mypy>=1.5 +types-markdown>=3.5 +types-pyyaml>=6.0 +safety>=2.3 + +# docs +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' \ No newline at end of file diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 727a614..88c7357 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/duties.py b/duties.py index 0da5c11..59d449f 100644 --- a/duties.py +++ b/duties.py @@ -22,7 +22,7 @@ CI = os.environ.get("CI", "0") in {"1", "true", "yes", ""} WINDOWS = os.name == "nt" PTY = not WINDOWS and not CI -MULTIRUN = os.environ.get("PDM_MULTIRUN", "0") == "1" +MULTIRUN = os.environ.get("MULTIRUN", "0") == "1" def pyprefix(title: str) -> str: # noqa: D103 @@ -45,33 +45,26 @@ 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") + args = [f"--bump={bump}"] if bump else [] + ctx.run(git_changelog, args=[args], title="Updating changelog", command="git-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). - """ + """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"), title=pyprefix("Checking code quality"), @@ -81,32 +74,24 @@ def check_quality(ctx: Context) -> None: @duty def check_dependencies(ctx: Context) -> None: - """Check for vulnerabilities in dependencies. - - Parameters: - ctx: The context instance (passed automatically). - """ + """Check for vulnerabilities in dependencies.""" # retrieve the list of dependencies requirements = ctx.run( - ["pdm", "export", "-f", "requirements", "--without-hashes"], - title="Exporting dependencies as requirements", + ["uv", "pip", "freeze"], + silent=True, allow_overrides=False, ) ctx.run( safety.check(requirements), title="Checking dependencies", - command="pdm export -f requirements --without-hashes | safety check --stdin", + 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(): @@ -119,11 +104,7 @@ def check_docs(ctx: Context) -> None: @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"), title=pyprefix("Type-checking"), @@ -133,11 +114,7 @@ def check_types(ctx: Context) -> None: @duty def check_api(ctx: Context) -> None: - """Check for API breaking changes. - - Parameters: - ctx: The context instance (passed automatically). - """ + """Check for API breaking changes.""" from griffe.cli import check as g_check griffe_check = lazy(g_check, name="griffe.check") @@ -149,31 +126,11 @@ def check_api(ctx: Context) -> None: ) -@duty(silent=True) -def clean(ctx: Context) -> None: - """Delete temporary files. - - Parameters: - ctx: The context instance (passed automatically). - """ - ctx.run("rm -rf .coverage*") - ctx.run("rm -rf .mypy_cache") - ctx.run("rm -rf .pytest_cache") - ctx.run("rm -rf build") - ctx.run("rm -rf dist") - ctx.run("rm -rf htmlcov") - ctx.run("rm -rf pip-wheel-metadata") - ctx.run("rm -rf site") - ctx.run("find . -type d -name __pycache__ | xargs rm -rf") - ctx.run("find . -name '*.rej' -delete") - - @duty def docs(ctx: Context, 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. """ @@ -187,11 +144,7 @@ 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 on GitHub pages.""" os.environ["DEPLOY"] = "true" with material_insiders() as insiders: if not insiders: @@ -201,11 +154,7 @@ def docs_deploy(ctx: Context) -> None: @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), title="Auto-fixing code", @@ -213,30 +162,56 @@ def format(ctx: Context) -> None: ctx.run(ruff.format(*PY_SRC_LIST, config="config/ruff.toml"), title="Formatting code") -@duty(post=["docs-deploy"]) -def release(ctx: Context, version: str) -> None: +@duty +def build(ctx: Context) -> None: + """Build source and wheel distributions.""" + from build.__main__ import main as pyproject_build + + ctx.run( + pyproject_build, + args=[()], + title="Building source and wheel distributions", + command="pyproject-build", + pty=PTY, + ) + + +@duty +def publish(ctx: Context) -> None: + """Publish source and wheel distributions to PyPI.""" + from twine.cli import dispatch as twine_upload + + if not Path("dist").exists(): + ctx.run("false", title="No distribution files found") + dists = [str(dist) for dist in Path("dist").iterdir()] + ctx.run( + twine_upload, + args=[["upload", "-r", "pypi", "--skip-existing", *dists]], + title="Publish source and wheel distributions to PyPI", + command="twine upload -r pypi --skip-existing dist/*", + pty=PTY, + ) + + +@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("pdm 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). - """ + """Report coverage as text and HTML.""" ctx.run(coverage.combine, nofail=True) ctx.run(coverage.report(rcfile="config/coverage.ini"), capture=False) ctx.run(coverage.html(rcfile="config/coverage.ini")) @@ -247,8 +222,6 @@ def test(ctx: Context, match: str = "") -> None: """Run the test suite. Parameters: - ctx: The context instance (passed automatically). - cleancov: Whether to remove the `.coverage` file before running the tests. match: A pytest expression to filter selected tests. """ py_version = f"{sys.version_info.major}{sys.version_info.minor}" @@ -258,28 +231,3 @@ def test(ctx: Context, match: str = "") -> None: 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}") diff --git a/pyproject.toml b/pyproject.toml index d341542..f1442b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Documentation", "Topic :: Software Development", "Topic :: Software Development :: Documentation", @@ -52,53 +53,13 @@ autorefs = "mkdocs_autorefs.plugin:AutorefsPlugin" [tool.pdm] version = {source = "scm"} -plugins = [ - "pdm-multirun", -] [tool.pdm.build] package-dir = "src" editable-backend = "editables" +source-includes = ["share"] -[tool.pdm.dev-dependencies] -duty = ["duty>=0.10"] -ci-quality = ["mkdocs-autorefs[duty,docs,quality,typing,security]"] -ci-tests = ["mkdocs-autorefs[duty,tests]"] -docs = [ - "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'", -] -maintain = [ - "black>=23.9", - "blacken-docs>=1.16", - "git-changelog>=2.3", -] -quality = [ - "ruff>=0.0", -] -tests = [ - "pygments>=2.16", - "pymdown-extensions>=10.0", - "pytest>=7.4", - "pytest-cov>=4.1", - "pytest-randomly>=3.15", - "pytest-xdist>=3.3", -] -typing = [ - "mypy>=1.5", - "types-markdown>=3.5", - "types-pyyaml>=6.0", -] -security = [ - "safety>=2.3", +[tool.pdm.build.wheel-data] +data = [ + {path = "share/**/*", relative-to = "."}, ] diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index bf35f0d..0f9645c 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -3,16 +3,17 @@ from __future__ import annotations import os -import re import sys -from importlib.metadata import PackageNotFoundError, metadata +from collections import defaultdict +from importlib.metadata import distributions from itertools import chain from pathlib import Path from textwrap import dedent -from typing import Mapping, cast +from typing import Dict, Iterable, 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. if sys.version_info >= (3, 11): @@ -24,71 +25,114 @@ with project_dir.joinpath("pyproject.toml").open("rb") as pyproject_file: pyproject = tomllib.load(pyproject_file) project = pyproject["project"] -pdm = pyproject["tool"]["pdm"] -with project_dir.joinpath("pdm.lock").open("rb") as lock_file: - lock_data = tomllib.load(lock_file) -lock_pkgs = {pkg["name"].lower(): pkg for pkg in lock_data["package"]} project_name = project["name"] -regex = re.compile(r"(?P[\w.-]+)(?P.*)$") +with project_dir.joinpath("devdeps.txt").open() as devdeps_file: + devdeps = [line.strip() for line in devdeps_file if line.strip() and not line.strip().startswith(("-e", "#"))] +PackageMetadata = Dict[str, Union[str, Iterable[str]]] +Metadata = Dict[str, PackageMetadata] -def _get_license(pkg_name: str) -> str: + +def _merge_fields(metadata: dict) -> PackageMetadata: + fields = defaultdict(list) + for header, value in metadata.items(): + fields[header.lower()].append(value.strip()) + return { + field: value if len(value) > 1 or field in ("classifier", "requires-dist") else value[0] + for field, value in fields.items() + } + + +def _norm_name(name: str) -> str: + return name.replace("_", "-").replace(".", "-").lower() + + +def _requirements(deps: list[str]) -> dict[str, Requirement]: + return {_norm_name((req := Requirement(dep)).name): req for dep in deps} + + +def _extra_marker(req: Requirement) -> str | None: + if not req.marker: + return None try: - data = metadata(pkg_name) - except PackageNotFoundError: - return "?" - license_name = cast(dict, data).get("License", "").strip() - multiple_lines = bool(license_name.count("\n")) - # TODO: Remove author logic once all my packages licenses are fixed. - author = "" - if multiple_lines or not license_name or license_name == "UNKNOWN": - for header, value in cast(dict, data).items(): - if header == "Classifier" and value.startswith("License ::"): - license_name = value.rsplit("::", 1)[1].strip() - elif header == "Author-email": - author = value - if license_name == "Other/Proprietary License" and "pawamoy" in author: - license_name = "ISC" - return license_name or "?" - - -def _get_deps(base_deps: Mapping[str, Mapping[str, str]]) -> dict[str, dict[str, str]]: + return next(marker[2].value for marker in req.marker._markers if getattr(marker[0], "value", None) == "extra") + except StopIteration: + return None + + +def _get_metadata() -> Metadata: + metadata = {} + for pkg in distributions(): + name = _norm_name(pkg.name) # type: ignore[attr-defined,unused-ignore] + metadata[name] = _merge_fields(pkg.metadata) # type: ignore[arg-type] + metadata[name]["spec"] = set() + metadata[name]["extras"] = set() + metadata[name].setdefault("summary", "") + _set_license(metadata[name]) + return metadata + + +def _set_license(metadata: PackageMetadata) -> None: + license_field = metadata.get("license-expression", metadata.get("license", "")) + license_name = license_field if isinstance(license_field, str) else " + ".join(license_field) + check_classifiers = license_name in ("UNKNOWN", "Dual License", "") or license_name.count("\n") + if check_classifiers: + license_names = [] + for classifier in metadata["classifier"]: + if classifier.startswith("License ::"): + license_names.append(classifier.rsplit("::", 1)[1].strip()) + license_name = " + ".join(license_names) + metadata["license"] = license_name or "?" + + +def _get_deps(base_deps: dict[str, Requirement], metadata: Metadata) -> Metadata: deps = {} - for dep in base_deps: - parsed = regex.match(dep).groupdict() # type: ignore[union-attr] - dep_name = parsed["dist"].lower() - if dep_name not in lock_pkgs: + for dep_name, dep_req in base_deps.items(): + if dep_name not in metadata or dep_name == "mkdocs-autorefs": continue - deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]} + 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] + deps[dep_name] = metadata[dep_name] again = True while again: again = False - for pkg_name in lock_pkgs: + for pkg_name in metadata: if pkg_name in deps: - for pkg_dependency in lock_pkgs[pkg_name].get("dependencies", []): - parsed = regex.match(pkg_dependency).groupdict() # type: ignore[union-attr] - dep_name = parsed["dist"].lower() - if dep_name in lock_pkgs and dep_name not in deps and dep_name != project["name"]: - deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]} + for pkg_dependency in metadata[pkg_name].get("requires-dist", []): + requirement = Requirement(pkg_dependency) + dep_name = _norm_name(requirement.name) + extra_marker = _extra_marker(requirement) + if ( + dep_name in metadata + and dep_name not in deps + and dep_name != project["name"] + and (not extra_marker or extra_marker in deps[pkg_name]["extras"]) + ): + metadata[dep_name]["spec"] |= {str(spec) for spec in requirement.specifier} # type: ignore[operator] + deps[dep_name] = metadata[dep_name] again = True return deps def _render_credits() -> str: - dev_dependencies = _get_deps(chain(*pdm.get("dev-dependencies", {}).values())) # type: ignore[arg-type] + metadata = _get_metadata() + dev_dependencies = _get_deps(_requirements(devdeps), metadata) prod_dependencies = _get_deps( - chain( # type: ignore[arg-type] - project.get("dependencies", []), - chain(*project.get("optional-dependencies", {}).values()), + _requirements( + chain( # type: ignore[arg-type] + project.get("dependencies", []), + chain(*project.get("optional-dependencies", {}).values()), + ), ), + metadata, ) template_data = { "project_name": project_name, - "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: dep["name"]), - "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: 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( @@ -97,14 +141,15 @@ def _render_credits() -> str: These projects were used to build *{{ project_name }}*. **Thank you!** - [`python`](https://www.python.org/) | - [`pdm`](https://pdm.fming.dev/) | - [`copier-pdm`](https://github.com/pawamoy/copier-pdm) + [Python](https://www.python.org/) | + [uv](https://github.com/astral-sh/uv) | + [copier-uv](https://github.com/pawamoy/copier-uv) {% macro dep_line(dep) -%} - [`{{ dep.name }}`](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} + [{{ dep.name }}](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec|sort(reverse=True)|join(", ") ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} {%- endmacro %} + {% if prod_dependencies -%} ### Runtime dependencies Project | Summary | Version (accepted) | Version (last resolved) | License @@ -113,6 +158,8 @@ def _render_credits() -> str: {{ dep_line(dep) }} {% endfor %} + {% endif -%} + {% if dev_dependencies -%} ### Development dependencies Project | Summary | Version (accepted) | Version (last resolved) | License @@ -121,6 +168,7 @@ def _render_credits() -> str: {{ dep_line(dep) }} {% endfor %} + {% endif -%} {% if more_credits %}**[More credits from the author]({{ more_credits }})**{% endif %} """, ) 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 new file mode 100755 index 0000000..11a3c5f --- /dev/null +++ b/scripts/make @@ -0,0 +1,242 @@ +#!/usr/bin/env bash + +set -e +export PYTHON_VERSIONS=${PYTHON_VERSIONS-3.8 3.9 3.10 3.11 3.12 3.13} + +exe="" +prefix="" + + +# Install runtime and development dependencies, +# as well as current project in editable mode. +uv_install() { + local uv_opts + if [ -n "${UV_RESOLUTION}" ]; then + uv_opts="--resolution=${UV_RESOLUTION}" + fi + uv pip compile ${uv_opts} pyproject.toml devdeps.txt | uv pip install -r - + if [ -z "${CI}" ]; then + uv pip install --no-deps -e . + else + uv pip install --no-deps . + 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 a specific virtual environment. +run() { + local version="$1" + local cmd="$2" + shift 2 + + if [ "${version}" = "default" ]; then + (activate .venv && "${prefix}${cmd}${exe}" "$@") + else + (activate ".venvs/${version}" && MULTIRUN=1 "${prefix}${cmd}${exe}" "$@") + fi +} + + +# Run a command in all configured Python virtual environments. +# We allow `PYTHON_VERSIONS` to be empty, and in that case +# we run the command in the default virtual environment only. +multirun() { + if [ -n "${PYTHON_VERSIONS}" ]; then + for version in ${PYTHON_VERSIONS}; do + run "${version}" "$@" + done + else + run default "$@" + fi +} + + +# Run a command in all configured Python virtual environments, +# as well as in the default virtual environment. +allrun() { + run default "$@" + if [ -n "${PYTHON_VERSIONS}" ]; then + multirun "$@" + fi +} + + +# Clean project by deleting build artifacts and cache files. +clean() { + rm -rf build + rm -rf dist + rm -rf htmlcov + rm -rf site + rm -rf .coverage* + rm -rf .pdm-build + + find . -type d \ + -path ./.venv -prune \ + -path ./.venvs -prune \ + -o -name .cache \ + -o -name .pytest_cache \ + -o -name .mypy_cache \ + -o -name .ruff_cache \ + -o -name __pycache__ | + xargs rm -rf +} + +# 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` +vscode() { + mkdir -p .vscode &>/dev/null + cp -v config/vscode/* .vscode +} + +# 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 + + if [ $# -eq 0 ] || [ "$1" = "help" ]; then + if [ -n "$2" ]; then + run default duty --help "$2" + else + echo "Available commands" + echo " help Print this help. Add task name to print help." + echo " setup Setup all virtual environments (install dependencies)." + echo " run Run a command in the default virtual environment." + echo " multirun Run a command for all configured Python versions." + echo " allrun Run a command in all virtual environments." + echo " 3.x Run a command in the virtual environment for Python 3.x." + echo " clean Delete build artifacts and cache files." + echo " vscode Configure VSCode to work on this project." + if run default python -V &>/dev/null; then + echo + echo "Available tasks" + run default duty --list + fi + fi + exit 0 + fi + + while [ $# -ne 0 ]; do + cmd="$1" + shift + + # Handle `run` early to simplify `case` below. + if [ "${cmd}" = "run" ]; then + run default "$@" + exit $? + fi + + # Handle `multirun` early to simplify `case` below. + if [ "${cmd}" = "multirun" ]; then + multirun "$@" + exit $? + fi + + # Handle `allrun` early to simplify `case` below. + if [ "${cmd}" = "allrun" ]; then + allrun "$@" + exit $? + fi + + # Handle `3.x` early to simplify `case` below. + if [[ "${cmd}" = 3.* ]]; then + run "${cmd}" "$@" + 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. + # The following statement reads options into an array. A syntax quirk means + # that with no options, the array still contains a single empty string. + # In that case, the `options` function returned 0, so we can empty the array. + opts=("$(options "$@")") && opts=() || shift $? + + case "${cmd}" in + # The following commands require special handling. + check) + multirun duty check-quality check-types check-docs + run default duty check-dependencies check-api + ;; + clean|setup|vscode) + "${cmd}" ;; + + # 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. + *) + run default duty "${cmd}" "${opts[@]}" ;; + esac + done +} + + +# Execute the main function. +main "$@" diff --git a/scripts/setup.sh b/scripts/setup.sh deleted file mode 100755 index eef3843..0000000 --- a/scripts/setup.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -set -e - -if ! command -v pdm &>/dev/null; then - if ! command -v pipx &>/dev/null; then - python3 -m pip install --user pipx - fi - pipx install pdm -fi -if ! pdm self list 2>/dev/null | grep -q pdm-multirun; then - pdm install --plugins -fi - -if [ -n "${PDM_MULTIRUN_VERSIONS}" ]; then - if [ "${PDM_MULTIRUN_USE_VENVS}" -eq "1" ]; then - for version in ${PDM_MULTIRUN_VERSIONS}; do - if ! pdm venv --path "${version}" &>/dev/null; then - pdm venv create --name "${version}" "${version}" - fi - done - fi - pdm multirun -v pdm install -G:all -else - pdm install -G:all -fi diff --git a/src/mkdocs_autorefs/debug.py b/src/mkdocs_autorefs/debug.py index 05af0e6..5ca07e3 100644 --- a/src/mkdocs_autorefs/debug.py +++ b/src/mkdocs_autorefs/debug.py @@ -37,6 +37,8 @@ class Environment: """Python interpreter name.""" interpreter_version: str """Python interpreter version.""" + interpreter_path: str + """Path to Python executable.""" platform: str """Operating System.""" packages: list[Package] @@ -83,6 +85,7 @@ def get_debug_info() -> Environment: return Environment( interpreter_name=py_name, interpreter_version=py_version, + interpreter_path=sys.executable, platform=platform.platform(), variables=[Variable(var, val) for var in variables if (val := os.getenv(var))], packages=[Package(pkg, get_version(pkg)) for pkg in packages], @@ -93,7 +96,7 @@ def print_debug_info() -> None: """Print debug/environment information.""" info = get_debug_info() print(f"- __System__: {info.platform}") - print(f"- __Python__: {info.interpreter_name} {info.interpreter_version}") + print(f"- __Python__: {info.interpreter_name} {info.interpreter_version} ({info.interpreter_path})") print("- __Environment variables__:") for var in info.variables: print(f" - `{var.name}`: `{var.value}`")