diff --git a/.copier-answers.yml b/.copier-answers.yml index 1cbdf80..5434b81 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.1.0 +_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 index 01e293a..a502284 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +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 cea0d6f..a34ed24 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: @@ -31,31 +32,31 @@ jobs: - name: Set up Graphviz uses: ts-graphviz/setup-graphviz@v1 - - 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 exclude-test-jobs: runs-on: ubuntu-latest @@ -81,7 +82,6 @@ jobs: needs: exclude-test-jobs strategy: - max-parallel: 4 matrix: os: - ubuntu-latest @@ -101,17 +101,17 @@ jobs: - 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 + run: make setup - name: Run the test suite - run: pdm run duty test + run: make test diff --git a/.gitignore b/.gitignore index 588e34e..41fee62 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,24 @@ +# editors .idea/ .vscode/ -__pycache__/ -*.py[cod] -dist/ + +# python *.egg-info/ -build/ -htmlcov/ +*.py[cod] +.venv/ +.venvs/ +/build/ +/dist/ + +# tools .coverage* -pip-wheel-metadata/ +/.pdm-build/ +/htmlcov/ +/site/ + +# cache +.cache/ .pytest_cache/ .mypy_cache/ -site/ -pdm.lock -pdm.toml -.pdm-plugins/ -.pdm-python -__pypackages__/ -.venv/ -.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 aa9313d..de03bfc 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 8ad5209..771b333 100644 --- a/Makefile +++ b/Makefile @@ -1,54 +1,27 @@ -.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 = match - -BASIC_DUTIES = \ +actions = \ changelog \ + check \ check-api \ check-dependencies \ + check-docs \ + check-quality \ + check-types \ clean \ coverage \ docs \ docs-deploy \ format \ + help \ 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/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 97d3229..b56a286 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] precision = 2 diff --git a/config/pytest.ini b/config/pytest.ini index 5a49395..ebdeb48 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 b414815..06025fd 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 = ["markdown_exec"] -[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..b198878 --- /dev/null +++ b/devdeps.txt @@ -0,0 +1,37 @@ +build>=1.0 +duty>=0.10 +black>=23.9 +markdown-callouts>=0.3 +markdown-exec>=1.7 +mkdocs>=1.5 +mkdocs-coverage>=1.0 +mkdocs-gen-files>=0.5 +mkdocs-git-committers-plugin-2>=1.2 +mkdocs-literate-nav>=0.6 +mkdocs-material>=9.4 +mkdocs-minify-plugin>=0.7 +mkdocstrings[python]>=0.23 +tomli>=2.0; python_version < '3.11' +pydeps>=1.12 +diagrams>=0.21 +rich>=12.3 +matplotlib>=3.5 +numpy>=1.23 +textual +pytermgui>=6.3 +pipdeptree>=2.6 +pip>=24 +black>=23.9 +blacken-docs>=1.16 +git-changelog>=2.3 +ruff>=0.0 +pytest>=7.4 +pytest-cov>=4.1 +pytest-randomly>=3.15 +pytest-xdist>=3.3 +pygments>=2.15 +mypy>=1.5 +types-markdown>=3.5 +types-pyyaml>=6.0 +safety>=2.3 +twine>=5.0 diff --git a/docs/insiders/goals.yml b/docs/insiders/goals.yml index beb69a4..7d1fc0a 100644 --- a/docs/insiders/goals.yml +++ b/docs/insiders/goals.yml @@ -5,3 +5,12 @@ goals: - name: Pyodide fence ref: /usage/pyodide/ since: 2023/04/26 + 1000: + name: GraviFridge Fluid Renewal + features: [] + 1500: + name: HyperLamp Navigation Tips + features: [] + 2000: + name: FusionDrive Ejection Configuration + features: [] diff --git a/docs/insiders/index.md b/docs/insiders/index.md index f47f7fe..f0b5738 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -169,7 +169,7 @@ This section lists all funding goals that were previously completed, which means that those features were part of Insiders, but are now generally available and can be used by all users. -```python exec="1" session="insiders" +```python exec="1" session="insiders" idprefix="" for goal in goals.values(): if goal.complete: goal.render() diff --git a/duties.py b/duties.py index 6d9c118..afecd47 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 @@ -92,15 +92,15 @@ def check_dependencies(ctx: Context) -> None: """ # 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", ) @@ -160,17 +160,17 @@ def clean(ctx: Context) -> None: 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 tests/.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") + + def _rm(*targets: str) -> None: + for target in targets: + ctx.run(f"rm -rf {target}") + + def _find_rm(*targets: str) -> None: + for target in targets: + ctx.run(f"find . -type d -name '{target}' | xargs rm -rf") + + _rm("build", "dist", ".coverage*", "htmlcov", "site", ".pdm-build") + _find_rm(".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__") @duty(skip_if=not below_312, skip_reason=skip_docs_reason) @@ -249,7 +249,7 @@ def release(ctx: Context, version: str) -> None: 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("pyproject-build", title="Building dist/wheel", pty=PTY) ctx.run("twine upload --skip-existing dist/*", title="Publishing version", pty=PTY) diff --git a/pyproject.toml b/pyproject.toml index 5f9791d..1b0fdda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,61 +49,7 @@ markdown-exec = "markdown_exec.mkdocs_plugin:MarkdownExecPlugin" [tool.pdm] version = {source = "scm"} -plugins = [ - "pdm-multirun", -] [tool.pdm.build] package-dir = "src" editable-backend = "editables" - -[tool.pdm.dev-dependencies] -duty = ["duty>=0.10"] -ci-quality = ["markdown-exec[duty,docs,quality,typing,security]"] -ci-tests = ["markdown-exec[duty,tests]"] -docs = [ - "black>=23.9", - "markdown-callouts>=0.3", - "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'", - # gallery dependencies - "toml>=0.10; python_version < '3.12'", - "pydeps>=1.12; python_version < '3.12'", - "diagrams>=0.21; python_version < '3.12'", - "rich>=12.3; python_version < '3.12'", - "matplotlib>=3.5; python_version < '3.12'", - "numpy>=1.23; python_version < '3.12'", - "textual; python_version < '3.12'", - "pytermgui>=6.3; python_version < '3.12'", - "pipdeptree>=2.6; python_version < '3.12'", -] -maintain = [ - "black>=23.9", - "blacken-docs>=1.16", - "git-changelog>=2.3", -] -quality = [ - "ruff>=0.0", -] -tests = [ - "pytest>=7.4", - "pytest-cov>=4.1", - "pytest-randomly>=3.15", - "pytest-xdist>=3.3", - "pygments>=2.15", -] -typing = [ - "mypy>=1.5", - "types-markdown>=3.5", - "types-pyyaml>=6.0", -] -security = [ - "safety>=2.3", -] diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index bf35f0d..27f94d6 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 not line.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: 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"])), + "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: str(dep["name"])), "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/insiders.py b/scripts/insiders.py index 85ecbd0..1521248 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -78,9 +78,16 @@ def human_readable_amount(self) -> str: # noqa: D102 def render(self, rel_base: str = "..") -> None: # noqa: D102 print(f"#### $ {self.human_readable_amount} — {self.name}\n") - for feature in self.features: - feature.render(rel_base) - print("") + if self.features: + for feature in self.features: + feature.render(rel_base) + print("") + else: + print("There are no features in this goal for this project. ") + print( + "[See the features in this goal **for all Insiders projects.**]" + f"(https://pawamoy.github.io/insiders/#{self.amount}-{self.name.lower().replace(' ', '-')})", + ) def load_goals(data: str, funding: int = 0, project: Project | None = None) -> dict[int, Goal]: @@ -104,8 +111,7 @@ def load_goals(data: str, funding: int = 0, project: Project | None = None) -> d Feature( name=feature_data["name"], ref=feature_data.get("ref"), - since=feature_data.get("since") - and datetime.strptime(feature_data["since"], "%Y/%m/%d").date(), # noqa: DTZ007 + since=feature_data.get("since") and datetime.strptime(feature_data["since"], "%Y/%m/%d").date(), # noqa: DTZ007 project=project, ) for feature_data in goal_data["features"] diff --git a/scripts/make b/scripts/make new file mode 100755 index 0000000..8ea542b --- /dev/null +++ b/scripts/make @@ -0,0 +1,159 @@ +#!/usr/bin/env bash + +set -e +export PYTHON_VERSIONS=${PYTHON_VERSIONS-3.8 3.9 3.10 3.11 3.12} + +exe="" +prefix="" + + +# Install runtime and development dependencies, +# as well as current project in editable mode. +uv_install() { + uv pip compile pyproject.toml devdeps.txt | uv pip install -r - + if [ -z "${CI}" ]; then + uv pip install -e .[ansi] + else + uv pip install "markdown-exec @ .[ansi]" + fi +} + + +# Setup the development environment by installing dependencies +# in multiple Python virtual environments with uv: +# one venv per Python version in `.venvs/$py`, +# and an additional default venv in `.venv`. +setup() { + if ! command -v uv &>/dev/null; then + echo "make: setup: uv must be installed, see https://github.com/astral-sh/uv" >&2 + return 1 + fi + + if [ -n "${PYTHON_VERSIONS}" ]; then + for version in ${PYTHON_VERSIONS}; do + if [ ! -d ".venvs/${version}" ]; then + uv venv --python "${version}" ".venvs/${version}" + fi + VIRTUAL_ENV="${PWD}/.venvs/${version}" uv_install + done + fi + + if [ ! -d .venv ]; then uv venv --python python; fi + uv_install +} + + +# Activate a Python virtual environments. +# The annoying operating system also requires +# that we set some global variables to help it find commands... +activate() { + local path + if [ -f "$1/bin/activate" ]; then + source "$1/bin/activate" + return 0 + fi + if [ -f "$1/Scripts/activate.bat" ]; then + "$1/Scripts/activate.bat" + exe=".exe" + prefix="$1/Scripts/" + return 0 + fi + echo "run: Cannot activate venv $1" >&2 + return 1 +} + + +# Run a command in all configured Python virtual environments. +# We handle the case when the `PYTHON_VERSIONS` environment variable +# is unset or empty, for robustness. +multirun() { + local cmd="$1" + shift + + if [ -n "${PYTHON_VERSIONS}" ]; then + for version in ${PYTHON_VERSIONS}; do + (activate ".venvs/${version}" && MULTIRUN=1 "${prefix}${cmd}${exe}" "$@") + done + else + (activate .venv && "${prefix}${cmd}${exe}" "$@") + fi +} + + +# Run a command in the default Python virtual environment. +# We rely on `multirun`'s handling of empty `PYTHON_VERSIONS`. +singlerun() { + PYTHON_VERSIONS= multirun "$@" +} + + +# Record options following a command name, +# until a non-option argument is met or there are no more arguments. +# Output each option on a new line, so the parent caller can store them in an array. +# Return the number of times the parent caller must shift arguments. +options() { + local shift_count=0 + for arg in "$@"; do + if [[ "${arg}" =~ ^- || "${arg}" =~ ^.+= ]]; then + echo "${arg}" + ((shift_count++)) + else + break + fi + done + return ${shift_count} +} + + +# Main function. +main() { + local cmd + while [ $# -ne 0 ]; do + cmd="$1" + shift + + # Handle `run` early to simplify `case` below. + if [ "${cmd}" = "run" ]; then + singlerun "$@" + exit $? + fi + + # Handle `multirun` early to simplify `case` below. + if [ "${cmd}" = "multirun" ]; then + multirun "$@" + exit $? + fi + + # All commands except `run` and `multirun` can be chained on a single line. + # Some of them accept options in two formats: `-f`, `--flag` and `param=value`. + # Some of them don't, and will print warnings/errors if options were given. + opts=($(options "$@")) || shift $? + + case "${cmd}" in + # The following commands require special handling. + help|"") + singlerun duty --list ;; + setup) + setup ;; + check) + multirun duty check-quality check-types check-docs + singlerun duty check-dependencies check-api + ;; + + # The following commands run in all venvs. + check-quality|\ + check-docs|\ + check-types|\ + test) + multirun duty "${cmd}" "${opts[@]}" ;; + + # The following commands run in the default venv only. + *) + singlerun duty "${cmd}" "${opts[@]}" ;; + esac + done +} + + +# Execute the main function. +main "$@" diff --git a/scripts/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/markdown_exec/debug.py b/src/markdown_exec/debug.py index dad33dd..119c286 100644 --- a/src/markdown_exec/debug.py +++ b/src/markdown_exec/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}`")