From 3416deccb9c81ab161ec4d8613e1a490524a125e Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 00:43:00 +0200 Subject: [PATCH 01/26] Math is the language of the universe. From 8ebd6c9f71f4336af770bb73d637ca616904eb7f Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 00:45:24 +0200 Subject: [PATCH 02/26] Add open-source license MIT license because the project is for everybody --- LICENSE.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..274a887 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Alexander Hess [alexander@webartifex.biz] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From c7d8b7f283c2cc42b2c09d1aa12436b575616ef6 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 01:02:32 +0200 Subject: [PATCH 03/26] Add README file - describe the project's goals - contributions are welcome - mention the GitFlow branching model --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d659785 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# A Python library to study linear algebra + +The goal of the `lalib` project is to create + a library written in pure [Python](https://docs.python.org/3/) + (incl. the [standard library](https://docs.python.org/3/library/index.html)) + and thereby learn about + [linear algebra](https://en.wikipedia.org/wiki/Linear_algebra) + by reading and writing code. + + +## Contributing & Development + +This project is open for any kind of contribution, + be it by writing code for new features or bugfixes, + or by raising [issues](https://github.com/webartifex/lalib/issues). +All contributions become open-source themselves, under the + [MIT license](https://github.com/webartifex/lalib/blob/main/LICENSE.txt). + + +### Branching Strategy + +The branches in this repository follow the + [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) model. +Feature branches are rebased onto + the [develop](https://github.com/webartifex/lalib/tree/develop) branch + *before* being merged. +Whereas a rebase makes a simple fast-forward merge possible, + all merges are made with explicit and *empty* merge commits. +This ensures that past branches remain visible in the logs, + for example, with `git log --graph`. From e832333ed9f06ee0e15d30ec2d196385560b2397 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 01:15:36 +0200 Subject: [PATCH 04/26] Initialize the project - describe how a local develop environment can be set up - we use poetry to manage the project => add pyproject.toml and poetry.lock files - add a package for the source code => "src" layout structure to ensure that pytest runs the tests against a packaged version installed in a virtual environment and not the *.py files in the project directory (Source: https://hynek.me/articles/testing-packaging/) - ignore poetry's artifacts in git --- .gitignore | 1 + README.md | 28 ++++++++++++++++++++++++++ poetry.lock | 6 ++++++ pyproject.toml | 46 +++++++++++++++++++++++++++++++++++++++++++ src/lalib/__init__.py | 1 + 5 files changed, 82 insertions(+) create mode 100644 .gitignore create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 src/lalib/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21d0b89 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.venv/ diff --git a/README.md b/README.md index d659785..0252b7c 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,34 @@ All contributions become open-source themselves, under the [MIT license](https://github.com/webartifex/lalib/blob/main/LICENSE.txt). +### Local Develop Environment + +In order to play with the `lalib` codebase, + you need to set up a develop environment on your own computer. + +First, get your own copy of this repository: + +`git clone git@github.com:webartifex/lalib.git` + +While `lalib` comes without any dependencies + except core Python and the standard library for the user, + we assume a couple of packages and tools be installed + to ensure code quality during development. +These can be viewed in the + [pyproject.toml](https://github.com/webartifex/lalib/blob/main/pyproject.toml) file + and are managed with [poetry](https://python-poetry.org/docs/) + which needs to be installed as well. +`poetry` also creates and manages a + [virtual environment](https://docs.python.org/3/tutorial/venv.html) + with the develop tools, + and pins their exact installation versions in the + [poetry.lock](https://github.com/webartifex/lalib/blob/main/poetry.lock) file. + +To replicate the project maintainer's develop environment, run: + +`poetry install` + + ### Branching Strategy The branches in this repository follow the diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..2f7ba0f --- /dev/null +++ b/poetry.lock @@ -0,0 +1,6 @@ +package = [] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "0ef693f84017d54b0ea1d26b405d979fc57973167b307ccd9ed780ad9bdc1a3a" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b4894b6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[tool.poetry] + +name = "lalib" +version = "0.4.2.dev0" + +authors = [ + "Alexander Hess ", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +description = "A Python library to study linear algebra" +license = "MIT" +readme = "README.md" + +homepage = "https://github.com/webartifex/lalib" +repository = "https://github.com/webartifex/lalib" + + +[tool.poetry.dependencies] + +python = "^3.9" + + +[tool.poetry.group.dev.dependencies] + + + +[tool.poetry.urls] + +"Issues Tracker" = "https://github.com/webartifex/lalib/issues" + + + +[build-system] + +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/src/lalib/__init__.py b/src/lalib/__init__.py new file mode 100644 index 0000000..50db320 --- /dev/null +++ b/src/lalib/__init__.py @@ -0,0 +1 @@ +"""A Python library to study linear algebra.""" From ceabb00bab2b867a3612763d828f10ee70b3b4be Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 01:21:37 +0200 Subject: [PATCH 05/26] Set up nox as the task runner - base configuration for all nox sessions to come - add infos about nox in the README file - ignore [py]cache folders in git --- .gitignore | 2 ++ README.md | 20 ++++++++++++ noxfile.py | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 noxfile.py diff --git a/.gitignore b/.gitignore index 21d0b89..0687299 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ +.cache/ +**/__pycache__/ .venv/ diff --git a/README.md b/README.md index 0252b7c..3892c23 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,26 @@ To replicate the project maintainer's develop environment, run: `poetry install` +### Maintenance Tasks + +We use [nox](https://nox.thea.codes/en/stable/) to run + the test suite and other maintenance tasks during development + in isolated environments. +`nox` is similar to the popular [tox](https://tox.readthedocs.io/en/latest/). +It is configured in the + [noxfile.py](https://github.com/webartifex/lalib/blob/main/noxfile.py) file. +`nox` is assumed to be installed as well + and is therefore not a project dependency. + +To list all available tasks, called sessions in `nox`, simply run: + +`nox --list` or `nox -l` for short + +To execute all default tasks, simply invoke: + +`nox` + + ### Branching Strategy The branches in this repository follow the diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..64333e7 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,92 @@ +"""Maintenance tasks run in isolated environments.""" + +import collections +from collections.abc import Mapping +from typing import Any + +import nox +from packaging import version as pkg_version + + +try: + from nox_poetry import session as nox_session +except ImportError: + nox_session = nox.session + + +def nested_defaultdict() -> collections.defaultdict[str, Any]: + """Create a multi-level `defaultdict` with variable depth. + + The returned `dict`ionary never raises a `KeyError` + but always returns an empty `dict`ionary instead. + This behavior is occurs recursively. + + Adjusted from: https://stackoverflow.com/a/8702435 + """ + return collections.defaultdict(nested_defaultdict) + + +def defaultify(obj: Any) -> Any: + """Turn nested `dict`s into nested `defaultdict`s.""" + if isinstance(obj, Mapping): + return collections.defaultdict( + nested_defaultdict, + {key: defaultify(val) for key, val in obj.items()}, + ) + return obj + + +def load_pyproject_toml() -> collections.defaultdict[str, Any]: + """Load the contents of the pyproject.toml file. + + The contents are represented as a `nested_defaultdict`; + so, missing keys and tables (i.e., "sections" in the .ini format) + do not result in `KeyError`s but return empty `nested_defaultdict`s. + """ + return defaultify(nox.project.load_toml("pyproject.toml")) + + +def load_supported_python_versions(*, reverse: bool = False) -> list[str]: + """Parse the Python versions from the pyproject.toml file.""" + pyproject = load_pyproject_toml() + version_names = { + classifier.rsplit(" ")[-1] + for classifier in pyproject["tool"]["poetry"]["classifiers"] + if classifier.startswith("Programming Language :: Python :: ") + } + return sorted(version_names, key=pkg_version.Version, reverse=reverse) + + +SUPPORTED_PYTHONS = load_supported_python_versions(reverse=True) +MAIN_PYTHON = "3.12" + +SRC_LOCATIONS = ("./noxfile.py", "src/") + + +nox.options.envdir = ".cache/nox" +nox.options.error_on_external_run = True # only `git` and `poetry` are external +nox.options.reuse_venv = "no" +nox.options.sessions = ( # run by default when invoking `nox` on the CLI +) +nox.options.stop_on_first_error = True + + +def start(session: nox.Session) -> None: + """Show generic info about a session.""" + if session.posargs: + session.debug(f"Received extra arguments: {session.posargs}") + + session.debug("Some generic information about the environment") + session.run("python", "--version") + session.run("python", "-c", "import sys; print(sys.executable)") + session.run("python", "-c", "import sys; print(sys.path)") + session.run("python", "-c", "import os; print(os.getcwd())") + session.run("python", "-c", 'import os; print(os.environ["PATH"])') + + session.env["PIP_CACHE_DIR"] = ".cache/pip" + session.env["PIP_DISABLE_PIP_VERSION_CHECK"] = "true" + + +if MAIN_PYTHON not in SUPPORTED_PYTHONS: + msg = f"MAIN_PYTHON version, v{MAIN_PYTHON}, is not in SUPPORTED_PYTHONS" + raise RuntimeError(msg) From fb407631d996bb7c60062bbbc2ac59efdd3b2902 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 01:27:34 +0200 Subject: [PATCH 06/26] Set up code formatting tools - auto-format code with: + autoflake => * remove unused imports and variables * remove duplicate dict keys * expand star imports + black => enforce an uncompromising code style + isort => enforce a consistent import style compliant with Google's Python style guide - add nox session "format" to run these tools --- .gitignore | 1 + README.md | 18 +++++ noxfile.py | 79 +++++++++++++++++++++ poetry.lock | 184 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 55 +++++++++++++++ 5 files changed, 335 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 0687299..13eb353 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .cache/ +poetry.toml **/__pycache__/ .venv/ diff --git a/README.md b/README.md index 3892c23..12abf56 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ The goal of the `lalib` project is to create by reading and writing code. +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + + ## Contributing & Development This project is open for any kind of contribution, @@ -65,6 +68,21 @@ To execute all default tasks, simply invoke: `nox` +#### Code Formatting + +We follow [Google's Python style guide](https://google.github.io/styleguide/pyguide.html). + +During development, + `nox -s format` may be helpful. +It can be speed up by re-using a previously created environment + with the `-R` flag. + +This task formats all source code files with + [autoflake](https://pypi.org/project/autoflake/), + [black](https://pypi.org/project/black/), and + [isort](https://pypi.org/project/isort/). + + ### Branching Strategy The branches in this repository follow the diff --git a/noxfile.py b/noxfile.py index 64333e7..087f97a 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,6 +1,9 @@ """Maintenance tasks run in isolated environments.""" import collections +import pathlib +import re +import tempfile from collections.abc import Mapping from typing import Any @@ -12,6 +15,9 @@ try: from nox_poetry import session as nox_session except ImportError: nox_session = nox.session + nox_poetry_available = False +else: + nox_poetry_available = True def nested_defaultdict() -> collections.defaultdict[str, Any]: @@ -67,10 +73,30 @@ nox.options.envdir = ".cache/nox" nox.options.error_on_external_run = True # only `git` and `poetry` are external nox.options.reuse_venv = "no" nox.options.sessions = ( # run by default when invoking `nox` on the CLI + "format", ) nox.options.stop_on_first_error = True +@nox_session(name="format", python=MAIN_PYTHON) +def format_(session: nox.Session) -> None: + """Format source files with `autoflake`, `black`, and `isort`.""" + start(session) + + install_pinned(session, "autoflake", "black", "isort") + + locations = session.posargs or SRC_LOCATIONS + + session.run("autoflake", "--version") + session.run("autoflake", *locations) + + session.run("black", "--version") + session.run("black", *locations) + + session.run("isort", "--version-number") + session.run("isort", *locations) + + def start(session: nox.Session) -> None: """Show generic info about a session.""" if session.posargs: @@ -83,10 +109,63 @@ def start(session: nox.Session) -> None: session.run("python", "-c", "import os; print(os.getcwd())") session.run("python", "-c", 'import os; print(os.environ["PATH"])') + session.env["BLACK_CACHE_DIR"] = ".cache/black" session.env["PIP_CACHE_DIR"] = ".cache/pip" session.env["PIP_DISABLE_PIP_VERSION_CHECK"] = "true" +def install_pinned( + session: nox.Session, + *packages_or_pip_args: str, + **kwargs: Any, +) -> None: + """Install packages respecting the "poetry.lock" file. + + Wraps `nox.sessions.Session.install()` such that it installs + packages respecting the pinned versions specified in poetry's + lock file. This makes nox sessions more deterministic. + """ + session.debug("Install packages respecting the poetry.lock file") + + session.run( # temporary fix to avoid poetry's future warning + "poetry", + "config", + "--local", + "warnings.export", + "false", + external=True, + log=False, # because it's just a fix + ) + + if nox_poetry_available: + session.install(*packages_or_pip_args, **kwargs) + return + + with tempfile.NamedTemporaryFile() as requirements_txt: + session.run( + "poetry", + "export", + "--format=requirements.txt", + f"--output={requirements_txt.name}", + "--with=dev", + "--without-hashes", + external=True, + ) + + # `pip install --constraint ...` raises an error if the + # dependencies in requirements.txt contain "extras" + # => Strip "package[extras]==1.2.3" into "package==1.2.3" + dependencies = pathlib.Path(requirements_txt.name).read_text().split("\n") + dependencies = [re.sub(r"\[.*\]==", "==", dep) for dep in dependencies] + pathlib.Path(requirements_txt.name).write_text("\n".join(dependencies)) + + session.install( + f"--constraint={requirements_txt.name}", + *packages_or_pip_args, + **kwargs, + ) + + if MAIN_PYTHON not in SUPPORTED_PYTHONS: msg = f"MAIN_PYTHON version, v{MAIN_PYTHON}, is not in SUPPORTED_PYTHONS" raise RuntimeError(msg) diff --git a/poetry.lock b/poetry.lock index 2f7ba0f..a5994bb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,186 @@ -package = [] +[[package]] +name = "autoflake" +version = "2.3.1" +description = "Removes unused imports and unused variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840"}, + {file = "autoflake-2.3.1.tar.gz", hash = "sha256:c98b75dc5b0a86459c4f01a1d32ac7eb4338ec4317a4469515ff1e687ecd909e"}, +] + +[package.dependencies] +pyflakes = ">=3.0.0" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[[package]] +name = "black" +version = "24.8.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"}, + {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "0ef693f84017d54b0ea1d26b405d979fc57973167b307ccd9ed780ad9bdc1a3a" +content-hash = "01960a0fd26ee6cb565391f829bae290abbf990c15496a4c93b323b0253a09fa" diff --git a/pyproject.toml b/pyproject.toml index b4894b6..4684745 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,10 @@ python = "^3.9" [tool.poetry.group.dev.dependencies] +# Code formatters +autoflake = "^2.3" +black = "^24.8" +isort = "^5.13" [tool.poetry.urls] @@ -40,6 +44,57 @@ python = "^3.9" +[tool.autoflake] +# Source: https://github.com/PyCQA/autoflake#configuration + +in-place = true +recursive = true +expand-star-imports = true +remove-all-unused-imports = true +ignore-init-module-imports = true # modifies "remove-all-unused-imports" +remove-duplicate-keys = true +remove-unused-variables = true + + + +[tool.black] +# Source: https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html + +line-length = 88 +target-version = ["py312", "py311", "py310", "py39"] + + + +[tool.isort] +# Source: https://pycqa.github.io/isort/docs/configuration/options.html + +known_first_party = ["lalib"] + +atomic = true +case_sensitive = true +combine_star = true +force_alphabetical_sort_within_sections = true +lines_after_imports = 2 +remove_redundant_aliases = true + +# Comply with black's style => Instead of: 'profile = "black"' +# Source: https://pycqa.github.io/isort/docs/configuration/profiles.html +ensure_newline_before_comments = true +force_grid_wrap = 0 +include_trailing_comma = true +line_length = 88 +multi_line_output = 3 +split_on_trailing_comma = true +use_parentheses = true + +# Comply with Google's Python style guide +# => All imports go on a single line (with some exceptions) +# Source: https://google.github.io/styleguide/pyguide.html#313-imports-formatting +force_single_line = true +single_line_exclusions = ["collections.abc", "typing"] + + + [build-system] requires = ["poetry-core"] From ecf142074294f6aeee1526f3afac9e0d5b96c5b2 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 01:33:54 +0200 Subject: [PATCH 07/26] Set up code linting tools - use flake8 as the main linting tool with the following plug-ins: + flake8-annotations + flake8-bandit + flake8-black + flake8-broken-line + flake8-bugbear + flake8-commas + flake8-comprehensions + flake8-debugger + flake8-docstrings + flake8-eradicate + flake8-isort + flake8-quotes + flake8-string-format + flake8-pyproject + pep8-naming + pydoclint - use mypy for static type checking - use ruff for linting for future compatibility - add nox session "lint" to run these tools - add `ruff check --fix ...` in nox session "format" - lint all source files => no errors found --- README.md | 20 +- noxfile.py | 46 +++- poetry.lock | 633 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 208 +++++++++++++++- 4 files changed, 899 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 12abf56..7916164 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ The goal of the `lalib` project is to create [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Type checking: mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) +[![Code linting: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) ## Contributing & Development @@ -68,20 +70,28 @@ To execute all default tasks, simply invoke: `nox` -#### Code Formatting +#### Code Formatting & Linting -We follow [Google's Python style guide](https://google.github.io/styleguide/pyguide.html). +We follow [Google's Python style guide](https://google.github.io/styleguide/pyguide.html) + and include [type hints](https://docs.python.org/3/library/typing.html) + where possible. During development, - `nox -s format` may be helpful. -It can be speed up by re-using a previously created environment + `nox -s format` and `nox -s lint` may be helpful. +Both can be speed up by re-using a previously created environment with the `-R` flag. -This task formats all source code files with +The first task formats all source code files with [autoflake](https://pypi.org/project/autoflake/), [black](https://pypi.org/project/black/), and [isort](https://pypi.org/project/isort/). +The second task lints all source code files with + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), and + [ruff](https://pypi.org/project/ruff/). +`flake8` is configured with a couple of plug-ins. + ### Branching Strategy diff --git a/noxfile.py b/noxfile.py index 087f97a..3e281e7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -74,6 +74,7 @@ nox.options.error_on_external_run = True # only `git` and `poetry` are external nox.options.reuse_venv = "no" nox.options.sessions = ( # run by default when invoking `nox` on the CLI "format", + "lint", ) nox.options.stop_on_first_error = True @@ -83,7 +84,7 @@ def format_(session: nox.Session) -> None: """Format source files with `autoflake`, `black`, and `isort`.""" start(session) - install_pinned(session, "autoflake", "black", "isort") + install_pinned(session, "autoflake", "black", "isort", "ruff") locations = session.posargs or SRC_LOCATIONS @@ -96,6 +97,49 @@ def format_(session: nox.Session) -> None: session.run("isort", "--version-number") session.run("isort", *locations) + session.run("ruff", "--version") + session.run("ruff", "check", "--fix-only", *locations) + + +@nox_session(python=MAIN_PYTHON) +def lint(session: nox.Session) -> None: + """Lint source files with `flake8`, `mypy`, and `ruff`.""" + start(session) + + install_pinned( + session, + "flake8", + "flake8-annotations", + "flake8-bandit", + "flake8-black", + "flake8-broken-line", + "flake8-bugbear", + "flake8-commas", + "flake8-comprehensions", + "flake8-debugger", + "flake8-docstrings", + "flake8-eradicate", + "flake8-isort", + "flake8-quotes", + "flake8-string-format", + "flake8-pyproject", + "mypy", + "pep8-naming", # flake8 plug-in + "pydoclint[flake8]", + "ruff", + ) + + locations = session.posargs or SRC_LOCATIONS + + session.run("flake8", "--version") + session.run("flake8", *locations) + + session.run("mypy", "--version") + session.run("mypy", *locations) + + session.run("ruff", "--version") + session.run("ruff", "check", *locations) + def start(session: nox.Session) -> None: """Show generic info about a session.""" diff --git a/poetry.lock b/poetry.lock index a5994bb..85cb937 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,22 @@ +[[package]] +name = "attrs" +version = "24.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + [[package]] name = "autoflake" version = "2.3.1" @@ -13,6 +32,30 @@ files = [ pyflakes = ">=3.0.0" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} +[[package]] +name = "bandit" +version = "1.7.9" +description = "Security oriented static analyser for python code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, + {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +PyYAML = ">=5.3.1" +rich = "*" +stevedore = ">=1.20.0" + +[package.extras] +baseline = ["GitPython (>=3.1.30)"] +sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] +test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] +toml = ["tomli (>=1.1.0)"] +yaml = ["PyYAML"] + [[package]] name = "black" version = "24.8.0" @@ -84,6 +127,262 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "docstring-parser-fork" +version = "0.0.9" +description = "Parse Python docstrings in reST, Google and Numpydoc format" +optional = false +python-versions = "<4.0,>=3.7" +files = [ + {file = "docstring_parser_fork-0.0.9-py3-none-any.whl", hash = "sha256:0be85ad00cb25bf5beeb673e46e777facf0f47552fa3a7570d120ef7e3374401"}, + {file = "docstring_parser_fork-0.0.9.tar.gz", hash = "sha256:95b23cc5092af85080c716a6da68360f5ae4fcffa75f4a3aca5e539783cbcc3d"}, +] + +[[package]] +name = "eradicate" +version = "2.3.0" +description = "Removes commented-out code." +optional = false +python-versions = "*" +files = [ + {file = "eradicate-2.3.0-py3-none-any.whl", hash = "sha256:2b29b3dd27171f209e4ddd8204b70c02f0682ae95eecb353f10e8d72b149c63e"}, + {file = "eradicate-2.3.0.tar.gz", hash = "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37"}, +] + +[[package]] +name = "flake8" +version = "7.1.1" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, + {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.12.0,<2.13.0" +pyflakes = ">=3.2.0,<3.3.0" + +[[package]] +name = "flake8-annotations" +version = "3.1.1" +description = "Flake8 Type Annotation Checks" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8_annotations-3.1.1-py3-none-any.whl", hash = "sha256:102935bdcbfa714759a152aeb07b14aee343fc0b6f7c55ad16968ce3e0e91a8a"}, + {file = "flake8_annotations-3.1.1.tar.gz", hash = "sha256:6c98968ccc6bdc0581d363bf147a87df2f01d0d078264b2da805799d911cf5fe"}, +] + +[package.dependencies] +attrs = ">=21.4" +flake8 = ">=5.0" + +[[package]] +name = "flake8-bandit" +version = "4.1.1" +description = "Automated security testing with bandit and flake8." +optional = false +python-versions = ">=3.6" +files = [ + {file = "flake8_bandit-4.1.1-py3-none-any.whl", hash = "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d"}, + {file = "flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e"}, +] + +[package.dependencies] +bandit = ">=1.7.3" +flake8 = ">=5.0.0" + +[[package]] +name = "flake8-black" +version = "0.3.6" +description = "flake8 plugin to call black as a code style validator" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8-black-0.3.6.tar.gz", hash = "sha256:0dfbca3274777792a5bcb2af887a4cad72c72d0e86c94e08e3a3de151bb41c34"}, + {file = "flake8_black-0.3.6-py3-none-any.whl", hash = "sha256:fe8ea2eca98d8a504f22040d9117347f6b367458366952862ac3586e7d4eeaca"}, +] + +[package.dependencies] +black = ">=22.1.0" +flake8 = ">=3" +tomli = {version = "*", markers = "python_version < \"3.11\""} + +[package.extras] +develop = ["build", "twine"] + +[[package]] +name = "flake8-broken-line" +version = "1.0.0" +description = "Flake8 plugin to forbid backslashes for line breaks" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "flake8_broken_line-1.0.0-py3-none-any.whl", hash = "sha256:96c964336024a5030dc536a9f6fb02aa679e2d2a6b35b80a558b5136c35832a9"}, + {file = "flake8_broken_line-1.0.0.tar.gz", hash = "sha256:e2c6a17f8d9a129e99c1320fce89b33843e2963871025c4c2bb7b8b8d8732a85"}, +] + +[package.dependencies] +flake8 = ">5" + +[[package]] +name = "flake8-bugbear" +version = "24.8.19" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8_bugbear-24.8.19-py3-none-any.whl", hash = "sha256:25bc3867f7338ee3b3e0916bf8b8a0b743f53a9a5175782ddc4325ed4f386b89"}, + {file = "flake8_bugbear-24.8.19.tar.gz", hash = "sha256:9b77627eceda28c51c27af94560a72b5b2c97c016651bdce45d8f56c180d2d32"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=6.0.0" + +[package.extras] +dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"] + +[[package]] +name = "flake8-commas" +version = "4.0.0" +description = "Flake8 lint for trailing commas." +optional = false +python-versions = ">=3.8" +files = [ + {file = "flake8_commas-4.0.0-py3-none-any.whl", hash = "sha256:cad476d71ba72e8b941a8508d5b9ffb6b03e50f7102982474f085ad0d674b685"}, + {file = "flake8_commas-4.0.0.tar.gz", hash = "sha256:a68834b42a9a31c94ca790efe557a932c0eae21a3479c6b9a23c4dc077e3ea96"}, +] + +[package.dependencies] +flake8 = ">=5" + +[[package]] +name = "flake8-comprehensions" +version = "3.15.0" +description = "A flake8 plugin to help you write better list/set/dict comprehensions." +optional = false +python-versions = ">=3.8" +files = [ + {file = "flake8_comprehensions-3.15.0-py3-none-any.whl", hash = "sha256:b7e027bbb52be2ceb779ee12484cdeef52b0ad3c1fcb8846292bdb86d3034681"}, + {file = "flake8_comprehensions-3.15.0.tar.gz", hash = "sha256:923c22603e0310376a6b55b03efebdc09753c69f2d977755cba8bb73458a5d4d"}, +] + +[package.dependencies] +flake8 = ">=3,<3.2 || >3.2" + +[[package]] +name = "flake8-debugger" +version = "4.1.2" +description = "ipdb/pdb statement checker plugin for flake8" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8-debugger-4.1.2.tar.gz", hash = "sha256:52b002560941e36d9bf806fca2523dc7fb8560a295d5f1a6e15ac2ded7a73840"}, + {file = "flake8_debugger-4.1.2-py3-none-any.whl", hash = "sha256:0a5e55aeddcc81da631ad9c8c366e7318998f83ff00985a49e6b3ecf61e571bf"}, +] + +[package.dependencies] +flake8 = ">=3.0" +pycodestyle = "*" + +[[package]] +name = "flake8-docstrings" +version = "1.7.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, + {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, +] + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + +[[package]] +name = "flake8-eradicate" +version = "1.5.0" +description = "Flake8 plugin to find commented out code" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "flake8_eradicate-1.5.0-py3-none-any.whl", hash = "sha256:18acc922ad7de623f5247c7d5595da068525ec5437dd53b22ec2259b96ce9d22"}, + {file = "flake8_eradicate-1.5.0.tar.gz", hash = "sha256:aee636cb9ecb5594a7cd92d67ad73eb69909e5cc7bd81710cf9d00970f3983a6"}, +] + +[package.dependencies] +attrs = "*" +eradicate = ">=2.0,<3.0" +flake8 = ">5" + +[[package]] +name = "flake8-isort" +version = "6.1.1" +description = "flake8 plugin that integrates isort" +optional = false +python-versions = ">=3.8" +files = [ + {file = "flake8_isort-6.1.1-py3-none-any.whl", hash = "sha256:0fec4dc3a15aefbdbe4012e51d5531a2eb5fa8b981cdfbc882296a59b54ede12"}, + {file = "flake8_isort-6.1.1.tar.gz", hash = "sha256:c1f82f3cf06a80c13e1d09bfae460e9666255d5c780b859f19f8318d420370b3"}, +] + +[package.dependencies] +flake8 = "*" +isort = ">=5.0.0,<6" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "flake8-pyproject" +version = "1.2.3" +description = "Flake8 plug-in loading the configuration from pyproject.toml" +optional = false +python-versions = ">= 3.6" +files = [ + {file = "flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a"}, +] + +[package.dependencies] +Flake8 = ">=5" +TOMLi = {version = "*", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["pyTest", "pyTest-cov"] + +[[package]] +name = "flake8-quotes" +version = "3.4.0" +description = "Flake8 lint for quotes." +optional = false +python-versions = "*" +files = [ + {file = "flake8-quotes-3.4.0.tar.gz", hash = "sha256:aad8492fb710a2d3eabe68c5f86a1428de650c8484127e14c43d0504ba30276c"}, +] + +[package.dependencies] +flake8 = "*" +setuptools = "*" + +[[package]] +name = "flake8-string-format" +version = "0.3.0" +description = "string format checker, plugin for flake8" +optional = false +python-versions = "*" +files = [ + {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"}, + {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, +] + +[package.dependencies] +flake8 = "*" + [[package]] name = "isort" version = "5.13.2" @@ -98,6 +397,99 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mypy" +version = "1.11.2" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, + {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, + {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, + {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, + {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, + {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, + {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, + {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, + {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, + {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, + {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, + {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, + {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, + {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, + {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, + {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, + {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, + {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -131,6 +523,31 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "pbr" +version = "6.1.0" +description = "Python Build Reasonableness" +optional = false +python-versions = ">=2.6" +files = [ + {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, + {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, +] + +[[package]] +name = "pep8-naming" +version = "0.14.1" +description = "Check PEP-8 naming conventions, plugin for flake8" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pep8-naming-0.14.1.tar.gz", hash = "sha256:1ef228ae80875557eb6c1549deafed4dabbf3261cfcafa12f773fe0db9be8a36"}, + {file = "pep8_naming-0.14.1-py3-none-any.whl", hash = "sha256:63f514fc777d715f935faf185dedd679ab99526a7f2f503abb61587877f7b1c5"}, +] + +[package.dependencies] +flake8 = ">=5.0.0" + [[package]] name = "platformdirs" version = "4.3.2" @@ -147,6 +564,54 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-a test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.11.2)"] +[[package]] +name = "pycodestyle" +version = "2.12.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, +] + +[[package]] +name = "pydoclint" +version = "0.5.7" +description = "A Python docstring linter that checks arguments, returns, yields, and raises sections" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydoclint-0.5.7-py2.py3-none-any.whl", hash = "sha256:22561b8f876b7bd3d4966a3b7fe27bfdbfa41fa50d0719f13b08844a39e2717c"}, + {file = "pydoclint-0.5.7.tar.gz", hash = "sha256:d2938efc95233205822bebe5da5c8e1c4796da09988a2a73c2d6b63cef4577a6"}, +] + +[package.dependencies] +click = ">=8.1.0" +docstring-parser-fork = ">=0.0.9" +flake8 = {version = ">=4", optional = true, markers = "extra == \"flake8\""} +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +flake8 = ["flake8 (>=4)"] + +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] + +[package.dependencies] +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3)"] + [[package]] name = "pyflakes" version = "3.2.0" @@ -158,6 +623,172 @@ files = [ {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, ] +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "rich" +version = "13.8.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"}, + {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "ruff" +version = "0.6.4" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.6.4-py3-none-linux_armv6l.whl", hash = "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258"}, + {file = "ruff-0.6.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60"}, + {file = "ruff-0.6.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa"}, + {file = "ruff-0.6.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6"}, + {file = "ruff-0.6.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d"}, + {file = "ruff-0.6.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa"}, + {file = "ruff-0.6.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1"}, + {file = "ruff-0.6.4-py3-none-win32.whl", hash = "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523"}, + {file = "ruff-0.6.4-py3-none-win_amd64.whl", hash = "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58"}, + {file = "ruff-0.6.4-py3-none-win_arm64.whl", hash = "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14"}, + {file = "ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212"}, +] + +[[package]] +name = "setuptools" +version = "74.1.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-74.1.2-py3-none-any.whl", hash = "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308"}, + {file = "setuptools-74.1.2.tar.gz", hash = "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "stevedore" +version = "5.3.0" +description = "Manage dynamic plugins for Python applications" +optional = false +python-versions = ">=3.8" +files = [ + {file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"}, + {file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"}, +] + +[package.dependencies] +pbr = ">=2.0.0" + [[package]] name = "tomli" version = "2.0.1" @@ -183,4 +814,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "01960a0fd26ee6cb565391f829bae290abbf990c15496a4c93b323b0253a09fa" +content-hash = "ba133486945806a456cac2a2e5ba54a79b772ef1257b0a5f3b39545063c6e229" diff --git a/pyproject.toml b/pyproject.toml index 4684745..8dbfd7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,27 @@ autoflake = "^2.3" black = "^24.8" isort = "^5.13" +# Code linters +flake8 = "^7.1" +flake8-annotations = "^3.1" +flake8-bandit = "^4.1" +flake8-black = "^0.3" +flake8-broken-line = "^1.0" +flake8-bugbear = "^24.8" +flake8-commas = "^4.0" +flake8-comprehensions = "^3.15" +flake8-debugger = "^4.1" +flake8-docstrings = "^1.7" +flake8-eradicate = "^1.5" +flake8-isort = "^6.1" +flake8-quotes = "^3.4" +flake8-string-format = "^0.3" +flake8-pyproject = "^1.2" +mypy = "^1.11" +pep8-naming = "^0.14" # flake8 plug-in +pydoclint = { extras = ["flake8"], version = "^0.5" } +ruff = "^0.6" + [tool.poetry.urls] @@ -65,7 +86,84 @@ target-version = ["py312", "py311", "py310", "py39"] -[tool.isort] +[tool.flake8] + +select = [ + + # violations also covered by `ruff` below + + "ANN", # flake8-annotations => enforce type checking for functions + "B", # flake8-bugbear => bugs and design flaws + "C4", # flake8-comprehensions => better comprehensions + "C8", # flake8-commas => better comma placements ("COM" for `ruff`) + "C90", # mccabe => cyclomatic complexity (Source: https://github.com/pycqa/mccabe#plugin-for-flake8) + "D", # flake8-docstrings / pydocstyle => PEP257 compliance + "E", "W", # pycodestyle => PEP8 compliance (Source: https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes) + "E800", # flake8-eradicate / eradicate => no commented out code ("ERA" for `ruff`) + "F", # pyflakes => basic errors (Source: https://flake8.pycqa.org/en/latest/user/error-codes.html) + "I", # flake8-isort => isort would make changes + "N", # pep8-naming + "Q", # flake8-quotes => use double quotes everywhere (complying with black) + "S", # flake8-bandit => common security issues + "T10", # flake8-debugger => no debugger usage + + # violations not covered by `ruff` below + + "BLK", # flake8-black => complain if black wants to make changes + "DOC", # pydoclint (replaces "darglint") => docstring matches implementation + "N400", # flake8-broken-line => no "\" to end a line + "P", # flake8-string-format => unify usage of `str.format()` ("FMT" in the future) + +] + +ignore = [] + +extend-ignore = [ # never check the following codes + + "ANN101", "ANN102", # `self` and `cls` in methods need no annotation + + "ANN401", # allow dynamically typed expressions with `typing.Any` + + # Comply with black's style + # Sources: https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#pycodestyle + "E203", "E701", "E704", "W503", + +] + +per-file-ignores = [ + +] + +# Explicitly set mccabe's maximum complexity to 10 as recommended by +# Thomas McCabe, the inventor of the McCabe complexity, and the NIST +# Source: https://en.wikipedia.org/wiki/Cyclomatic_complexity#Limiting_complexity_during_development +max-complexity = 10 + +# Whereas black and isort break the line at 88 characters, +# make flake8 not complain about anything (e.g., comments) until 100 +max-line-length = 99 + +# Preview the code lines that cause errors +show-source = true + +# Plug-in: flake8-docstrings +# Source: https://www.pydocstyle.org/en/latest/error_codes.html#default-conventions +docstring-convention = "google" + +# Plug-in: flake8-eradicate +# Source: https://github.com/wemake-services/flake8-eradicate#options +eradicate-aggressive = true + +# Plug-in: flake8-quotes +# Source: https://github.com/zheller/flake8-quotes#configuration +avoid-escape = true +docstring-quotes = "double" +inline-quotes = "double" +multiline-quotes = "double" + + + +[tool.isort] # aligned with [tool.ruff.lint.isort] below # Source: https://pycqa.github.io/isort/docs/configuration/options.html known_first_party = ["lalib"] @@ -95,6 +193,114 @@ single_line_exclusions = ["collections.abc", "typing"] +[tool.mypy] +# Source: https://mypy.readthedocs.io/en/latest/config_file.html + +cache_dir = ".cache/mypy" + + +[[tool.mypy.overrides]] + +module = [ + "nox", + "nox_poetry", +] +ignore_missing_imports = true + + + +[tool.ruff] +# Source: https://docs.astral.sh/ruff/ + +cache-dir = ".cache/ruff" + +target-version = "py39" # minimum supported Python version + +indent-width = 4 +line-length = 88 + + +[tool.ruff.format] + +# Align with black +indent-style = "space" +line-ending = "lf" +quote-style = "double" +skip-magic-trailing-comma = false + +# Format docstrings as well +docstring-code-format = true +docstring-code-line-length = "dynamic" + + +[tool.ruff.lint] # aligned with [tool.flake8] above + +select = [ + + # violations also covered by `flake8` above + + "ANN", # flake8-annotations => enforce type checking for functions + "B", # flake8-bugbear => bugs and design flaws + "C4", # flake8-comprehensions => better comprehensions + "C90", # mccabe => cyclomatic complexity + "COM", # "C8" for flake8-commas => better comma placements + "D", # flake8-docstrings / pydocstyle => PEP257 compliance + "E", "W", # pycodestyle => PEP8 compliance + "ERA", # "E800" for flake8-eradicate / eradicate => no commented out code + "F", # pyflakes => basic errors + "I", # flake8-isort => isort would make changes + "N", # pep8-naming + "Q", # flake8-quotes => use double quotes everywhere + "S", # flake8-bandit => common security issues + "T10", # flake8-debugger => no debugger usage + + # violations not covered by `flake8` above + + "T20", # flake8-print => forbid `[p]print` + +] + +ignore = [] + +extend-ignore = [ # never check the following codes + + "ANN101", "ANN102", # `self` and `cls` in methods need no annotation + + "ANN401", # allow dynamically typed expressions with `typing.Any` + + # Comply with black's style + # Sources: https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#pycodestyle + "E203", "E701", # "E704" and "W503" do not exist for `ruff` + +] + + +[tool.ruff.lint.isort] # aligned with [tool.isort] above + +case-sensitive = true +force-single-line = true +single-line-exclusions = ["collections.abc", "typing"] +lines-after-imports = 2 +split-on-trailing-comma = true +known-first-party = ["lalib"] + + +[tool.ruff.lint.per-file-ignores] + + + +[tool.ruff.lint.pycodestyle] + +max-doc-length = 72 +max-line-length = 99 + + +[tool.ruff.lint.pydocstyle] + +convention = "google" + + + [build-system] requires = ["poetry-core"] From b8ceee39c51c469e4f21d24cbcdf69524c39036a Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 01:38:26 +0200 Subject: [PATCH 08/26] Set up a test suite - use pytest as the test suite and measure test coverage with coverage.py - add package for the test suite under tests/ - add nox session "test" to run the test suite for all supported Python versions - use flake8 to lint pytest for consistent style --- README.md | 15 ++++ noxfile.py | 55 ++++++++++++- poetry.lock | 194 +++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 75 ++++++++++++++++++ tests/__init__.py | 1 + 5 files changed, 338 insertions(+), 2 deletions(-) create mode 100644 tests/__init__.py diff --git a/README.md b/README.md index 7916164..513f168 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,9 @@ To execute all default tasks, simply invoke: `nox` +This includes running the test suite for the project's main Python version + (i.e., [3.12](https://devguide.python.org/versions/)). + #### Code Formatting & Linting @@ -93,6 +96,18 @@ The second task lints all source code files with `flake8` is configured with a couple of plug-ins. +#### Test Suite + +We use [pytest](https://docs.pytest.org/en/stable/) + to obtain confidence in the correctness of `lalib`. +To run the tests + for *all* supported Python versions + in isolated (and perfectly reproducable) environments, + invoke: + +`nox -s test` + + ### Branching Strategy The branches in this repository follow the diff --git a/noxfile.py b/noxfile.py index 3e281e7..26e5f36 100644 --- a/noxfile.py +++ b/noxfile.py @@ -66,7 +66,8 @@ def load_supported_python_versions(*, reverse: bool = False) -> list[str]: SUPPORTED_PYTHONS = load_supported_python_versions(reverse=True) MAIN_PYTHON = "3.12" -SRC_LOCATIONS = ("./noxfile.py", "src/") +TESTS_LOCATION = "tests/" +SRC_LOCATIONS = ("./noxfile.py", "src/", TESTS_LOCATION) nox.options.envdir = ".cache/nox" @@ -75,6 +76,7 @@ nox.options.reuse_venv = "no" nox.options.sessions = ( # run by default when invoking `nox` on the CLI "format", "lint", + f"test-{MAIN_PYTHON}", ) nox.options.stop_on_first_error = True @@ -123,6 +125,7 @@ def lint(session: nox.Session) -> None: "flake8-quotes", "flake8-string-format", "flake8-pyproject", + "flake8-pytest-style", "mypy", "pep8-naming", # flake8 plug-in "pydoclint[flake8]", @@ -141,6 +144,28 @@ def lint(session: nox.Session) -> None: session.run("ruff", "check", *locations) +@nox_session(python=SUPPORTED_PYTHONS) +def test(session: nox.Session) -> None: + """Test code with `pytest`.""" + start(session) + + install_unpinned(session, "-e", ".") # "-e" makes session reuseable + install_pinned( + session, + "pytest", + "pytest-cov", + ) + + args = session.posargs or ( + "--cov", + "--no-cov-on-fail", + TESTS_LOCATION, + ) + + # Code 5 is temporary as long as there are no tests + session.run("pytest", *args, success_codes=[0, 5]) + + def start(session: nox.Session) -> None: """Show generic info about a session.""" if session.posargs: @@ -210,6 +235,34 @@ def install_pinned( ) +def install_unpinned( + session: nox.Session, + *packages_or_pip_args: str, + **kwargs: Any, +) -> None: + """Install the latest PyPI versions of packages.""" + # Same logic to skip package installation as in core nox + # See: https://github.com/wntrblm/nox/blob/2024.04.15/nox/sessions.py#L775 + venv = session._runner.venv # noqa: SLF001 + if session._runner.global_config.no_install and venv._reused: # noqa: SLF001 + return + + if kwargs.get("silent") is None: + kwargs["silent"] = True + + # Cannot use `session.install(...)` here because + # with "nox-poetry" installed this leads to an + # installation respecting the "poetry.lock" file + session.run( + "python", + "-m", + "pip", + "install", + *packages_or_pip_args, + **kwargs, + ) + + if MAIN_PYTHON not in SUPPORTED_PYTHONS: msg = f"MAIN_PYTHON version, v{MAIN_PYTHON}, is not in SUPPORTED_PYTHONS" raise RuntimeError(msg) diff --git a/poetry.lock b/poetry.lock index 85cb937..fc9d3af 100644 --- a/poetry.lock +++ b/poetry.lock @@ -127,6 +127,93 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.6.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "docstring-parser-fork" version = "0.0.9" @@ -149,6 +236,20 @@ files = [ {file = "eradicate-2.3.0.tar.gz", hash = "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37"}, ] +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "flake8" version = "7.1.1" @@ -338,6 +439,17 @@ isort = ">=5.0.0,<6" [package.extras] test = ["pytest"] +[[package]] +name = "flake8-plugin-utils" +version = "1.3.3" +description = "The package provides base classes and utils for flake8 plugin writing" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "flake8-plugin-utils-1.3.3.tar.gz", hash = "sha256:39f6f338d038b301c6fd344b06f2e81e382b68fa03c0560dff0d9b1791a11a2c"}, + {file = "flake8_plugin_utils-1.3.3-py3-none-any.whl", hash = "sha256:e4848c57d9d50f19100c2d75fa794b72df068666a9041b4b0409be923356a3ed"}, +] + [[package]] name = "flake8-pyproject" version = "1.2.3" @@ -355,6 +467,20 @@ TOMLi = {version = "*", markers = "python_version < \"3.11\""} [package.extras] dev = ["pyTest", "pyTest-cov"] +[[package]] +name = "flake8-pytest-style" +version = "2.0.0" +description = "A flake8 plugin checking common style issues or inconsistencies with pytest-based tests." +optional = false +python-versions = "<4.0.0,>=3.8.1" +files = [ + {file = "flake8_pytest_style-2.0.0-py3-none-any.whl", hash = "sha256:abcb9f56f277954014b749e5a0937fae215be01a21852e9d05e7600c3de6aae5"}, + {file = "flake8_pytest_style-2.0.0.tar.gz", hash = "sha256:919c328cacd4bc4f873ea61ab4db0d8f2c32e0db09a3c73ab46b1de497556464"}, +] + +[package.dependencies] +flake8-plugin-utils = ">=1.3.2,<2.0.0" + [[package]] name = "flake8-quotes" version = "3.4.0" @@ -383,6 +509,17 @@ files = [ [package.dependencies] flake8 = "*" +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "isort" version = "5.13.2" @@ -564,6 +701,21 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-a test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.11.2)"] +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "pycodestyle" version = "2.12.1" @@ -637,6 +789,46 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pytest" +version = "8.3.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "pyyaml" version = "6.0.2" @@ -814,4 +1006,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "ba133486945806a456cac2a2e5ba54a79b772ef1257b0a5f3b39545063c6e229" +content-hash = "d57341979796164059f8d01fd2cc367cc21f1000a91b7d96fb02c6319a9b012b" diff --git a/pyproject.toml b/pyproject.toml index 8dbfd7e..64c1a27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,11 +53,15 @@ flake8-isort = "^6.1" flake8-quotes = "^3.4" flake8-string-format = "^0.3" flake8-pyproject = "^1.2" +flake8-pytest-style = "^2.0" mypy = "^1.11" pep8-naming = "^0.14" # flake8 plug-in pydoclint = { extras = ["flake8"], version = "^0.5" } ruff = "^0.6" +# Test suite +pytest = "^8.3" +pytest-cov = "^5.0" [tool.poetry.urls] @@ -86,6 +90,34 @@ target-version = ["py312", "py311", "py310", "py39"] +[tool.coverage] +# Source: https://coverage.readthedocs.io/en/latest/config.html + + +[tool.coverage.paths] + +source = ["src/", "*/site-packages/"] + + +[tool.coverage.report] + +show_missing = true + +skip_covered = true +skip_empty = true + + +[tool.coverage.run] + +data_file = ".cache/coverage/data" + +branch = true +parallel = true + +source = ["lalib"] + + + [tool.flake8] select = [ @@ -103,6 +135,7 @@ select = [ "F", # pyflakes => basic errors (Source: https://flake8.pycqa.org/en/latest/user/error-codes.html) "I", # flake8-isort => isort would make changes "N", # pep8-naming + "PT", # flake8-pytest-style => enforce a consistent style with pytest "Q", # flake8-quotes => use double quotes everywhere (complying with black) "S", # flake8-bandit => common security issues "T10", # flake8-debugger => no debugger usage @@ -154,6 +187,22 @@ docstring-convention = "google" # Source: https://github.com/wemake-services/flake8-eradicate#options eradicate-aggressive = true +# Plug-in: flake8-pytest-style +# +# Aligned with [tool.ruff.lint.flake8-pytest-style] below +# +# Prefer `@pytest.fixture` over `@pytest.fixture()` +pytest-fixture-no-parentheses = true +# +# Prefer `@pytest.mark.foobar` over `@pytest.mark.foobar()` +pytest-mark-no-parentheses = true +# +# Prefer `@pytest.mark.parametrize(['param1', 'param2'], [(1, 2), (3, 4)])` +# over `@pytest.mark.parametrize(('param1', 'param2'), ([1, 2], [3, 4]))` +pytest-parametrize-names-type = "list" +pytest-parametrize-values-row-type = "tuple" +pytest-parametrize-values-type = "list" + # Plug-in: flake8-quotes # Source: https://github.com/zheller/flake8-quotes#configuration avoid-escape = true @@ -209,6 +258,16 @@ ignore_missing_imports = true +[tool.pytest.ini_options] +# Source: https://docs.pytest.org/en/stable/ + +cache_dir = ".cache/pytest" + +addopts = "--strict-markers" +console_output_style = "count" + + + [tool.ruff] # Source: https://docs.astral.sh/ruff/ @@ -250,6 +309,7 @@ select = [ "F", # pyflakes => basic errors "I", # flake8-isort => isort would make changes "N", # pep8-naming + "PT", # flake8-pytest-style => enforce a consistent style with pytest "Q", # flake8-quotes => use double quotes everywhere "S", # flake8-bandit => common security issues "T10", # flake8-debugger => no debugger usage @@ -275,6 +335,21 @@ extend-ignore = [ # never check the following codes ] +[tool.ruff.lint.flake8-pytest-style] # aligned with [tool.flake8] above + +# Prefer `@pytest.fixture` over `@pytest.fixture()` +fixture-parentheses = false + +# Prefer `@pytest.mark.foobar` over `@pytest.mark.foobar()` +mark-parentheses = false + +# Prefer `@pytest.mark.parametrize(['param1', 'param2'], [(1, 2), (3, 4)])` +# over `@pytest.mark.parametrize(('param1', 'param2'), ([1, 2], [3, 4]))` +parametrize-names-type = "list" +parametrize-values-row-type = "tuple" +parametrize-values-type = "list" + + [tool.ruff.lint.isort] # aligned with [tool.isort] above case-sensitive = true diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..9c490c2 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the `lalib` library.""" From 4100a7f3f54688544626969c08ee7d22562189ad Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 01:45:47 +0200 Subject: [PATCH 09/26] Add `__version__` identifier - `lalib.__version__` is dynamically assigned - the format is "x.y.z[.dev0|aN|bN|rcN|.postM]" where x, y, z, M, and N are non-negative integers + x, y, and z model the "major", "minor", and "patch" parts of semantic versioning (See: https://semver.org/#semantic-versioning-200) + M is a single digit and N either 1 or 2 => This complies with (a strict subset of) PEP440 - add unit tests for the `__version__` identifier --- README.md | 11 + noxfile.py | 5 +- poetry.lock | 13 +- pyproject.toml | 16 ++ src/lalib/__init__.py | 18 ++ tests/test_version.py | 587 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 647 insertions(+), 3 deletions(-) create mode 100644 tests/test_version.py diff --git a/README.md b/README.md index 513f168..a3b794e 100644 --- a/README.md +++ b/README.md @@ -119,3 +119,14 @@ Whereas a rebase makes a simple fast-forward merge possible, all merges are made with explicit and *empty* merge commits. This ensures that past branches remain visible in the logs, for example, with `git log --graph`. + + +#### Versioning + +The version identifiers adhere to a subset of the rules in + [PEP440](https://peps.python.org/pep-0440/) and + follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +So, releases to [PyPI](https://pypi.org/) + come in the popular `major.minor.patch` format. +The specific rules for this project are explained + [here](https://github.com/webartifex/lalib/blob/main/tests/test_version.py). diff --git a/noxfile.py b/noxfile.py index 26e5f36..63f462a 100644 --- a/noxfile.py +++ b/noxfile.py @@ -152,8 +152,10 @@ def test(session: nox.Session) -> None: install_unpinned(session, "-e", ".") # "-e" makes session reuseable install_pinned( session, + "packaging", "pytest", "pytest-cov", + "semver", ) args = session.posargs or ( @@ -162,8 +164,7 @@ def test(session: nox.Session) -> None: TESTS_LOCATION, ) - # Code 5 is temporary as long as there are no tests - session.run("pytest", *args, success_codes=[0, 5]) + session.run("pytest", *args) def start(session: nox.Session) -> None: diff --git a/poetry.lock b/poetry.lock index fc9d3af..accb356 100644 --- a/poetry.lock +++ b/poetry.lock @@ -936,6 +936,17 @@ files = [ {file = "ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212"}, ] +[[package]] +name = "semver" +version = "3.0.2" +description = "Python helper for Semantic Versioning (https://semver.org)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, + {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, +] + [[package]] name = "setuptools" version = "74.1.2" @@ -1006,4 +1017,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "d57341979796164059f8d01fd2cc367cc21f1000a91b7d96fb02c6319a9b012b" +content-hash = "4ba46548b380fe7c34cf4ed48492253b2d3a1866f86c8d8bf569eb18829281c2" diff --git a/pyproject.toml b/pyproject.toml index 64c1a27..762c1ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,8 +60,11 @@ pydoclint = { extras = ["flake8"], version = "^0.5" } ruff = "^0.6" # Test suite +packaging = "^24.1" # to test the version identifier pytest = "^8.3" pytest-cov = "^5.0" +semver = "^3.0" # to test the version identifier +tomli = [ { python = "<3.11", version = "^2.0" } ] [tool.poetry.urls] @@ -165,6 +168,11 @@ extend-ignore = [ # never check the following codes per-file-ignores = [ + # Linting rules for the test suite: + # - type hints are not required + # - `assert`s are normal + "tests/*.py:ANN,S101", + ] # Explicitly set mccabe's maximum complexity to 10 as recommended by @@ -253,6 +261,9 @@ cache_dir = ".cache/mypy" module = [ "nox", "nox_poetry", + "pytest", + "semver", + "tomli", ] ignore_missing_imports = true @@ -362,6 +373,11 @@ known-first-party = ["lalib"] [tool.ruff.lint.per-file-ignores] +"tests/*.py" = [ # Linting rules for the test suite: + "ANN", # - type hints are not required + "S101", # - `assert`s are normal + "W505", # - docstrings may be longer than 72 characters +] [tool.ruff.lint.pycodestyle] diff --git a/src/lalib/__init__.py b/src/lalib/__init__.py index 50db320..53f0508 100644 --- a/src/lalib/__init__.py +++ b/src/lalib/__init__.py @@ -1 +1,19 @@ """A Python library to study linear algebra.""" + +from importlib import metadata + + +try: + pkg_info = metadata.metadata(__name__) + +except metadata.PackageNotFoundError: + __pkg_name__ = "unknown" + __version__ = "unknown" + +else: + __pkg_name__ = pkg_info["name"] + __version__ = pkg_info["version"] + del pkg_info + + +del metadata diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..be02fae --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,587 @@ +"""Test the package's version identifier. + +The packaged version identifier (i.e., `lalib.__version__`) +adheres to PEP440 and its base part follows semantic versioning: + + - In general, version identifiers follow the "x.y.z" format + where x, y, and z are non-negative integers (e.g., "0.1.0" + or "1.2.3") matching the "major", "minor", and "patch" parts + of semantic versioning + + - Without suffixes, these "x.y.z" versions represent + the ordinary releases to PyPI + + - Developmental or non-release versions are indicated + with a ".dev0" suffix; we use solely a "0" to keep things simple + + - Pre-releases come as "alpha", "beta", and "release candidate" + versions, indicated with "aN", "bN", and "rcN" suffixes + (no "." separator before the suffixes) where N is either 1 or 2 + + - Post-releases are possible and indicated with a ".postN" suffix + where N is between 0 and 9; they must not change the code + as compared to their corresponding ordinary release version + +Examples: + - "0.4.2" => ordinary release; public API may be unstable + as explained by the rules of semantic versioning + - "1.2.3" => ordinary release (long-term stable versions) + - "4.5.6.dev0" => early development stage before release "4.5.6" + - "4.5.6a1" => pre-release shortly before publication of "4.5.6" + +Sources: + - https://peps.python.org/pep-0440/ + - https://semver.org/spec/v2.0.0.html + +Implementation notes: + + - The `packaging` library and the `importlib.metadata` module + are very forgiving when parsing version identifiers + => The test cases in this file enforce a strict style + + - The `DECLARED_VERSION` (in pyproject.toml) and the + `PACKAGED_VERSION` (read from the metadata after installation + in a virtual environment) are tested besides a lot of + example `VALID_VERSIONS` to obtain a high confidence + in the test cases + + - There are two generic kind of test cases: + + + `TestVersionIdentifier` uses the `packaging` and `semver + libraries to parse the various `*_VERSION`s and validate + their infos + + + `TestVersionIdentifierWithPattern` defines a `regex` pattern + comprising all rules at once +""" + +import contextlib +import importlib +import itertools +import pathlib +import re +import string +import sys + +import pytest +import semver +from packaging import version as pkg_version + +import lalib + + +# Support Python 3.9 and 3.10 +try: + import tomllib +except ModuleNotFoundError: + import tomli as tomllib # type: ignore[no-redef] + + +def load_version_from_pyproject_toml(): + """The version declared in pyproject.toml.""" + with pathlib.Path("pyproject.toml").open("rb") as fp: + pyproject_toml = tomllib.load(fp) + + return pyproject_toml["tool"]["poetry"]["version"] + + +def expand_digits_to_versions( + digits=string.digits[1:], + *, + pre_process=(lambda x, y, z: (x, y, z)), + filter_=(lambda _x, _y, _z: True), + post_process=(lambda x, y, z: (x, y, z)), + unique=False, +): + """Yield examplatory semantic versions. + + For example, "12345" is expanded into "1.2.345", "1.23.45", ..., "123.4.5". + + In general, the `digits` are sliced into three parts `x`, `y`, and `z` + that could be thought of the "major", "minor", and "patch" parts of + a version identifier. The `digits` themselves are not re-arranged. + + `pre_process(x, y, z)` transform the parts individually. As an example, + `part % 100` makes each part only use the least significant digits. + So, in the example, "1.2.345" becomes "1.2.45". + + `post_process(x, y, z)` does the same but only after applying the + `filter_(x, y, z)` which signals if the current `x`, `y`, and `z` + should be skipped. + + `unique=True` ensures a produced version identifier is yielded only once. + """ + seen_before = set() if unique else None + + for i in range(1, len(digits) - 1): + for j in range(i + 1, len(digits)): + x, y, z = int(digits[:i]), int(digits[i:j]), int(digits[j:]) + + x, y, z = pre_process(x, y, z) + + if not filter_(x, y, z): + continue + + x, y, z = post_process(x, y, z) + + if unique: + if (x, y, z) in seen_before: + continue + else: + seen_before.add((x, y, z)) + + yield f"{x}.{y}.{z}" + + +DECLARED_VERSION = load_version_from_pyproject_toml() +PACKAGED_VERSION = lalib.__version__ + +VALID_AND_NORMALIZED_VERSIONS = ( + "0.1.0", + "0.1.1", + "0.1.99", + "0.2.0", + "0.99.0", + "1.0.0", + "1.2.3.dev0", + "1.2.3a1", + "1.2.3a2", + "1.2.3b1", + "1.2.3b2", + "1.2.3rc1", + "1.2.3rc2", + "1.2.3", + *(f"1.2.3.post{n}" for n in range(10)), + # e.g., "1.2.89", "1.23.89", "1.78.9", "12.3.89", "12.34.89", and "67.8.9" + *expand_digits_to_versions( + "12345689", + pre_process=(lambda x, y, z: (x % 100, y % 100, z % 100)), + ), +) + +# The `packaging` library can parse the following versions +# that are then normalized according to PEP440 +# Source: https://peps.python.org/pep-0440/#normalization +VALID_AND_NOT_NORMALIZED_VERSIONS = ( + "1.2.3dev0", + "1.2.3-dev0", + "1.2.3_dev0", + "1.2.3alpha1", + "1.2.3.alpha1", + "1.2.3-alpha1", + "1.2.3_alpha1", + "1.2.3.a1", + "1.2.3.a2", + "1.2.3beta1", + "1.2.3.beta1", + "1.2.3-beta1", + "1.2.3_beta1", + "1.2.3.b1", + "1.2.3.b2", + "1.2.3c1", + "1.2.3.c1", + "1.2.3.rc1", + "1.2.3c2", + "1.2.3.c2", + "1.2.3.rc2", + "1.2.3post0", + "1.2.3-post0", + "1.2.3_post0", + "1.2.3-r0", + "1.2.3-rev0", + "1.2.3-0", + "1.2.3_post9", +) + +VALID_VERSIONS = ( + DECLARED_VERSION, + PACKAGED_VERSION, + *VALID_AND_NORMALIZED_VERSIONS, + *VALID_AND_NOT_NORMALIZED_VERSIONS, +) + +# The following persions cannot be parsed by the `packaging` library +INVALID_NOT_READABLE = ( + "-1.2.3", + "+1.2.3", + "!1.2.3", + "x.2.3", + "1.y.3", + "1.2.z", + "x.y.z", + "1.2.3.abc", + "1.2.3.d0", + "1.2.3.develop0", + "1.2.3..dev0", + "1..2.3", + "1.2..3", + "1-2-3", + "1,2,3", +) + +# The `packaging` library is able to parse the following versions +# that however are not considered valid for this project +INVALID_NOT_SEMANTIC = ( + "1", + "1.2", + "01.2.3", + "1.02.3", + "1.2.03", + "1.2.3.4", + "1.2.3.dev-1", + "1.2.3.dev01", + "v1.2.3", +) + +INVALID_VERSIONS = INVALID_NOT_READABLE + INVALID_NOT_SEMANTIC + + +@pytest.mark.parametrize( + ["version1", "version2"], + zip( # loop over pairs of neighboring elements + VALID_AND_NORMALIZED_VERSIONS, + VALID_AND_NORMALIZED_VERSIONS[1:], + ), +) +def test_versions_are_strictly_ordered(version1, version2): + """`VALID_AND_NORMALIZED_VERSIONS` are ordered.""" + version1_parsed = pkg_version.Version(version1) + version2_parsed = pkg_version.Version(version2) + + assert version1_parsed < version2_parsed + + +@pytest.mark.parametrize( + ["version1", "version2"], + zip( # loop over pairs of neighboring elements + VALID_AND_NOT_NORMALIZED_VERSIONS, + VALID_AND_NOT_NORMALIZED_VERSIONS[1:], + ), +) +def test_versions_are_weakly_ordered(version1, version2): + """`VALID_AND_NOT_NORMALIZED_VERSIONS` are ordered.""" + version1_parsed = pkg_version.Version(version1) + version2_parsed = pkg_version.Version(version2) + + assert version1_parsed <= version2_parsed + + +class VersionClassification: + """Classifying version identifiers. + + There are four distinct kinds of version identifiers: + - "X.Y.Z.devN" => developmental non-releases + - "X.Y.Z[aN|bN|rcN]" => pre-releases (e.g., alpha, beta, or release candidates) + - "X.Y.Z" => ordinary (or "official) releases to PypI + - "X.Y.Z.postN" => post-releases (e.g., to add missing non-code artifacts) + + The `packaging` library models these four cases slightly different, and, + most notably in an overlapping fashion (i.e., developmental releases are + also pre-releases). + + The four methods in this class introduce our own logic + treating the four cases in a non-overlapping fashion. + """ + + def is_dev_release(self, parsed_version): + """A "X.Y.Z.devN" release.""" + is_so_by_parts = ( + parsed_version.dev is not None + and parsed_version.pre is None + and parsed_version.post is None + ) + + if is_so_by_parts: + assert parsed_version.is_devrelease is True + assert parsed_version.is_prerelease is True + assert parsed_version.is_postrelease is False + + return is_so_by_parts + + def is_pre_release(self, parsed_version): + """A "X.Y.Z[aN|bN|rcN]" release.""" + is_so_by_parts = ( + parsed_version.dev is None + and parsed_version.pre is not None + and parsed_version.post is None + ) + + if is_so_by_parts: + assert parsed_version.is_devrelease is False + assert parsed_version.is_prerelease is True + assert parsed_version.is_postrelease is False + + return is_so_by_parts + + def is_ordinary_release(self, parsed_version): + """A "X.Y.Z" release.""" + is_so_by_parts = ( + parsed_version.dev is None + and parsed_version.pre is None + and parsed_version.post is None + ) + + if is_so_by_parts: + assert parsed_version.is_devrelease is False + assert parsed_version.is_prerelease is False + assert parsed_version.is_postrelease is False + + return is_so_by_parts + + def is_post_release(self, parsed_version): + """A "X.Y.Z.postN" release.""" + is_so_by_parts = ( + parsed_version.dev is None + and parsed_version.pre is None + and parsed_version.post is not None + ) + + if is_so_by_parts: + assert parsed_version.is_devrelease is False + assert parsed_version.is_prerelease is False + assert parsed_version.is_postrelease is True + + return is_so_by_parts + + +class TestVersionIdentifier(VersionClassification): + """The versions must comply with PEP440 ... + + and follow some additional constraints, most notably that a version's + base complies with semantic versioning. + """ + + def test_packaged_version_is_declared_version(self): + """`lalib.__version__` matches "version" in pyproject.toml exactly.""" + assert PACKAGED_VERSION == DECLARED_VERSION + + @pytest.fixture + def parsed_version(self, request): + """A version identifier parsed with `packaging.version.Version()`.""" + return pkg_version.Version(request.param) + + @pytest.fixture + def unparsed_version(self, request): + """A version identifier represented as an ordinary `str`.""" + return request.param + + @pytest.mark.parametrize("unparsed_version", INVALID_NOT_READABLE) + def test_does_not_follow_pep440(self, unparsed_version): + """A version's base does not follow PEP440.""" + with pytest.raises(pkg_version.InvalidVersion): + pkg_version.Version(unparsed_version) + + @pytest.mark.parametrize( + ["parsed_version", "unparsed_version"], + [(v, v) for v in VALID_VERSIONS], + indirect=True, + ) + def test_base_follows_semantic_versioning(self, parsed_version, unparsed_version): + """A version's base follows semantic versioning.""" + result = semver.Version.parse(parsed_version.base_version) + result = str(result) + + assert unparsed_version.startswith(result) + + @pytest.mark.parametrize("version", INVALID_NOT_READABLE + INVALID_NOT_SEMANTIC) + def test_base_does_not_follow_semantic_versioning(self, version): + """A version's base does not follow semantic versioning.""" + with pytest.raises(ValueError, match="not valid SemVer"): + semver.Version.parse(version) + + @pytest.mark.parametrize("parsed_version", VALID_VERSIONS, indirect=True) + def test_has_major_minor_patch_parts(self, parsed_version): + """A version's base consists of three parts.""" + three_parts = 3 + + assert len(parsed_version.release) == three_parts + + @pytest.mark.parametrize("parsed_version", VALID_VERSIONS, indirect=True) + @pytest.mark.parametrize("part", ["major", "minor", "micro"]) + def test_major_minor_patch_parts_are_within_range(self, parsed_version, part): + """A version's "major", "minor", and "patch" parts are non-negative and `< 100`.""" + # "micro" in PEP440 is "patch" in semantic versioning + part = getattr(parsed_version, part, -1) + two_digits_only = 100 + + assert 0 <= part < two_digits_only + + @pytest.mark.parametrize("parsed_version", VALID_VERSIONS, indirect=True) + def test_is_either_dev_pre_post_or_ordinary_release(self, parsed_version): + """A version is exactly one of four kinds.""" + result = ( # `bool`s behaving like `int`s + self.is_dev_release(parsed_version) + + self.is_pre_release(parsed_version) + + self.is_ordinary_release(parsed_version) + + self.is_post_release(parsed_version) + ) + + assert result == 1 + + @pytest.mark.parametrize("parsed_version", VALID_VERSIONS, indirect=True) + def test_dev_releases_come_with_dev0(self, parsed_version): + """A ".devN" version always comes with ".dev0".""" + if self.is_dev_release(parsed_version): + assert parsed_version.dev == 0 + assert parsed_version.pre is None + assert parsed_version.post is None + else: + assert parsed_version.dev is None + + @pytest.mark.parametrize("parsed_version", VALID_VERSIONS, indirect=True) + def test_pre_releases_come_with_suffix1_or_suffix2(self, parsed_version): + """A "aN", "bN", or "rcN" version always comes with N as 1 or 2.""" + if self.is_pre_release(parsed_version): + assert parsed_version.dev is None + assert parsed_version.pre[1] in (1, 2) + assert parsed_version.post is None + else: + assert parsed_version.pre is None + + @pytest.mark.parametrize("parsed_version", VALID_VERSIONS, indirect=True) + def test_ordinary_releases_have_no_suffixes(self, parsed_version): + """A ordinary release versions has no suffixes.""" + if self.is_ordinary_release(parsed_version): + assert parsed_version.dev is None + assert parsed_version.pre is None + assert parsed_version.post is None + + @pytest.mark.parametrize("parsed_version", VALID_VERSIONS, indirect=True) + def test_post_releases_come_with_post0_to_post9(self, parsed_version): + """A ".postN" version always comes with N as 0 through 9.""" + if self.is_post_release(parsed_version): + assert parsed_version.dev is None + assert parsed_version.pre is None + assert parsed_version.post in range(10) + else: + assert parsed_version.post is None + + @pytest.mark.parametrize("parsed_version", VALID_VERSIONS, indirect=True) + def test_has_no_epoch_segment(self, parsed_version): + """A version has no epoch segment.""" + assert parsed_version.epoch == 0 + + @pytest.mark.parametrize("parsed_version", VALID_VERSIONS, indirect=True) + def test_has_no_local_segment(self, parsed_version): + """A parsed_version has no local segment. + + In semantic versioning, the "local" segment is referred + to as the "build metadata". + """ + assert parsed_version.local is None + + @pytest.mark.parametrize( + ["parsed_version", "unparsed_version"], + [ + (v, v) + for v in ( + DECLARED_VERSION, + PACKAGED_VERSION, + *VALID_AND_NORMALIZED_VERSIONS, + ) + ], + indirect=True, + ) + def test_is_normalized(self, parsed_version, unparsed_version): + """A version is already normalized. + + For example, a version cannot be "1.2.3.a1" + because this gets normalized into "1.2.3a1". + """ + assert parsed_version.public == unparsed_version + + @pytest.mark.parametrize( + ["parsed_version", "unparsed_version"], + [(v, v) for v in VALID_AND_NOT_NORMALIZED_VERSIONS], + indirect=True, + ) + def test_is_not_normalized(self, parsed_version, unparsed_version): + """A version is not yet normalized. + + For example, the version "1.2.3.a1" + gets normalized into "1.2.3a1". + """ + assert parsed_version.public != unparsed_version + + +class TestVersionIdentifierWithPattern: + """Test the versioning with a custom `regex` pattern.""" + + x_y_z_version = r"^(0|([1-9]\d*))\.(0|([1-9]\d*))\.(0|([1-9]\d*))" + suffixes = r"((\.dev0)|(((a)|(b)|(rc))(1|2))|(\.post\d{1}))" + version_pattern = re.compile(f"^{x_y_z_version}{suffixes}?$") + + @pytest.mark.parametrize( + "version", + [ + DECLARED_VERSION, + PACKAGED_VERSION, + ], + ) + def test_packaged_and_declared_version(self, version): + """Packaged version follows PEP440 and semantic versioning.""" + result = self.version_pattern.fullmatch(version) + + assert result is not None + + # The next two test cases are sanity checks to validate the `version_pattern`. + + @pytest.mark.parametrize("version", VALID_AND_NORMALIZED_VERSIONS) + def test_valid_versioning(self, version): + """A version follows the "x.y.z[.devN|aN|bN|rcN|.postN]" format.""" + result = self.version_pattern.fullmatch(version) + + assert result is not None + + @pytest.mark.parametrize("version", INVALID_VERSIONS) + def test_invalid_versioning(self, version): + """A version does not follow the "x.y.z[.devN|aN|bN|rcN|.postN]" format.""" + result = self.version_pattern.fullmatch(version) + + assert result is None + + +class TestUnavailablePackageMetadata: + """Pretend only source files are available, without metadata.""" + + def find_path_to_package_metadata_folder(self, name): + """Find the path to a locally installed package within a `venv`.""" + paths = tuple( + itertools.chain( + *(pathlib.Path(path).glob(f"{name}-*.dist-info/") for path in sys.path), + ), + ) + + # Sanity Check: There must be exactly one folder + # for an installed package within a virtual environment + assert len(paths) == 1 + + return pathlib.Path(paths[0]).relative_to(pathlib.Path.cwd()) + + @contextlib.contextmanager + def hide_metadata_from_package(self, name): + """Hide the metadata of a locally installed package.""" + # Rename the metadata folder + path = self.find_path_to_package_metadata_folder(name) + path.rename(str(path).replace(name, f"{name}.tmp")) + + # (Re-)Load the package with missing metadata + package = importlib.import_module(name) + importlib.reload(package) + + try: + yield package + + finally: + # Restore the original metadata folder + path = self.find_path_to_package_metadata_folder(f"{name}.tmp") + path = path.rename(str(path).replace(f"{name}.tmp", name)) + + # Reload the package with the original metadata for other tests + importlib.reload(package) + + def test_package_without_version_info(self): + """Import `lalib` with no available version info.""" + with self.hide_metadata_from_package("lalib") as lalib_pkg: + assert lalib_pkg.__pkg_name__ == "unknown" + assert lalib_pkg.__version__ == "unknown" From 01d270e39c71fc156d404650b8e4ee135caa701c Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 01:57:02 +0200 Subject: [PATCH 10/26] Add doctests to the test suite - use xdoctest to validate code snippets in docstrings - make xdoctest part of the nox session "test" via the new `test_docstrings()` test case - add nox session "test-docstrings" for convenience; also, `xdoctest.doctest_module()` does not discover docstrings that are imported at the root of the package => each new module with docstrings must be added to `test_docstrings()` by hand, which is likely forgotten => the nox session "test-docstrings" should run on CI --- noxfile.py | 12 ++++++++++++ poetry.lock | 33 ++++++++++++++++++++++++++++++++- pyproject.toml | 2 ++ src/lalib/__init__.py | 8 +++++++- tests/test_docstrings.py | 23 +++++++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 tests/test_docstrings.py diff --git a/noxfile.py b/noxfile.py index 63f462a..8751332 100644 --- a/noxfile.py +++ b/noxfile.py @@ -76,6 +76,7 @@ nox.options.reuse_venv = "no" nox.options.sessions = ( # run by default when invoking `nox` on the CLI "format", "lint", + "test-docstrings", f"test-{MAIN_PYTHON}", ) nox.options.stop_on_first_error = True @@ -156,6 +157,7 @@ def test(session: nox.Session) -> None: "pytest", "pytest-cov", "semver", + "xdoctest", ) args = session.posargs or ( @@ -167,6 +169,16 @@ def test(session: nox.Session) -> None: session.run("pytest", *args) +@nox_session(name="test-docstrings", python=MAIN_PYTHON) +def test_docstrings(session: nox.Session) -> None: + """Test docstrings with `xdoctest`.""" + start(session) + install_pinned(session, "xdoctest[colors]") + + session.run("xdoctest", "--version") + session.run("xdoctest", "src/lalib") + + def start(session: nox.Session) -> None: """Show generic info about a session.""" if session.posargs: diff --git a/poetry.lock b/poetry.lock index accb356..6438f42 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1014,7 +1014,38 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "xdoctest" +version = "1.2.0" +description = "A rewrite of the builtin doctest module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "xdoctest-1.2.0-py3-none-any.whl", hash = "sha256:0f1ecf5939a687bd1fc8deefbff1743c65419cce26dff908f8b84c93fbe486bc"}, + {file = "xdoctest-1.2.0.tar.gz", hash = "sha256:d8cfca6d8991e488d33f756e600d35b9fdf5efd5c3a249d644efcbbbd2ed5863"}, +] + +[package.dependencies] +colorama = {version = ">=0.4.1", optional = true, markers = "platform_system == \"Windows\" and extra == \"colors\""} +Pygments = {version = ">=2.4.1", optional = true, markers = "python_version >= \"3.5.0\" and extra == \"colors\""} + +[package.extras] +all = ["IPython (>=7.23.1)", "Pygments (>=2.0.0)", "Pygments (>=2.4.1)", "attrs (>=19.2.0)", "colorama (>=0.4.1)", "debugpy (>=1.0.0)", "debugpy (>=1.3.0)", "debugpy (>=1.6.0)", "ipykernel (>=6.0.0)", "ipykernel (>=6.11.0)", "ipython-genutils (>=0.2.0)", "jedi (>=0.16)", "jinja2 (>=3.0.0)", "jupyter-client (>=7.0.0)", "jupyter-core (>=4.7.0)", "nbconvert (>=6.0.0)", "nbconvert (>=6.1.0)", "pyflakes (>=2.2.0)", "pytest (>=4.6.0)", "pytest (>=6.2.5)", "pytest-cov (>=3.0.0)", "tomli (>=0.2.0)"] +all-strict = ["IPython (==7.23.1)", "Pygments (==2.0.0)", "Pygments (==2.4.1)", "attrs (==19.2.0)", "colorama (==0.4.1)", "debugpy (==1.0.0)", "debugpy (==1.3.0)", "debugpy (==1.6.0)", "ipykernel (==6.0.0)", "ipykernel (==6.11.0)", "ipython-genutils (==0.2.0)", "jedi (==0.16)", "jinja2 (==3.0.0)", "jupyter-client (==7.0.0)", "jupyter-core (==4.7.0)", "nbconvert (==6.0.0)", "nbconvert (==6.1.0)", "pyflakes (==2.2.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "tomli (==0.2.0)"] +colors = ["Pygments (>=2.0.0)", "Pygments (>=2.4.1)", "colorama (>=0.4.1)"] +colors-strict = ["Pygments (==2.0.0)", "Pygments (==2.4.1)", "colorama (==0.4.1)"] +docs = ["Pygments (>=2.9.0)", "myst-parser (>=0.18.0)", "sphinx (>=5.0.1)", "sphinx-autoapi (>=1.8.4)", "sphinx-autobuild (>=2021.3.14)", "sphinx-reredirects (>=0.0.1)", "sphinx-rtd-theme (>=1.0.0)", "sphinxcontrib-napoleon (>=0.7)"] +docs-strict = ["Pygments (==2.9.0)", "myst-parser (==0.18.0)", "sphinx (==5.0.1)", "sphinx-autoapi (==1.8.4)", "sphinx-autobuild (==2021.3.14)", "sphinx-reredirects (==0.0.1)", "sphinx-rtd-theme (==1.0.0)", "sphinxcontrib-napoleon (==0.7)"] +jupyter = ["IPython (>=7.23.1)", "attrs (>=19.2.0)", "debugpy (>=1.0.0)", "debugpy (>=1.3.0)", "debugpy (>=1.6.0)", "ipykernel (>=6.0.0)", "ipykernel (>=6.11.0)", "ipython-genutils (>=0.2.0)", "jedi (>=0.16)", "jinja2 (>=3.0.0)", "jupyter-client (>=7.0.0)", "jupyter-core (>=4.7.0)", "nbconvert (>=6.0.0)", "nbconvert (>=6.1.0)"] +jupyter-strict = ["IPython (==7.23.1)", "attrs (==19.2.0)", "debugpy (==1.0.0)", "debugpy (==1.3.0)", "debugpy (==1.6.0)", "ipykernel (==6.0.0)", "ipykernel (==6.11.0)", "ipython-genutils (==0.2.0)", "jedi (==0.16)", "jinja2 (==3.0.0)", "jupyter-client (==7.0.0)", "jupyter-core (==4.7.0)", "nbconvert (==6.0.0)", "nbconvert (==6.1.0)"] +optional = ["IPython (>=7.23.1)", "Pygments (>=2.0.0)", "Pygments (>=2.4.1)", "attrs (>=19.2.0)", "colorama (>=0.4.1)", "debugpy (>=1.0.0)", "debugpy (>=1.3.0)", "debugpy (>=1.6.0)", "ipykernel (>=6.0.0)", "ipykernel (>=6.11.0)", "ipython-genutils (>=0.2.0)", "jedi (>=0.16)", "jinja2 (>=3.0.0)", "jupyter-client (>=7.0.0)", "jupyter-core (>=4.7.0)", "nbconvert (>=6.0.0)", "nbconvert (>=6.1.0)", "pyflakes (>=2.2.0)", "tomli (>=0.2.0)"] +optional-strict = ["IPython (==7.23.1)", "Pygments (==2.0.0)", "Pygments (==2.4.1)", "attrs (==19.2.0)", "colorama (==0.4.1)", "debugpy (==1.0.0)", "debugpy (==1.3.0)", "debugpy (==1.6.0)", "ipykernel (==6.0.0)", "ipykernel (==6.11.0)", "ipython-genutils (==0.2.0)", "jedi (==0.16)", "jinja2 (==3.0.0)", "jupyter-client (==7.0.0)", "jupyter-core (==4.7.0)", "nbconvert (==6.0.0)", "nbconvert (==6.1.0)", "pyflakes (==2.2.0)", "tomli (==0.2.0)"] +tests = ["pytest (>=4.6.0)", "pytest (>=6.2.5)", "pytest-cov (>=3.0.0)"] +tests-binary = ["cmake (>=3.21.2)", "cmake (>=3.25.0)", "ninja (>=1.10.2)", "ninja (>=1.11.1)", "pybind11 (>=2.10.3)", "pybind11 (>=2.7.1)", "scikit-build (>=0.11.1)", "scikit-build (>=0.16.1)"] +tests-binary-strict = ["cmake (==3.21.2)", "cmake (==3.25.0)", "ninja (==1.10.2)", "ninja (==1.11.1)", "pybind11 (==2.10.3)", "pybind11 (==2.7.1)", "scikit-build (==0.11.1)", "scikit-build (==0.16.1)"] +tests-strict = ["pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)"] + [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "4ba46548b380fe7c34cf4ed48492253b2d3a1866f86c8d8bf569eb18829281c2" +content-hash = "3648b3426753e3e36e575d6b08fe8ab1b5f5443dcf2d1effaed5e921cdb40933" diff --git a/pyproject.toml b/pyproject.toml index 762c1ec..477f59e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ pytest = "^8.3" pytest-cov = "^5.0" semver = "^3.0" # to test the version identifier tomli = [ { python = "<3.11", version = "^2.0" } ] +xdoctest = { extras = ["colors"], version = "^1.2" } [tool.poetry.urls] @@ -264,6 +265,7 @@ module = [ "pytest", "semver", "tomli", + "xdoctest", ] ignore_missing_imports = true diff --git a/src/lalib/__init__.py b/src/lalib/__init__.py index 53f0508..fb2c190 100644 --- a/src/lalib/__init__.py +++ b/src/lalib/__init__.py @@ -1,4 +1,10 @@ -"""A Python library to study linear algebra.""" +"""A Python library to study linear algebra. + +First, verify that your installation of `lalib` works: +>>> import lalib +>>> lalib.__version__ != '0.0.0' +True +""" from importlib import metadata diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py new file mode 100644 index 0000000..4890a36 --- /dev/null +++ b/tests/test_docstrings.py @@ -0,0 +1,23 @@ +"""Integrate `xdoctest` into the test suite. + +Ensure all code snippets in docstrings are valid and functioning code. + +Important: All modules with docstrings containing code snippets + must be put on the parameter list below by hand! +""" + +import pytest +import xdoctest + + +@pytest.mark.parametrize( + "module", + [ + "lalib", + ], +) +def test_docstrings(module): + """Test code snippets within the package with `xdoctest`.""" + result = xdoctest.doctest_module(module, "all") + + assert result["n_failed"] == 0 From 6945cdef0aed7147304f4d30b25b42fcedb20b12 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 02:01:23 +0200 Subject: [PATCH 11/26] Add coverage reporting to the test suite - the nox session "test-coverage" triggers further nox sessions that run the test suite for all supported Python versions - the nox session "_test-coverage-run" runs the test suite for a particular Python version using the coverage tool - the nox session "_test-coverage-report" combines the individual coverage reports --- .gitignore | 1 + noxfile.py | 86 +++++++++++++++++++++++++++++++++++++++++++++----- poetry.lock | 2 +- pyproject.toml | 1 + 4 files changed, 81 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 13eb353..c22ea0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .cache/ +dist/ poetry.toml **/__pycache__/ .venv/ diff --git a/noxfile.py b/noxfile.py index 8751332..301aa2e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -2,6 +2,7 @@ import collections import pathlib +import random import re import tempfile from collections.abc import Mapping @@ -145,20 +146,22 @@ def lint(session: nox.Session) -> None: session.run("ruff", "check", *locations) +TEST_DEPENDENCIES = ( + "packaging", + "pytest", + "pytest-cov", + "semver", + "xdoctest", +) + + @nox_session(python=SUPPORTED_PYTHONS) def test(session: nox.Session) -> None: """Test code with `pytest`.""" start(session) install_unpinned(session, "-e", ".") # "-e" makes session reuseable - install_pinned( - session, - "packaging", - "pytest", - "pytest-cov", - "semver", - "xdoctest", - ) + install_pinned(session, *TEST_DEPENDENCIES) args = session.posargs or ( "--cov", @@ -169,6 +172,58 @@ def test(session: nox.Session) -> None: session.run("pytest", *args) +_magic_number = random.randint(0, 987654321) # noqa: S311 + + +@nox_session(name="test-coverage", python=MAIN_PYTHON, reuse_venv=True) +def test_coverage(session: nox.Session) -> None: + """Report the combined coverage statistics. + + Run the test suite for all supported Python versions + and combine the coverage statistics. + """ + install_pinned(session, "coverage") + + session.run("python", "-m", "coverage", "erase") + + for version in SUPPORTED_PYTHONS: + session.notify(f"_test-coverage-run-{version}", (_magic_number,)) + session.notify("_test-coverage-report", (_magic_number,)) + + +@nox_session(name="_test-coverage-run", python=SUPPORTED_PYTHONS, reuse_venv=False) +def test_coverage_run(session: nox.Session) -> None: + """Measure the test coverage.""" + do_not_reuse(session) + do_not_run_directly(session) + + start(session) + + session.install(".") + install_pinned(session, "coverage", *TEST_DEPENDENCIES) + + session.run( + "python", + "-m", + "coverage", + "run", + "-m", + "pytest", + TESTS_LOCATION, + ) + + +@nox_session(name="_test-coverage-report", python=MAIN_PYTHON, reuse_venv=True) +def test_coverage_report(session: nox.Session) -> None: + """Report the combined coverage statistics.""" + do_not_run_directly(session) + + install_pinned(session, "coverage") + + session.run("python", "-m", "coverage", "combine") + session.run("python", "-m", "coverage", "report", "--fail-under=100") + + @nox_session(name="test-docstrings", python=MAIN_PYTHON) def test_docstrings(session: nox.Session) -> None: """Test docstrings with `xdoctest`.""" @@ -179,6 +234,21 @@ def test_docstrings(session: nox.Session) -> None: session.run("xdoctest", "src/lalib") +def do_not_reuse(session: nox.Session, *, raise_error: bool = True) -> None: + """Do not reuse a session with the "-r" flag.""" + if session._runner.venv._reused: # noqa:SLF001 + if raise_error: + session.error('The session must be run without the "-r" flag') + else: + session.warn('The session must be run without the "-r" flag') + + +def do_not_run_directly(session: nox.Session) -> None: + """Do not run a session with `nox -s SESSION_NAME` directly.""" + if not session.posargs or session.posargs[0] != _magic_number: + session.error("This session must not be run directly") + + def start(session: nox.Session) -> None: """Show generic info about a session.""" if session.posargs: diff --git a/poetry.lock b/poetry.lock index 6438f42..f39a1f3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1048,4 +1048,4 @@ tests-strict = ["pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "3648b3426753e3e36e575d6b08fe8ab1b5f5443dcf2d1effaed5e921cdb40933" +content-hash = "41aa1b0224786397f339cd01099c1a687cb57e16898426ef101fd66247d3bd5c" diff --git a/pyproject.toml b/pyproject.toml index 477f59e..9363e64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ pydoclint = { extras = ["flake8"], version = "^0.5" } ruff = "^0.6" # Test suite +coverage = "^7.6" packaging = "^24.1" # to test the version identifier pytest = "^8.3" pytest-cov = "^5.0" From 0a85e60b51eda3a1a40156abc675a3cbf2c5ea93 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 02:03:01 +0200 Subject: [PATCH 12/26] Set up a dependency auditing tool - use pip-audit to check the project's dependencies for known security vulnerabilities and exploits - add nox sessions "audit" and "audit-updates" to run the above checks against the pinned and unpinned dependencies --- noxfile.py | 94 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 9 deletions(-) diff --git a/noxfile.py b/noxfile.py index 301aa2e..3ca3e2a 100644 --- a/noxfile.py +++ b/noxfile.py @@ -77,12 +77,80 @@ nox.options.reuse_venv = "no" nox.options.sessions = ( # run by default when invoking `nox` on the CLI "format", "lint", + "audit", "test-docstrings", f"test-{MAIN_PYTHON}", ) nox.options.stop_on_first_error = True +@nox_session(name="audit", python=MAIN_PYTHON, reuse_venv=False) +def audit_pinned_dependencies(session: nox.Session) -> None: + """Check dependencies for vulnerabilities with `pip-audit`. + + The dependencies are those defined in the "poetry.lock" file. + + `pip-audit` uses the Python Packaging Advisory Database + (Source: https://github.com/pypa/advisory-database). + """ + do_not_reuse(session) + start(session) + + install_unpinned(session, "pip-audit") + + session.run("pip-audit", "--version") + suppress_poetry_export_warning(session) + with tempfile.NamedTemporaryFile() as requirements_txt: + session.run( + "poetry", + "export", + "--format=requirements.txt", + f"--output={requirements_txt.name}", + "--with=dev", + external=True, + ) + session.run( + "pip-audit", + f"--requirement={requirements_txt.name}", + "--local", + "--progress-spinner=off", + "--strict", + ) + + +@nox_session(name="audit-updates", python=MAIN_PYTHON, reuse_venv=False) +def audit_unpinned_dependencies(session: nox.Session) -> None: + """Check updates for dependencies with `pip-audit`. + + Convenience task to check dependencies before updating + them in the "poetry.lock" file. + + Uses `pip` to resolve the dependencies declared in the + "pyproject.toml" file (incl. the "dev" group) to their + latest PyPI version. + """ + do_not_reuse(session) + start(session) + + pyproject = load_pyproject_toml() + poetry_config = pyproject["tool"]["poetry"] + + dependencies = { + *(poetry_config["dependencies"].keys()), + *(poetry_config["group"]["dev"]["dependencies"].keys()), + } + dependencies.discard("python") # Python itself cannot be installed f> + + install_unpinned(session, "pip-audit", *sorted(dependencies)) + session.run("pip-audit", "--version") + session.run( + "pip-audit", + "--local", + "--progress-spinner=off", + "--strict", + ) + + @nox_session(name="format", python=MAIN_PYTHON) def format_(session: nox.Session) -> None: """Format source files with `autoflake`, `black`, and `isort`.""" @@ -266,6 +334,22 @@ def start(session: nox.Session) -> None: session.env["PIP_DISABLE_PIP_VERSION_CHECK"] = "true" +def suppress_poetry_export_warning(session: nox.Session) -> None: + """Temporary fix to avoid poetry's warning ... + + ... about "poetry-plugin-export not being installed in the future". + """ + session.run( + "poetry", + "config", + "--local", + "warnings.export", + "false", + external=True, + log=False, # because it's just a fix we don't want any message in the logs + ) + + def install_pinned( session: nox.Session, *packages_or_pip_args: str, @@ -279,15 +363,7 @@ def install_pinned( """ session.debug("Install packages respecting the poetry.lock file") - session.run( # temporary fix to avoid poetry's future warning - "poetry", - "config", - "--local", - "warnings.export", - "false", - external=True, - log=False, # because it's just a fix - ) + suppress_poetry_export_warning(session) if nox_poetry_available: session.install(*packages_or_pip_args, **kwargs) From c07a9ed19f8d7fb87a54f0969fbd7f9d75f65cf1 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 02:09:09 +0200 Subject: [PATCH 13/26] Set up a documentation tool - use sphinx to document the developed package - create nox session "docs" to build the docs --- docs/conf.py | 15 ++ docs/index.rst | 25 ++ docs/license.rst | 4 + docs/reference.rst | 6 + noxfile.py | 21 +- poetry.lock | 527 +++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 10 + src/lalib/__init__.py | 2 + 8 files changed, 608 insertions(+), 2 deletions(-) create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/license.rst create mode 100644 docs/reference.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..9e117e3 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,15 @@ +"""Configure sphinx.""" + +import lalib + + +project = lalib.__pkg_name__ +author = lalib.__author__ +project_copyright = f"2024, {author}" +version = release = lalib.__version__ + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx_autodoc_typehints", +] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..465f50f --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,25 @@ +lalib - A Python library to study linear algebra +================================================ + +.. toctree:: + :hidden: + :maxdepth: 1 + + license + reference + + +The goal of this package is to provide +a library written in pure `Python`_ (incl. the `standard library`_) +to learn about linear algebra by reading and writing code. + + +Prerequisites +------------- + +Python 3.9 or newer is needed. +The package depends only on core Python (incl. the standard library). + + +.. _standard library: https://docs.python.org/3/library/index.html +.. _python: https://docs.python.org/3/ diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 0000000..8f93aa7 --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,4 @@ +License +======= + +.. include:: ../LICENSE.txt diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 0000000..066d730 --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,6 @@ +Reference +========= + +.. contents:: + :local: + :backlinks: none diff --git a/noxfile.py b/noxfile.py index 3ca3e2a..5699fa8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -67,8 +67,9 @@ def load_supported_python_versions(*, reverse: bool = False) -> list[str]: SUPPORTED_PYTHONS = load_supported_python_versions(reverse=True) MAIN_PYTHON = "3.12" +DOCS_SRC, DOCS_BUILD = ("docs/", ".cache/docs/") TESTS_LOCATION = "tests/" -SRC_LOCATIONS = ("./noxfile.py", "src/", TESTS_LOCATION) +SRC_LOCATIONS = ("./noxfile.py", "src/", DOCS_SRC, TESTS_LOCATION) nox.options.envdir = ".cache/nox" @@ -78,6 +79,7 @@ nox.options.sessions = ( # run by default when invoking `nox` on the CLI "format", "lint", "audit", + "docs", "test-docstrings", f"test-{MAIN_PYTHON}", ) @@ -151,6 +153,23 @@ def audit_unpinned_dependencies(session: nox.Session) -> None: ) +@nox_session(python=MAIN_PYTHON) +def docs(session: nox.Session) -> None: + """Build the documentation with `sphinx`.""" + start(session) + + # The documentation tools require the developed package as + # otherwise sphinx's autodoc could not include the docstrings + session.debug("Install only the `lalib` package and the documentation tools") + install_unpinned(session, "-e", ".") # editable to be able to reuse the session + install_pinned(session, "sphinx", "sphinx-autodoc-typehints") + + session.run("sphinx-build", "--builder=html", DOCS_SRC, DOCS_BUILD) + session.run("sphinx-build", "--builder=linkcheck", DOCS_SRC, DOCS_BUILD) # > 200 OK + + session.log(f"Docs are available at {DOCS_BUILD}index.html") + + @nox_session(name="format", python=MAIN_PYTHON) def format_(session: nox.Session) -> None: """Format source files with `autoflake`, `black`, and `isort`.""" diff --git a/poetry.lock b/poetry.lock index f39a1f3..06e42f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,25 @@ +[[package]] +name = "alabaster" +version = "0.7.16" +description = "A light, configurable Sphinx theme" +optional = false +python-versions = ">=3.9" +files = [ + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +description = "A light, configurable Sphinx theme" +optional = false +python-versions = ">=3.10" +files = [ + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, +] + [[package]] name = "attrs" version = "24.2.0" @@ -32,6 +54,20 @@ files = [ pyflakes = ">=3.0.0" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} +[[package]] +name = "babel" +version = "2.16.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + [[package]] name = "bandit" version = "1.7.9" @@ -102,6 +138,116 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "click" version = "8.1.7" @@ -225,6 +371,17 @@ files = [ {file = "docstring_parser_fork-0.0.9.tar.gz", hash = "sha256:95b23cc5092af85080c716a6da68360f5ae4fcffa75f4a3aca5e539783cbcc3d"}, ] +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + [[package]] name = "eradicate" version = "2.3.0" @@ -509,6 +666,47 @@ files = [ [package.dependencies] flake8 = "*" +[[package]] +name = "idna" +version = "3.8" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, + {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "importlib-metadata" +version = "8.4.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, + {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -534,6 +732,23 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -558,6 +773,75 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -891,6 +1175,27 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "rich" version = "13.8.0" @@ -978,6 +1283,190 @@ files = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] +[[package]] +name = "sphinx" +version = "7.4.7" +description = "Python documentation generator" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, + {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, +] + +[package.dependencies] +alabaster = ">=0.7.14,<0.8.0" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] + +[[package]] +name = "sphinx" +version = "8.0.2" +description = "Python documentation generator" +optional = false +python-versions = ">=3.10" +files = [ + {file = "sphinx-8.0.2-py3-none-any.whl", hash = "sha256:56173572ae6c1b9a38911786e206a110c9749116745873feae4f9ce88e59391d"}, + {file = "sphinx-8.0.2.tar.gz", hash = "sha256:0cce1ddcc4fd3532cf1dd283bc7d886758362c5c1de6598696579ce96d8ffa5b"}, +] + +[package.dependencies] +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=6.0)", "mypy (==1.11.0)", "pytest (>=6.0)", "ruff (==0.5.5)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240520)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20240724)", "types-requests (>=2.30.0)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "2.3.0" +description = "Type hints (PEP 484) support for the Sphinx autodoc extension" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinx_autodoc_typehints-2.3.0-py3-none-any.whl", hash = "sha256:3098e2c6d0ba99eacd013eb06861acc9b51c6e595be86ab05c08ee5506ac0c67"}, + {file = "sphinx_autodoc_typehints-2.3.0.tar.gz", hash = "sha256:535c78ed2d6a1bad393ba9f3dfa2602cf424e2631ee207263e07874c38fde084"}, +] + +[package.dependencies] +sphinx = ">=7.3.5" + +[package.extras] +docs = ["furo (>=2024.1.29)"] +numpy = ["nptyping (>=2.5)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.4.4)", "defusedxml (>=0.7.1)", "diff-cover (>=9)", "pytest (>=8.1.1)", "pytest-cov (>=5)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.11)"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + [[package]] name = "stevedore" version = "5.3.0" @@ -1014,6 +1503,23 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "xdoctest" version = "1.2.0" @@ -1045,7 +1551,26 @@ tests-binary = ["cmake (>=3.21.2)", "cmake (>=3.25.0)", "ninja (>=1.10.2)", "nin tests-binary-strict = ["cmake (==3.21.2)", "cmake (==3.25.0)", "ninja (==1.10.2)", "ninja (==1.11.1)", "pybind11 (==2.10.3)", "pybind11 (==2.7.1)", "scikit-build (==0.11.1)", "scikit-build (==0.16.1)"] tests-strict = ["pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)"] +[[package]] +name = "zipp" +version = "3.20.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, + {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "41aa1b0224786397f339cd01099c1a687cb57e16898426ef101fd66247d3bd5c" +content-hash = "cc035dcf07b2024900d20f7e2873c3c4c5497e71fa493bf04a629a087f994ec3" diff --git a/pyproject.toml b/pyproject.toml index 9363e64..ac9b78e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,13 @@ pep8-naming = "^0.14" # flake8 plug-in pydoclint = { extras = ["flake8"], version = "^0.5" } ruff = "^0.6" +# Documentation +sphinx = [ + { python = "=3.9", version = "^7.4" }, + { python = ">=3.10", version = "^8.0" }, +] +sphinx-autodoc-typehints = "^2.3" + # Test suite coverage = "^7.6" packaging = "^24.1" # to test the version identifier @@ -376,6 +383,9 @@ known-first-party = ["lalib"] [tool.ruff.lint.per-file-ignores] +# The "docs/" folder is not a package +"docs/conf.py" = ["INP001"] + "tests/*.py" = [ # Linting rules for the test suite: "ANN", # - type hints are not required "S101", # - `assert`s are normal diff --git a/src/lalib/__init__.py b/src/lalib/__init__.py index fb2c190..26b2482 100644 --- a/src/lalib/__init__.py +++ b/src/lalib/__init__.py @@ -13,10 +13,12 @@ try: pkg_info = metadata.metadata(__name__) except metadata.PackageNotFoundError: + __author__ = "unknown" __pkg_name__ = "unknown" __version__ = "unknown" else: + __author__ = pkg_info["author"] __pkg_name__ = pkg_info["name"] __version__ = pkg_info["version"] del pkg_info From 7a5246556a2c9731ed2f62788cd98e48c7cbdc9f Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 02:32:56 +0200 Subject: [PATCH 14/26] Set up pre-commit hooks - add pre-commit hooks: + run `nox -s lint` on staged *.py files + run common pre-commit hooks for validations that could not be achieved with tools in the develop environment so easily - add pre-merge hook: + run `nox -s _pre-commit-test-hook` before merges * ignores the paths to staged files passed in by the pre-commit framework * runs all test cases instead --- .pre-commit-config.yaml | 48 +++++++++++++++++++ README.md | 7 +++ noxfile.py | 38 ++++++++++++++- poetry.lock | 103 +++++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 + 5 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..30976b2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,48 @@ +default_stages: + - commit +fail_fast: true +repos: + - repo: local + hooks: + - id: local-lint + name: Lint the source files + entry: nox -s lint -- + language: system + stages: + - commit + types: + - python + verbose: true + - id: local-test + name: Run the entire test suite + entry: nox -s _pre-commit-test-hook -- + language: system + stages: + - merge-commit + types: + - text + verbose: true + - repo: "https://github.com/pre-commit/pre-commit-hooks" + rev: v4.6.0 + hooks: + - id: check-added-large-files + args: + - "--maxkb=100" + - id: check-builtin-literals + - id: check-case-conflict + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + stages: + - commit + - id: mixed-line-ending + args: + - "--fix=no" + - id: no-commit-to-branch + args: + - "--branch" + - main + - id: trailing-whitespace + stages: + - commit diff --git a/README.md b/README.md index a3b794e..0d2f961 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,13 @@ The second task lints all source code files with [ruff](https://pypi.org/project/ruff/). `flake8` is configured with a couple of plug-ins. +You may want to install the [pre-commit](https://pre-commit.com/) hooks + that come with the project: + +`nox -s pre-commit-install` + +Then, the linting and testing occurs automatically before every commit. + #### Test Suite diff --git a/noxfile.py b/noxfile.py index 5699fa8..1ffb18d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -250,7 +250,10 @@ def test(session: nox.Session) -> None: install_unpinned(session, "-e", ".") # "-e" makes session reuseable install_pinned(session, *TEST_DEPENDENCIES) - args = session.posargs or ( + # If this function is run by the `pre-commit` framework, extra + # arguments are dropped by the hack inside `pre_commit_test_hook()` + posargs = () if session.env.get("_drop_posargs") else session.posargs + args = posargs or ( "--cov", "--no-cov-on-fail", TESTS_LOCATION, @@ -321,6 +324,39 @@ def test_docstrings(session: nox.Session) -> None: session.run("xdoctest", "src/lalib") +@nox_session(name="pre-commit-install", python=MAIN_PYTHON, venv_backend="none") +def pre_commit_install(session: nox.Session) -> None: + """Install `pre-commit` hooks.""" + for type_ in ("pre-commit", "pre-merge-commit"): + session.run( + "poetry", + "run", + "pre-commit", + "install", + f"--hook-type={type_}", + external=True, + ) + + +@nox_session(name="_pre-commit-test-hook", python=MAIN_PYTHON, reuse_venv=False) +def pre_commit_test_hook(session: nox.Session) -> None: + """`pre-commit` hook to run all tests before merges. + + Ignores the paths to the staged files passed in by the + `pre-commit` framework and executes all tests instead. So, + `nox -s _pre-commit-test-hook -- FILE1, ...` drops the "FILE1, ...". + """ + do_not_reuse(session) + + # Little Hack: Create a flag in the env(ironment) ... + session.env["_drop_posargs"] = "true" + + # ... and call `test()` directly because `session.notify()` + # creates the "test" session as a new `nox.Session` object + # that does not have the flag set + test(session) + + def do_not_reuse(session: nox.Session, *, raise_error: bool = True) -> None: """Do not reuse a session with the "-r" flag.""" if session._runner.venv._reused: # noqa:SLF001 diff --git a/poetry.lock b/poetry.lock index 06e42f9..e172dc3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -149,6 +149,17 @@ files = [ {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -360,6 +371,17 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + [[package]] name = "docstring-parser-fork" version = "0.0.9" @@ -407,6 +429,22 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "filelock" +version = "3.16.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"}, + {file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"] +typing = ["typing-extensions (>=4.12.2)"] + [[package]] name = "flake8" version = "7.1.1" @@ -666,6 +704,20 @@ files = [ [package.dependencies] flake8 = "*" +[[package]] +name = "identify" +version = "2.6.0" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.8" @@ -922,6 +974,17 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + [[package]] name = "packaging" version = "24.1" @@ -1000,6 +1063,24 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "3.8.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "pycodestyle" version = "2.12.1" @@ -1520,6 +1601,26 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "virtualenv" +version = "20.26.4" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"}, + {file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [[package]] name = "xdoctest" version = "1.2.0" @@ -1573,4 +1674,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "cc035dcf07b2024900d20f7e2873c3c4c5497e71fa493bf04a629a087f994ec3" +content-hash = "3a34bd29eb4226a6054fe5ddba556605fcd621ceff7661334edf429d715c320f" diff --git a/pyproject.toml b/pyproject.toml index ac9b78e..99cf560 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,8 @@ python = "^3.9" [tool.poetry.group.dev.dependencies] +pre-commit = "^3.8" + # Code formatters autoflake = "^2.3" black = "^24.8" From c6763003dbe007fb583b11ca5124ea2f6d086059 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 02:36:10 +0200 Subject: [PATCH 15/26] Run `nox -s audit` on GitHub actions --- .github/workflows/audit.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/audit.yml diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 0000000..dda4fb9 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,21 @@ +name: audit +on: push +jobs: + audit: + runs-on: ubuntu-latest + name: audit + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.12 + architecture: x64 + + - run: python --version + - run: pip --version + + # The following pinned dependencies must be updated manually + - run: pip install nox==2024.4.15 + - run: pip install poetry==1.8.3 + + - run: nox -s audit From f98491a6b55de428b7024f8d4499d8afa00e4656 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 02:45:58 +0200 Subject: [PATCH 16/26] Run `nox -s docs` on GitHub actions --- .github/workflows/docs.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..da7d9d2 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,21 @@ +name: docs +on: push +jobs: + docs: + runs-on: ubuntu-latest + name: docs + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.12 + architecture: x64 + + - run: python --version + - run: pip --version + + # The following pinned dependencies must be updated manually + - run: pip install nox==2024.4.15 + - run: pip install poetry==1.8.3 + + - run: nox -s docs From 3e3182353e459e4123e68e82b3cf01393751e7f3 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 02:47:46 +0200 Subject: [PATCH 17/26] Run `nox -s lint` on GitHub actions --- .github/workflows/lint.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..318be47 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,21 @@ +name: lint +on: push +jobs: + lint: + runs-on: ubuntu-latest + name: lint + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.12 + architecture: x64 + + - run: python --version + - run: pip --version + + # The following pinned dependencies must be updated manually + - run: pip install nox==2024.4.15 + - run: pip install poetry==1.8.3 + + - run: nox -s lint From 62f7a79abf11f69976f7e7e50a734968a5819937 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 02:50:06 +0200 Subject: [PATCH 18/26] Run `nox -s test` on GitHub actions --- .github/workflows/tests.yml | 24 ++++++++++++++++++++++++ README.md | 1 + 2 files changed, 25 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..8d703e6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,24 @@ +name: tests +on: push +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + name: test-${{ matrix.python-version }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + + - run: python --version + - run: pip --version + + # The following pinned dependencies must be updated manually + - run: pip install nox==2024.4.15 + - run: pip install poetry==1.8.3 + + - run: nox -s test-${{ matrix.python-version }} diff --git a/README.md b/README.md index 0d2f961..2a02e5e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The goal of the `lalib` project is to create by reading and writing code. +[![Test suite: Status](https://github.com/webartifex/lalib/actions/workflows/tests.yml/badge.svg)](https://github.com/webartifex/lalib/actions/workflows/tests.yml) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Type checking: mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) [![Code linting: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) From 9c6cae3593d8f6b39fbf8807233cc532efd649c6 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 02:53:35 +0200 Subject: [PATCH 19/26] Run `nox -s test-coverage` on GitHub actions --- .github/workflows/test_coverage.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/test_coverage.yml diff --git a/.github/workflows/test_coverage.yml b/.github/workflows/test_coverage.yml new file mode 100644 index 0000000..1f80a2c --- /dev/null +++ b/.github/workflows/test_coverage.yml @@ -0,0 +1,29 @@ +name: test-coverage +on: push +jobs: + test-coverage: + runs-on: ubuntu-latest + name: test-coverage + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + # Make all of the below versions available simultaneously + python-version: | + 3.9 + 3.10 + 3.11 + 3.12 + architecture: x64 + + - run: python --version + - run: pip --version + + # The following pinned dependencies must be updated manually + - run: pip install nox==2024.4.15 + - run: pip install poetry==1.8.3 + + # "test-coverage" triggers further nox sessions, + # one for each of the above Python versions, + # before all results are collected together + - run: nox -s test-coverage From 5518837cbc198cdf7d48da5b602dcba93fb7ec3f Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 03:00:56 +0200 Subject: [PATCH 20/26] Run `nox -s test-docstrings` on GitHub actions The xdoctest integration in pytest (see tests/test_docstrings.py) is prone to miss docstrings in new source files as they must be included in the test case explicitly. So, to play it safe, we run the nox session "test-docstrings" on CI. --- .github/workflows/test_docstrings.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/test_docstrings.yml diff --git a/.github/workflows/test_docstrings.yml b/.github/workflows/test_docstrings.yml new file mode 100644 index 0000000..cc55589 --- /dev/null +++ b/.github/workflows/test_docstrings.yml @@ -0,0 +1,21 @@ +name: test-docstrings +on: push +jobs: + test-docstrings: + runs-on: ubuntu-latest + name: test-docstrings + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.12 + architecture: x64 + + - run: python --version + - run: pip --version + + # The following pinned dependencies must be updated manually + - run: pip install nox==2024.4.15 + - run: pip install poetry==1.8.3 + + - run: nox -s test-docstrings From 3f8b5cb14652820fe39440d17cea2899e4be2caa Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 03:10:26 +0200 Subject: [PATCH 21/26] Integrate codecov.io We publish the test coverage reporting to codecov.io --- .github/workflows/test_coverage.yml | 2 ++ README.md | 1 + noxfile.py | 16 +++++++++++++++- pyproject.toml | 5 +++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_coverage.yml b/.github/workflows/test_coverage.yml index 1f80a2c..3a9b498 100644 --- a/.github/workflows/test_coverage.yml +++ b/.github/workflows/test_coverage.yml @@ -27,3 +27,5 @@ jobs: # one for each of the above Python versions, # before all results are collected together - run: nox -s test-coverage + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index 2a02e5e..60da5e3 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The goal of the `lalib` project is to create [![Test suite: Status](https://github.com/webartifex/lalib/actions/workflows/tests.yml/badge.svg)](https://github.com/webartifex/lalib/actions/workflows/tests.yml) +[![Test coverage: codecov](https://codecov.io/github/webartifex/lalib/graph/badge.svg?token=J4LWOMVP0R)](https://codecov.io/github/webartifex/lalib) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Type checking: mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) [![Code linting: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) diff --git a/noxfile.py b/noxfile.py index 1ffb18d..6114c4a 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,6 +1,7 @@ """Maintenance tasks run in isolated environments.""" import collections +import os import pathlib import random import re @@ -311,7 +312,20 @@ def test_coverage_report(session: nox.Session) -> None: install_pinned(session, "coverage") session.run("python", "-m", "coverage", "combine") - session.run("python", "-m", "coverage", "report", "--fail-under=100") + + if codecov_token := os.environ.get("CODECOV_TOKEN"): + install_unpinned(session, "codecov-cli") + session.run("python", "-m", "coverage", "xml", "--fail-under=0") + session.run( + "codecovcli", + "upload-process", + "--fail-on-error", + "--file=.cache/coverage/report.xml", + f"--token={codecov_token}", + ) + + else: + session.run("python", "-m", "coverage", "report", "--fail-under=100") @nox_session(name="test-docstrings", python=MAIN_PYTHON) diff --git a/pyproject.toml b/pyproject.toml index 99cf560..a65fccf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,6 +131,11 @@ parallel = true source = ["lalib"] +[tool.coverage.xml] + +output = ".cache/coverage/report.xml" + + [tool.flake8] From b92d871acf2bd2ad77e8b6d5f2f2ccb8a2eaffaa Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 03:11:59 +0200 Subject: [PATCH 22/26] Integrate readthedocs.io We publish the docs to readthedocs.io --- .readthedocs.yml | 17 +++++++++++++++++ README.md | 1 + docs/requirements.txt | 3 +++ pyproject.toml | 1 + 4 files changed, 22 insertions(+) create mode 100644 .readthedocs.yml create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..78c25f1 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,17 @@ +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.12" + +sphinx: + configuration: docs/conf.py + fail_on_warning: true + +formats: all + +python: + install: + - requirements: docs/requirements.txt + - path: . diff --git a/README.md b/README.md index 60da5e3..ad173c5 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The goal of the `lalib` project is to create by reading and writing code. +[![Documentation: Status](https://readthedocs.org/projects/lalib/badge/?version=latest)](https://lalib.readthedocs.io/en/latest/?badge=latest) [![Test suite: Status](https://github.com/webartifex/lalib/actions/workflows/tests.yml/badge.svg)](https://github.com/webartifex/lalib/actions/workflows/tests.yml) [![Test coverage: codecov](https://codecov.io/github/webartifex/lalib/graph/badge.svg?token=J4LWOMVP0R)](https://codecov.io/github/webartifex/lalib) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..7c766a7 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +# The following pinned dependencies must be updated manually +sphinx==8.0.2 +sphinx-autodoc-typehints==2.3.0 diff --git a/pyproject.toml b/pyproject.toml index a65fccf..cb4bb05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ description = "A Python library to study linear algebra" license = "MIT" readme = "README.md" +documentation = "https://lalib.readthedocs.io" homepage = "https://github.com/webartifex/lalib" repository = "https://github.com/webartifex/lalib" From dd868cce4d65c0bd22d84cb59606d3a83e50b391 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 03:22:52 +0200 Subject: [PATCH 23/26] Run `poetry publish` on GitHub actions --- .github/workflows/release.yml | 33 +++++++++++++++++++++++++++++++++ README.md | 18 +++++++++++++++++- docs/index.rst | 16 ++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..37db62a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: release +on: + release: + types: [published] +jobs: + release: + runs-on: ubuntu-latest + name: release-to-pypi + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + # Make all of the below versions available simultaneously + python-version: | + 3.9 + 3.10 + 3.11 + 3.12 + architecture: x64 + + - run: python --version + - run: pip --version + + # The following pinned dependencies must be updated manually + - run: pip install nox==2024.4.15 + - run: pip install poetry==1.8.3 + + # Run some CI tasks before to ensure code/docs are still good; + # the "test" session is run once for every Pyhthon version above + - run: nox -s audit docs lint test test-docstrings + + - run: poetry build + - run: poetry publish --username=__token__ --password=${{ secrets.PYPI_TOKEN }} diff --git a/README.md b/README.md index ad173c5..3f6e39c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ The goal of the `lalib` project is to create by reading and writing code. +[![PyPI: Package version](https://img.shields.io/pypi/v/lalib?color=blue)](https://pypi.org/project/lalib/) +[![PyPI: Supported Python versions](https://img.shields.io/pypi/pyversions/lalib)](https://pypi.org/project/lalib/) +[![PyPI: Number of monthly downloads](https://img.shields.io/pypi/dm/lalib)](https://pypistats.org/packages/lalib) + [![Documentation: Status](https://readthedocs.org/projects/lalib/badge/?version=latest)](https://lalib.readthedocs.io/en/latest/?badge=latest) [![Test suite: Status](https://github.com/webartifex/lalib/actions/workflows/tests.yml/badge.svg)](https://github.com/webartifex/lalib/actions/workflows/tests.yml) [![Test coverage: codecov](https://codecov.io/github/webartifex/lalib/graph/badge.svg?token=J4LWOMVP0R)](https://codecov.io/github/webartifex/lalib) @@ -16,6 +20,18 @@ The goal of the `lalib` project is to create [![Code linting: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +## Installation + +This project is published on [PyPI](https://pypi.org/project/lalib/). +To install it, open any Python prompt and type: + +`pip install lalib` + +You may want to do so + within a [virtual environment](https://docs.python.org/3/library/venv.html) + or a [Jupyter notebook](https://docs.jupyter.org/en/latest/#what-is-a-notebook). + + ## Contributing & Development This project is open for any kind of contribution, @@ -136,7 +152,7 @@ This ensures that past branches remain visible in the logs, The version identifiers adhere to a subset of the rules in [PEP440](https://peps.python.org/pep-0440/) and follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -So, releases to [PyPI](https://pypi.org/) +So, releases to [PyPI](https://pypi.org/project/lalib/#history) come in the popular `major.minor.patch` format. The specific rules for this project are explained [here](https://github.com/webartifex/lalib/blob/main/tests/test_version.py). diff --git a/docs/index.rst b/docs/index.rst index 465f50f..fc8e1bc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,5 +21,21 @@ Python 3.9 or newer is needed. The package depends only on core Python (incl. the standard library). +Installation +------------ + +`lalib`_ is available on `PyPI`_ via `pip`_: + +.. code-block:: console + + $ pip install lalib + +It is recommended to install the package into a `virtual environment`_. + + .. _standard library: https://docs.python.org/3/library/index.html +.. _lalib: https://github.com/webartifex/lalib +.. _pip: https://pip.pypa.io/en/stable/ +.. _pypi: https://pypi.org/ .. _python: https://docs.python.org/3/ +.. _virtual environment: https://docs.python.org/3/tutorial/venv.html From 97b91c676ead6faffec0f129f69f295a4dbf7015 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 03:26:06 +0200 Subject: [PATCH 24/26] Add a "clean-cwd" task - add nox session "clean-cwd" to clean the working directory - the task is roughly `git clean -X` with minor exceptions --- noxfile.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 6114c4a..9f7cb0b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -6,7 +6,7 @@ import pathlib import random import re import tempfile -from collections.abc import Mapping +from collections.abc import Generator, Mapping from typing import Any import nox @@ -338,6 +338,43 @@ def test_docstrings(session: nox.Session) -> None: session.run("xdoctest", "src/lalib") +@nox_session(name="clean-cwd", python=MAIN_PYTHON, venv_backend="none") +def clean_cwd(session: nox.Session) -> None: + """Remove (almost) all glob patterns listed in git's ignore file. + + Compared to `git clean -X` do not remove pyenv's + ".python-version" file and poetry's virtual environment. + """ + do_not_remove = (".python-version", ".venv") + # Paths are resolved into absolute ones to avoid accidental matches + excluded_paths = {pathlib.Path(path).resolve() for path in do_not_remove} + + with pathlib.Path(".gitignore").open() as fp: + ignored_patterns = [pattern for pattern in fp if not pattern.startswith("#")] + + for path in _expand(*ignored_patterns): + # The `path` must not be a sub-path of an `excluded_path` + if {path, *path.parents} & excluded_paths: + continue + + session.run("rm", "-rf", path) + + +def _expand(*patterns: str) -> Generator[pathlib.Path, None, None]: + """Expand glob patterns into (resolved) paths. + + Args: + *patterns: patterns to be expanded + + Yields: + expanded: an expanded path + """ + for pattern in patterns: + expanded_paths = pathlib.Path.cwd().glob(pattern.strip()) + for path in expanded_paths: + yield path.resolve() + + @nox_session(name="pre-commit-install", python=MAIN_PYTHON, venv_backend="none") def pre_commit_install(session: nox.Session) -> None: """Install `pre-commit` hooks.""" From 0b5cc56925df41da6501878f895403a04f188c96 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 03:28:17 +0200 Subject: [PATCH 25/26] Drop nox-poetry support Whereas nox-poetry is a nice project, it is not (yet) widely supported. The `install_pinned()` function inside the "noxfile.py" achieves most of its functionality. Also, there is a chance poetry will be replaced in this project. So, we no longer drag nox-poetry along. --- noxfile.py | 54 +++++++++++++------------------------------------- pyproject.toml | 1 - 2 files changed, 14 insertions(+), 41 deletions(-) diff --git a/noxfile.py b/noxfile.py index 9f7cb0b..0ce38b7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -13,15 +13,6 @@ import nox from packaging import version as pkg_version -try: - from nox_poetry import session as nox_session -except ImportError: - nox_session = nox.session - nox_poetry_available = False -else: - nox_poetry_available = True - - def nested_defaultdict() -> collections.defaultdict[str, Any]: """Create a multi-level `defaultdict` with variable depth. @@ -87,7 +78,7 @@ nox.options.sessions = ( # run by default when invoking `nox` on the CLI nox.options.stop_on_first_error = True -@nox_session(name="audit", python=MAIN_PYTHON, reuse_venv=False) +@nox.session(name="audit", python=MAIN_PYTHON, reuse_venv=False) def audit_pinned_dependencies(session: nox.Session) -> None: """Check dependencies for vulnerabilities with `pip-audit`. @@ -121,7 +112,7 @@ def audit_pinned_dependencies(session: nox.Session) -> None: ) -@nox_session(name="audit-updates", python=MAIN_PYTHON, reuse_venv=False) +@nox.session(name="audit-updates", python=MAIN_PYTHON, reuse_venv=False) def audit_unpinned_dependencies(session: nox.Session) -> None: """Check updates for dependencies with `pip-audit`. @@ -154,7 +145,7 @@ def audit_unpinned_dependencies(session: nox.Session) -> None: ) -@nox_session(python=MAIN_PYTHON) +@nox.session(python=MAIN_PYTHON) def docs(session: nox.Session) -> None: """Build the documentation with `sphinx`.""" start(session) @@ -171,7 +162,7 @@ def docs(session: nox.Session) -> None: session.log(f"Docs are available at {DOCS_BUILD}index.html") -@nox_session(name="format", python=MAIN_PYTHON) +@nox.session(name="format", python=MAIN_PYTHON) def format_(session: nox.Session) -> None: """Format source files with `autoflake`, `black`, and `isort`.""" start(session) @@ -193,7 +184,7 @@ def format_(session: nox.Session) -> None: session.run("ruff", "check", "--fix-only", *locations) -@nox_session(python=MAIN_PYTHON) +@nox.session(python=MAIN_PYTHON) def lint(session: nox.Session) -> None: """Lint source files with `flake8`, `mypy`, and `ruff`.""" start(session) @@ -243,7 +234,7 @@ TEST_DEPENDENCIES = ( ) -@nox_session(python=SUPPORTED_PYTHONS) +@nox.session(python=SUPPORTED_PYTHONS) def test(session: nox.Session) -> None: """Test code with `pytest`.""" start(session) @@ -266,7 +257,7 @@ def test(session: nox.Session) -> None: _magic_number = random.randint(0, 987654321) # noqa: S311 -@nox_session(name="test-coverage", python=MAIN_PYTHON, reuse_venv=True) +@nox.session(name="test-coverage", python=MAIN_PYTHON, reuse_venv=True) def test_coverage(session: nox.Session) -> None: """Report the combined coverage statistics. @@ -282,7 +273,7 @@ def test_coverage(session: nox.Session) -> None: session.notify("_test-coverage-report", (_magic_number,)) -@nox_session(name="_test-coverage-run", python=SUPPORTED_PYTHONS, reuse_venv=False) +@nox.session(name="_test-coverage-run", python=SUPPORTED_PYTHONS, reuse_venv=False) def test_coverage_run(session: nox.Session) -> None: """Measure the test coverage.""" do_not_reuse(session) @@ -304,7 +295,7 @@ def test_coverage_run(session: nox.Session) -> None: ) -@nox_session(name="_test-coverage-report", python=MAIN_PYTHON, reuse_venv=True) +@nox.session(name="_test-coverage-report", python=MAIN_PYTHON, reuse_venv=True) def test_coverage_report(session: nox.Session) -> None: """Report the combined coverage statistics.""" do_not_run_directly(session) @@ -328,7 +319,7 @@ def test_coverage_report(session: nox.Session) -> None: session.run("python", "-m", "coverage", "report", "--fail-under=100") -@nox_session(name="test-docstrings", python=MAIN_PYTHON) +@nox.session(name="test-docstrings", python=MAIN_PYTHON) def test_docstrings(session: nox.Session) -> None: """Test docstrings with `xdoctest`.""" start(session) @@ -338,7 +329,7 @@ def test_docstrings(session: nox.Session) -> None: session.run("xdoctest", "src/lalib") -@nox_session(name="clean-cwd", python=MAIN_PYTHON, venv_backend="none") +@nox.session(name="clean-cwd", python=MAIN_PYTHON, venv_backend="none") def clean_cwd(session: nox.Session) -> None: """Remove (almost) all glob patterns listed in git's ignore file. @@ -375,7 +366,7 @@ def _expand(*patterns: str) -> Generator[pathlib.Path, None, None]: yield path.resolve() -@nox_session(name="pre-commit-install", python=MAIN_PYTHON, venv_backend="none") +@nox.session(name="pre-commit-install", python=MAIN_PYTHON, venv_backend="none") def pre_commit_install(session: nox.Session) -> None: """Install `pre-commit` hooks.""" for type_ in ("pre-commit", "pre-merge-commit"): @@ -389,7 +380,7 @@ def pre_commit_install(session: nox.Session) -> None: ) -@nox_session(name="_pre-commit-test-hook", python=MAIN_PYTHON, reuse_venv=False) +@nox.session(name="_pre-commit-test-hook", python=MAIN_PYTHON, reuse_venv=False) def pre_commit_test_hook(session: nox.Session) -> None: """`pre-commit` hook to run all tests before merges. @@ -471,10 +462,6 @@ def install_pinned( suppress_poetry_export_warning(session) - if nox_poetry_available: - session.install(*packages_or_pip_args, **kwargs) - return - with tempfile.NamedTemporaryFile() as requirements_txt: session.run( "poetry", @@ -512,20 +499,7 @@ def install_unpinned( if session._runner.global_config.no_install and venv._reused: # noqa: SLF001 return - if kwargs.get("silent") is None: - kwargs["silent"] = True - - # Cannot use `session.install(...)` here because - # with "nox-poetry" installed this leads to an - # installation respecting the "poetry.lock" file - session.run( - "python", - "-m", - "pip", - "install", - *packages_or_pip_args, - **kwargs, - ) + session.install(*packages_or_pip_args, **kwargs) if MAIN_PYTHON not in SUPPORTED_PYTHONS: diff --git a/pyproject.toml b/pyproject.toml index cb4bb05..7bc7e2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -277,7 +277,6 @@ cache_dir = ".cache/mypy" module = [ "nox", - "nox_poetry", "pytest", "semver", "tomli", From fbc9b5f13464dfb2202e8079fd39f90ff10fee2a Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 03:39:30 +0200 Subject: [PATCH 26/26] Prepare release v0.4.2 --- README.md | 12 ++++++++++++ pyproject.toml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f6e39c..890a074 100644 --- a/README.md +++ b/README.md @@ -156,3 +156,15 @@ So, releases to [PyPI](https://pypi.org/project/lalib/#history) come in the popular `major.minor.patch` format. The specific rules for this project are explained [here](https://github.com/webartifex/lalib/blob/main/tests/test_version.py). + + +## Releases + + +### v0.4.2, 2024-09-10 + +- This release provides no functionality +- Its purpose is to (re-)claim the + [lalib](https://pypi.org/project/lalib/) name on PyPI +- We can *not* start with **v0.1.0** + because we already used this when learning to use PyPI years back diff --git a/pyproject.toml b/pyproject.toml index 7bc7e2c..2b1b22e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "lalib" -version = "0.4.2.dev0" +version = "0.4.2" authors = [ "Alexander Hess ",