diff --git a/.copier-answers.yml b/.copier-answers.yml index e89749b..5e7178a 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 1.2.10 +_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 ec028a9..c13146a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/1-bug.md @@ -53,7 +53,7 @@ PASTE TRACEBACK HERE python -m markdown_exec.debug # | 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 ba02e7e..f72fec7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,20 +25,23 @@ 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: Set up Graphviz uses: ts-graphviz/setup-graphviz@v1 - - 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 @@ -66,11 +69,11 @@ jobs: echo 'jobs=[ {"os": "macos-latest"}, {"os": "windows-latest"}, - {"python-version": "3.9"}, {"python-version": "3.10"}, {"python-version": "3.11"}, {"python-version": "3.12"}, - {"python-version": "3.13"} + {"python-version": "3.13"}, + {"python-version": "3.14"} ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT else echo 'jobs=[ @@ -89,31 +92,38 @@ 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: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.python-version == '3.13' }} + 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: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eac0256..e347718 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,35 +11,34 @@ 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 build - if: github.repository_owner == 'pawamoy-insiders' - run: python -m pip install build + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Setup uv + uses: astral-sh/setup-uv@v3 - name: Build dists if: github.repository_owner == 'pawamoy-insiders' - run: python -m build + run: uv tool run --from build pyproject-build - name: Upload dists artifact uses: actions/upload-artifact@v4 if: github.repository_owner == 'pawamoy-insiders' with: name: markdown-exec-insiders path: ./dist/* - - name: Install git-changelog - if: github.repository_owner != 'pawamoy-insiders' - run: pip install git-changelog - name: Prepare release notes if: github.repository_owner != 'pawamoy-insiders' - run: git-changelog --release-notes > release-notes.md + run: uv tool run git-changelog --release-notes > release-notes.md - name: Create release with assets - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: github.repository_owner == 'pawamoy-insiders' with: files: ./dist/* - name: Create release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: github.repository_owner != 'pawamoy-insiders' 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 de03bfc..beb903c 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. @@ -36,13 +35,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/README.md b/README.md index 1501746..e41c8b7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![ci](https://github.com/pawamoy/markdown-exec/workflows/ci/badge.svg)](https://github.com/pawamoy/markdown-exec/actions?query=workflow%3Aci) [![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://pawamoy.github.io/markdown-exec/) [![pypi version](https://img.shields.io/pypi/v/markdown-exec.svg)](https://pypi.org/project/markdown-exec/) -[![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/markdown-exec) [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#markdown-exec:gitter.im) Utilities to execute code blocks in Markdown files. @@ -13,8 +12,6 @@ and this HTML is injected in place of the code block. ## Installation -With `pip`: - ```bash pip install markdown-exec[ansi] ``` diff --git a/config/ruff.toml b/config/ruff.toml index 06025fd..a5c3889 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/devdeps.txt b/devdeps.txt deleted file mode 100644 index 1cbc830..0000000 --- a/devdeps.txt +++ /dev/null @@ -1,49 +0,0 @@ -# dev -editables>=0.5 - -# maintenance -build>=1.2 -git-changelog>=2.5 -twine>=5.1; python_version < '3.13' - -# 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 -mkdocs>=1.6 -mkdocs-coverage>=1.0 -mkdocs-gen-files>=0.5 -mkdocs-git-committers-plugin-2>=2.3 -mkdocs-literate-nav>=0.6 -mkdocs-material>=9.5 -mkdocs-minify-plugin>=0.8 -mkdocstrings[python]>=0.25 -tomli>=2.0; python_version < '3.11' - -# docs gallery -pydeps>=1.12; python_version > '3.8' and python_version < '3.13' -diagrams>=0.21; python_version > '3.8' and python_version < '3.13' -rich>=12.3; python_version > '3.8' and python_version < '3.13' -matplotlib>=3.5; python_version > '3.8' and python_version < '3.13' -numpy>=1.24.4; python_version > '3.8' and python_version < '3.13' -textual>=0.67; python_version > '3.8' and python_version < '3.13' -pytermgui>=6.3; python_version > '3.8' and python_version < '3.13' -pipdeptree>=2.6; python_version > '3.8' and python_version < '3.13' -pip>=24; python_version > '3.8' and python_version < '3.13' -pygments>=2.15; python_version > '3.8' and python_version < '3.13' -drawsvg>=2.3; python_version > '3.8' and python_version < '3.13' -hyperbolic>=2.0; python_version > '3.8' and python_version < '3.13' -qrcode>=7.4; python_version > '3.8' and python_version < '3.13' -plotly>=5.22; python_version > '3.8' and python_version < '3.13' -pandas>=2.2; python_version > '3.8' and python_version < '3.13' -chalk-diagrams>=0.2; python_version > '3.8' and python_version < '3.13' diff --git a/docs/.overrides/main.html b/docs/.overrides/main.html index cf8adeb..1e95685 100644 --- a/docs/.overrides/main.html +++ b/docs/.overrides/main.html @@ -2,17 +2,19 @@ {% block announce %} - Sponsorship - is now available! + Fund this project through + sponsorship {% include ".icons/octicons/heart-fill-16.svg" %} — - 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..93c190d --- /dev/null +++ b/docs/.overrides/partials/comments.html @@ -0,0 +1,57 @@ + + + \ No newline at end of file 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/insiders/index.md b/docs/insiders/index.md index f0b5738..eed55b7 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -95,6 +95,10 @@ with your GitHub account, visit [pawamoy's sponsor profile][github sponsor profi and complete a sponsorship of **$10 a month or more**. You can use your individual or organization GitHub account for sponsoring. +Sponsorships lower than $10 a month are also very much appreciated, and useful. +They won't grant you access to Insiders, but they will be counted towards reaching sponsorship goals. +*Every* sponsorship helps us implementing new features and releasing them to the public. + **Important**: If you're sponsoring **[@pawamoy][github sponsor profile]** through a GitHub organization, please send a short email to insiders@pawamoy.fr with the name of your 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 d0c3187..f8e1669 100644 --- a/duties.py +++ b/duties.py @@ -7,11 +7,13 @@ import sys from contextlib import contextmanager from importlib.metadata import version as pkgversion from pathlib import Path -from typing import TYPE_CHECKING, Iterator +from typing import TYPE_CHECKING from duty import duty, tools if TYPE_CHECKING: + from collections.abc import Iterator + from duty.context import Context @@ -57,8 +59,8 @@ def changelog(ctx: Context, bump: str = "") -> None: 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 +@duty(pre=["check-quality", "check-types", "check-docs", "check-api"]) +def check(ctx: Context) -> None: """Check it all!""" @@ -119,19 +121,33 @@ def docs(ctx: Context, *cli_args: str, host: str = "127.0.0.1", port: int = 8000 @duty(skip_if=not below_312, skip_reason=skip_docs_reason) -def docs_deploy(ctx: Context) -> None: - """Deploy the documentation to GitHub pages.""" +def docs_deploy(ctx: Context, *, force: bool = False) -> None: + """Deploy the documentation to GitHub pages. + + Parameters: + force: Whether to force deployment, even from non-Insiders version. + """ 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!") origin = ctx.run("git config --get remote.origin.url", silent=True, allow_overrides=False) if "pawamoy-insiders/markdown-exec" in origin: - ctx.run("git remote add upstream git@github.com:pawamoy/markdown-exec", silent=True, nofail=True) + ctx.run( + "git remote add upstream git@github.com:pawamoy/markdown-exec", + silent=True, + nofail=True, + allow_overrides=False, + ) ctx.run( tools.mkdocs.gh_deploy(remote_name="upstream", force=True), title="Deploying documentation", ) + elif force: + ctx.run( + tools.mkdocs.gh_deploy(force=True), + title="Deploying documentation", + ) else: ctx.run( lambda: False, diff --git a/mkdocs.yml b/mkdocs.yml index ea7c3b5..8cf7dd2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -86,6 +86,9 @@ extra_css: - css/mkdocstrings.css - css/insiders.css +extra_javascript: +- js/feedback.js + markdown_extensions: - admonition - attr_list @@ -154,9 +157,10 @@ plugins: show_symbol_type_toc: true signature_crossrefs: true summary: true -- git-committers: +- git-revision-date-localized: enabled: !ENV [DEPLOY, false] - repository: pawamoy/markdown-exec + enable_creation_date: true + type: timeago - minify: minify_html: !ENV [DEPLOY, false] - group: @@ -176,3 +180,15 @@ extra: link: https://gitter.im/markdown-exec/community - icon: fontawesome/brands/python link: https://pypi.org/project/markdown-exec/ + 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 4ad5f8f..8ae860f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "Utilities to execute code blocks in Markdown files." authors = [{name = "Timothée Mazzucotelli", email = "dev@pawamoy.fr"}] license = {text = "ISC"} readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" keywords = ["markdown", "python", "exec", "shell", "bash", "mkdocs"] dynamic = ["version"] classifiers = [ @@ -17,12 +17,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 :: Utilities", @@ -54,6 +54,8 @@ 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", @@ -61,7 +63,6 @@ source-includes = [ "scripts", "share", "tests", - "devdeps.txt", "duties.py", "mkdocs.yml", "*.md", @@ -69,6 +70,64 @@ source-includes = [ ] [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'", + + # docs gallery + "pydeps>=1.12; python_version < '3.13'", + "diagrams>=0.21; python_version < '3.13'", + "rich>=12.3; python_version < '3.13'", + "matplotlib>=3.5; python_version < '3.13'", + "numpy>=1.24.4; python_version < '3.13'", + "textual>=0.67; python_version < '3.13'", + "pytermgui>=6.3; python_version < '3.13'", + "pipdeptree>=2.6; python_version < '3.13'", + "pip>=24; python_version < '3.13'", + "pygments>=2.15; python_version < '3.13'", + "drawsvg>=2.3; python_version < '3.13'", + "hyperbolic>=2.0; python_version < '3.13'", + "qrcode>=7.4; python_version < '3.13'", + "plotly>=5.22; python_version < '3.13'", + "pandas>=2.2; python_version < '3.13'", + "chalk-diagrams>=0.2; python_version < '3.13'", +] \ No newline at end of file diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index 88c146b..04a869a 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -5,17 +5,18 @@ from __future__ import annotations 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 @@ with project_dir.joinpath("pyproject.toml").open("rb") as pyproject_file: 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 line.strip() and not line.strip().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: diff --git a/scripts/insiders.py b/scripts/insiders.py index 1521248..849c631 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -10,13 +10,16 @@ from dataclasses import dataclass from datetime import date, datetime, timedelta from itertools import chain from pathlib import Path -from typing import Iterable, cast +from typing import TYPE_CHECKING, cast from urllib.error import HTTPError from urllib.parse import urljoin from urllib.request import urlopen import yaml +if TYPE_CHECKING: + from collections.abc import Iterable + logger = logging.getLogger(f"mkdocs.logs.{__name__}") diff --git a/scripts/make b/scripts/make deleted file mode 100755 index 1a74268..0000000 --- a/scripts/make +++ /dev/null @@ -1,203 +0,0 @@ -#!/usr/bin/env python3 -"""Management commands.""" - -import os -import shutil -import subprocess -import sys -from contextlib import contextmanager -from pathlib import Path -from typing import Any, Iterator - -PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.8 3.9 3.10 3.11 3.12 3.13").split() - -exe = "" -prefix = "" - - -def shell(cmd: str) -> None: - """Run a shell command.""" - subprocess.run(cmd, shell=True, check=True) # noqa: S602 - - -@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() -> None: - """Install dependencies using uv.""" - uv_opts = "" - if "UV_RESOLUTION" in os.environ: - uv_opts = f"--resolution={os.getenv('UV_RESOLUTION')}" - cmd = f"uv pip compile {uv_opts} --all-extras pyproject.toml devdeps.txt | uv pip install -r -" - shell(cmd) - if "CI" not in os.environ: - shell("uv pip install --no-deps -e .") - else: - shell("uv pip install --no-deps .") - - -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)") # noqa: T201 - default_venv = Path(".venv") - if not default_venv.exists(): - shell("uv venv --python python") - uv_install() - - if PYTHON_VERSIONS: - for version in PYTHON_VERSIONS: - print(f"\nInstalling dependencies (python{version})") # noqa: T201 - venv_path = Path(f".venvs/{version}") - if not venv_path.exists(): - shell(f"uv venv --python {version} {venv_path}") - with environ(VIRTUAL_ENV=str(venv_path.resolve())): - uv_install() - - -def activate(path: str) -> None: - """Activate a virtual environment.""" - global exe, prefix # noqa: PLW0603 - - if (bin := Path(path, "bin")).exists(): - activate_script = bin / "activate_this.py" - elif (scripts := Path(path, "Scripts")).exists(): - activate_script = scripts / "activate_this.py" - exe = ".exe" - prefix = f"{path}/Scripts/" - else: - raise ValueError(f"make: activate: Cannot find activation script in {path}") - - if not activate_script.exists(): - raise ValueError(f"make: activate: Cannot find activation script in {path}") - - exec(activate_script.read_text(), {"__file__": str(activate_script)}) # noqa: S102 - - -def run(version: str, cmd: str, *args: str, **kwargs: Any) -> None: - """Run a command in a virtual environment.""" - kwargs = {"check": True, **kwargs} - if version == "default": - activate(".venv") - subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510 - else: - activate(f".venvs/{version}") - os.environ["MULTIRUN"] = "1" - subprocess.run([f"{prefix}{cmd}{exe}", *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: - shell(f"rm -rf {path}") - - cache_dirs = [".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"] - for dirpath in Path(".").rglob("*"): - if any(dirpath.match(pattern) for pattern in cache_dirs) and not (dirpath.match(".venv") or dirpath.match(".venvs")): - shutil.rmtree(path, ignore_errors=True) - - -def vscode() -> None: - """Configure VSCode to work on this project.""" - Path(".vscode").mkdir(parents=True, exist_ok=True) - shell("cp -v config/vscode/* .vscode") - - -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("Available commands") # noqa: T201 - print(" help Print this help. Add task name to print help.") # noqa: T201 - print(" setup Setup all virtual environments (install dependencies).") # noqa: T201 - print(" run Run a command in the default virtual environment.") # noqa: T201 - print(" multirun Run a command for all configured Python versions.") # noqa: T201 - print(" allrun Run a command in all virtual environments.") # noqa: T201 - print(" 3.x Run a command in the virtual environment for Python 3.x.") # noqa: T201 - print(" clean Delete build artifacts and cache files.") # noqa: T201 - print(" vscode Configure VSCode to work on this project.") # noqa: T201 - try: - run("default", "python", "-V", capture_output=True) - except (subprocess.CalledProcessError, ValueError): - pass - else: - print("\nAvailable tasks") # noqa: T201 - run("default", "duty", "--list") - 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 Exception: # noqa: BLE001 - sys.exit(1) 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..cf3a489 --- /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 --all-extras --no-editable") + else: + shell("uv sync --all-extras") + + +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)