chore: Template upgrade

This commit is contained in:
Timothée Mazzucotelli
2024-12-03 13:56:51 +01:00
parent 0d86a983a7
commit 103bc1dc5f
27 changed files with 477 additions and 335 deletions
+1 -1
View File
@@ -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
@@ -53,7 +53,7 @@ PASTE TRACEBACK HERE
python -m markdown_exec.debug # | xclip -selection clipboard
```
PASTE OUTPUT HERE
PASTE MARKDOWN OUTPUT HERE
### Additional context
<!-- Add any other relevant context about the problem here,
+16
View File
@@ -0,0 +1,16 @@
---
name: Documentation update
about: Point at unclear, missing or outdated documentation.
title: "docs: "
labels: docs
assignees: pawamoy
---
### Is something unclear, missing or outdated in our documentation?
<!-- A clear and concise description of what the documentation issue is. Ex. I can't find an explanation on feature [...]. -->
### Relevant code snippets
<!-- If the documentation issue is related to code, please provide relevant code snippets. -->
### Link to the relevant documentation section
<!-- Add a link to the relevant section of our documentation, or any addition context. -->
+18
View File
@@ -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.
<!-- A clear and concise description of what the problem is. -->
### Describe the solution you'd like
<!-- A clear and concise description of what you want to happen. -->
### Describe alternatives you've considered
<!-- A clear and concise description of any alternative solutions you've considered. -->
### Additional context
<!-- Add any other context or screenshots about the change request here. -->
+24 -14
View File
@@ -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:
+12 -13
View File
@@ -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
+1
View File
@@ -15,6 +15,7 @@
/.pdm-build/
/htmlcov/
/site/
uv.lock
# cache
.cache/
-6
View File
@@ -1,6 +0,0 @@
FROM gitpod/workspace-full
USER gitpod
ENV PIP_USER=no
RUN pip3 install pipx; \
pipx install uv; \
pipx ensurepath
-13
View File
@@ -1,13 +0,0 @@
vscode:
extensions:
- ms-python.python
image:
file: .gitpod.dockerfile
ports:
- port: 8000
onOpen: notify
tasks:
- init: make setup
+7 -10
View File
@@ -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)
-3
View File
@@ -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]
```
+1 -1
View File
@@ -1,4 +1,4 @@
target-version = "py38"
target-version = "py39"
line-length = 120
[lint]
-49
View File
@@ -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'
+5 -3
View File
@@ -2,17 +2,19 @@
{% block announce %}
<a href="{{ 'insiders/#how-to-become-a-sponsor' | url }}"><strong>Sponsorship</strong></a>
is now available!
<strong>Fund this project</strong> through
<a href="{{ 'insiders/#how-to-become-a-sponsor' | url }}"><strong>sponsorship</strong></a>
<span class="twemoji heart pulse">
{% include ".icons/octicons/heart-fill-16.svg" %}
</span> &mdash;
For updates follow <strong>@pawamoy</strong> on
Follow
<strong>@pawamoy</strong> on
<a rel="me" href="https://fosstodon.org/@pawamoy">
<span class="twemoji mastodon">
{% include ".icons/fontawesome/brands/mastodon.svg" %}
</span>
<strong>Fosstodon</strong>
</a>
for updates
{% endblock %}
+57
View File
@@ -0,0 +1,57 @@
<!-- Giscus -->
<!-- https://squidfunk.github.io/mkdocs-material/setup/adding-a-comment-system/#giscus-integration -->
<div id="feedback" style="display: none;">
<h2 id="__comments">Feedback</h2>
<script src="https://giscus.app/client.js"
data-repo="pawamoy/markdown-exec"
data-repo-id="R_kgDOG1IOMQ"
data-category="Documentation"
data-category-id="DIC_kwDOG1IOMc4Ck2cD"
data-mapping="pathname"
data-strict="1"
data-reactions-enabled="0"
data-emit-metadata="0"
data-input-position="top"
data-theme="preferred_color_scheme"
data-lang="en"
data-loading="lazy"
crossorigin="anonymous"
async>
</script>
<!-- Synchronize Giscus theme with palette -->
<script>
var giscus = document.querySelector("script[src*=giscus]")
// Set palette on initial load
var palette = __md_get("__palette")
if (palette && typeof palette.color === "object") {
var theme = palette.color.scheme === "slate"
? "transparent_dark"
: "light"
// Instruct Giscus to set theme
giscus.setAttribute("data-theme", theme)
}
// Register event handlers after documented loaded
document.addEventListener("DOMContentLoaded", function() {
var ref = document.querySelector("[data-md-component=palette]")
ref.addEventListener("change", function() {
var palette = __md_get("__palette")
if (palette && typeof palette.color === "object") {
var theme = palette.color.scheme === "slate"
? "transparent_dark"
: "light"
// Instruct Giscus to change theme
var frame = document.querySelector(".giscus-frame")
frame.contentWindow.postMessage(
{ giscus: { setConfig: { theme } } },
"https://giscus.app"
)
}
})
})
</script>
</div>
+5
View File
@@ -1 +1,6 @@
---
hide:
- feedback
---
--8<-- "README.md"
+4
View File
@@ -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
+14
View File
@@ -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;
}
})
+5
View File
@@ -1,3 +1,8 @@
---
hide:
- feedback
---
# License
```
+22 -6
View File
@@ -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,
+18 -2
View File
@@ -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.
+62 -3
View File
@@ -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'",
]
+6 -6
View File
@@ -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:
+4 -1
View File
@@ -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__}")
-203
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
make.py
+193
View File
@@ -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)