diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75d722d..2d98564 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: entry: "conform enforce" pass_filenames: false - repo: "https://github.com/trufflesecurity/trufflehog" - rev: "907ac64fd42b18dab2ceba2fda39834d3f8ba7e3" # frozen: v3.90.1 + rev: "a05cf0859455b5b16317ee22d809887a4043cdf0" # frozen: v3.90.2 hooks: - id: "trufflehog" alias: "secrets" diff --git a/.vale.ini b/.vale.ini index c1a9d9f..0237ea7 100644 --- a/.vale.ini +++ b/.vale.ini @@ -15,6 +15,7 @@ RedHat.ReadabilityGrade = NO RedHat.GitLinks = NO Vale.Spelling = NO write-good.Passive = NO +RedHat.TermsErrors = NO [*.md] proselint.Annotations = NO @@ -27,6 +28,8 @@ RedHat.Spacing = NO RedHat.PassiveVoice = NO RedHat.Definitions = NO write-good.E-Prime = NO +# Ignored because of mismatches with regex +RedHat.TermsErrors = NO [{DCO.md,docs/DCO.md,LICENSE.md,docs/LICENSE.md}] BasedOnStyles = Vale diff --git a/README.md b/README.md index 0bd7338..408ddfb 100644 --- a/README.md +++ b/README.md @@ -48,71 +48,232 @@ ______________________________________________________________________ ## Features -__comver__ is a … allowing you to: - -- __Feature 1__: Description of the feature -- __Feature 2__: Description of the feature -- __Feature 3__: Description of the feature -- __Feature 4__: Description of the feature -- __Feature 5__: Description of the feature +__comver__ is a tool for calculating __[semantic versioning](https://semver.org/)__ +of your project __using only commit messages__ - no tags required! + +- __Separation of concerns__: versioning focuses on technical aspects, + not marketing. You can now use tags solely for communication. +- __Highly configurable__: include only relevant commits by filtering via + `message`, `author`, `email`, __or even commit path__. +- __Immutable__: version is calculated directly from the commit history. + Tags can now be used more meaningfully (e.g., to mark a major milestone + or release). +- __Batteries-included__: integrate with [pdm](https://pdm-project.org/en/latest/), + [Hatch](https://hatch.pypa.io/latest/) or [uv](https://docs.astral.sh/uv/). +- __Verifiable__: verify that a specific version was generated from a + given commit chain - more resistant to tampering like + [dependency substitution attacks](https://docs.aws.amazon.com/codeartifact/latest/ug/dependency-substitution-attacks.html) + +## Why? + +Semantic versioning based on Git tags has a few limitations: + +- Teams may avoid bumping the `major` version due to the + perceived weight of the change. + [__Double versioning scheme__](https://open-nudge.github.io/comver/tutorials/why); + one version for technical changes, another for public releases is + a viable mitigation. +- Not all commits are relevant for release versions + (e.g., CI changes, bot updates, or tooling config), + yet many schemes count them in. With filtering, `comver` can exclude + such noise. +- Tags are mutable by default and can be re-pointed. By calculating the version + based on commits, and combining it with the commit + `sha` and a config `checksum`, you get verifiable and reproducible results. ## Quick start +> [!NOTE] +> You can jump straight into the action and check `comver` +> [tutorials](https://open-nudge.github.io/comver/tutorials). + ### Installation ```sh > pip install comver ``` -### Usage +### Calculate version + +> [!IMPORTANT] +> Although written in Python, comver can be used +> with any programming language. + +If your commits follow the Conventional Commits format, run: + +```sh +> comver calculate +``` + +This will output a version string in the `MAJOR.MINOR.PATCH` format: + +```sh +23.1.3 # Output +``` + +> [!IMPORTANT] +> You can use [plugins](https://open-nudge.github.io/comver/tutorials/plugins) +> to integrate this versioning scheme +> with [`pdm`](https://github.com/pdm-project/pdm) or +> [`hatch`](https://github.com/pypa/hatch). More below! + + + +### Configuration + +Configuration can be done either in `pyproject.toml` +(recommended for `Python`-first project) or in a separate +`.comver.toml` file (recommended for non-python projects): + + + + + + + + + + +
pyproject.toml.comver.toml
+ +```toml +[tool.comver] +# Only commits to these paths are considered +path_includes = [ + "src/*", + "pyproject.toml", +] + +# Commits done by GitHub Actions bot are discarded +author_name_excludes = [ + "github-actions[bot]", +] +``` -```python -import comver + + +```toml +# No [tool.comver] needed here +# Source only commits considered +path_includes = [ + "src/*", +] + +# Commits messages with [no version] are discarded +message_excludes = [ + ".*\[no version\].*", + ".*\[skipversion\].*", +] +``` +
+ +> [!TIP] +> See suggested configuration examples [here](https://open-nudge.github.io/comver/tutorials/configuration) + +### Integrations + +> [!NOTE] +> You can use `comver` with [`uv`](https://github.com/astral-sh/uv) +> by selecting the appropriate [build backend](https://docs.astral.sh/uv/concepts/build-backend/#choosing-a-build-backend), +> such as `hatchling`. + +To integrate `comver` with [`pdm`](https://pdm-project.org/en/latest/) +or [`hatch`](https://hatch.pypa.io/latest/) add the following to +your `pyproject.toml`: + + + + + + + + + + +
PDMHatch
+ +```toml +# Register comver for the build process +[build-system] +build-backend = "pdm.backend" + +requires = [ + "pdm-backend", + "comver>=0.1.0", +] + +# Setup versioning for PDM +[tool.pdm.version] +source = "call" +getter = "comver.plugin:pdm" + +# Comver-specific settings +[tool.comver] ... ``` -### Examples + + +```toml +# Register comver for the build process +[build-system] +build-backend = "hatchling.build" + +requires = [ + "comver>=0.1.0", + "hatchling", +] -
- Short (click me) -  +# Setup versioning for Hatchling +[tool.hatch.version] +source = "comver" -Description of the example -```python -# Short example +# Comver-specific settings +[tool.comver] +... ``` -
+
+ +> [!TIP] +> See more in the [documentation](https://open-nudge.github.io/comver/tutorials/plugins) -
- Common (click me) -  +### Verification -Description of the example +To verify that a version was produced from the same Git tree and configuration, +first use the calculate command with additional flags: -```python -# Common use case +```sh +comver calculate --sha --checksum ``` -
+This outputs three space-separated values: -
- Advanced (click me) -  +```sh + +``` + +> [!TIP] +> Append `--format=json` for machine-friendly output -Description of the example +Before the next release provide these values to the `comver verify` +to ensure the version was previously generated from the +same codebase and config: -```python -# Something advanced and cool +```sh +comver verify ``` -
+If inconsistencies are found, you'll receive feedback, for example: - +> Provided checksum and the checksum of configuration do not match. - +> [!TIP] +> Explore verification workflows in the [tutorials](https://open-nudge.github.io/comver/tutorials/verification) + + ## Contribute diff --git a/ROADMAP.md b/ROADMAP.md index eaa5719..9f73545 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7,4 +7,11 @@ SPDX-License-Identifier: Apache-2.0 # Roadmap +- Obtaining public opinion +- Creating special `pyproject.toml` treatment + (for example versioning only based on `[project.dependencies]` + subsection, __not the whole file__) +- Design multi-comver approach (multiple releases based + on multiple configurations) + diff --git a/docs/tutorials/.nav.yml b/docs/tutorials/.nav.yml new file mode 100644 index 0000000..8189097 --- /dev/null +++ b/docs/tutorials/.nav.yml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +--- + +ignore: "*README.md" + +nav: + - Why comver?: "why.md" + - Configuration: "configuration.md" + - Plugins: "plugins.md" + - Verification: "verification.md" +... diff --git a/docs/tutorials/configuration.md b/docs/tutorials/configuration.md new file mode 100644 index 0000000..8f109b3 --- /dev/null +++ b/docs/tutorials/configuration.md @@ -0,0 +1,160 @@ + + +# Configuration + +This section describes how to configure `comver`. + +> [!IMPORTANT] +> You can configure `comver` via `.comver.toml` +> or under the `[tool.comver]` section in `pyproject.toml`. + +Example of `pyproject.toml` configuration: + +```toml +[tool.comver] +# Only commits to these paths are considered +path_includes = [ + "src/*", + "pyproject.toml", +] + +# Commits done by GitHub Actions bot are discarded +author_name_excludes = [ + "github-actions[bot]", +] +``` + +Equivalent `.comver.toml` configuration +(no `[tool.comver]` section needed): + +```toml +# Only commits to these paths are considered +path_includes = [ + "src/*", + "pyproject.toml", +] + +# Commits done by GitHub Actions bot are discarded +author_name_excludes = [ + "github-actions[bot]", +] +``` + +## Options + +> [!WARNING] +> All includes and excludes accept regex lists, +> evaluated using Python's [`re.match`](https://docs.python.org/3/library/re.html#re.match) +> (they are interpreted as raw strings, i.e., prefixed with `r`). + +> [!WARNING] +> `excludes` always take precedence over `includes` + +> [!WARNING] +> Conditions are composed via `and` (e.g. has to be a specific +> author `and` contain specific message) + +You can configure the following options: + +- `message_includes`: + List of regex patterns to include commits based on message. + __Default:__ all messages are included. +- `message_excludes`: + List of regex patterns to exclude commits based on message. + __Default:__ no messages are excluded. +- `path_includes`: + Regex list matching changed file paths to include commits. + __Default:__ every commit is included, no matter the changed file(s). +- `path_excludes`: + Regex list matching changed file paths to exclude commits. + __Default:__ no file is excluded based on path. +- `author_name_includes`: + Regex list to include commits based on author name. + __Default:__ commits from all authors are included. +- `author_name_excludes`: + Regex list to exclude commits based on author name. + __Default:__ no commits are excluded based on author. +- `author_email_includes`: + Regex list to include commits based on author email. + __Default:__ commits from all emails are included. +- `author_email_excludes`: + Regex list to exclude commits based on author email. + __Default:__ no commits are excluded based on email. +- `major_regexes`: + List of regex patterns that indicate a MAJOR version bump. + __Default:__ `fix(...)!:`/`feat(...)!:` + or `BREAKING CHANGE` anywhere in the commit message. +- `minor_regexes`: + List of regex patterns that indicate a MINOR version bump. + __Default__: commits starting with `feat(...):` +- `patch_regexes`: + List of regex patterns that indicate a PATCH version bump. + __Default__: commits starting with `fix(...):` +- `unrecognized_message`: + Action to take if the message doesn’t match any + `major`, `minor`, or `patch` patterns. + __Options__: `"ignore"` (default) or `"error"`. + +## Suggested + +This subsection includes example configurations for common use cases. + +> [!IMPORTANT] +> The list is growing. Feel free to [open a pull request](https://github.com/open-nudge/comver/pulls) +> with yours configuration ideas! + +> [!NOTE] +> These examples are modular; feel free to mix and match +> based on your project’s needs. + +## Python package + +For Python packages with a /src/ layout, the following is recommended: + +```toml +[tool.comver] +path_includes = [ + "src/*", + "pyproject.toml", +] +``` + +> [!NOTE] +> Any change to `pyproject.toml` currently affects versioning. +> More granular handling is being considered on the [`ROADMAP`](../ROADMAP.md). + +## Exclude `bot` commits + +To ignore commits made by bots (e.g. GitHub Actions or [`renovatebot`](https://github.com/renovatebot/renovate)): + +```toml +[tool.comver] +author_excludes = [ + ".*\[bot\].*", +] +``` + +> [!NOTE] +> This pattern excludes any author containing `[bot]`, +> which might unintentionally filter out human contributors. + +## Github-style commit skips + +GitHub allows skipping [skipping CI workflows](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/skip-workflow-runs) +with commit annotations like [skip ci]. + +Similarly, comver supports skipping version bumps via: + +```toml +[tool.comver] +message_excludes = [ + ".*\[no version\].*", + ".*\[skip version\].*", + ".*\[version skip\].*", +] +``` diff --git a/docs/tutorials/plugins.md b/docs/tutorials/plugins.md new file mode 100644 index 0000000..659b87a --- /dev/null +++ b/docs/tutorials/plugins.md @@ -0,0 +1,75 @@ + + +# Plugins + +> `comver` includes plugins to simplify integration with popular +> Python packaging tools. Contributions adding support for other +> tools are welcome! + +> [!NOTE] +> Core `comver` configuration is described in the +> [configuration](configuration.md) section. + +## [PDM](https://pdm-project.org/en/latest/) + +To integrate with `PDM`, update your `pyproject.toml` as follows: + +```toml +# Register comver for the build process +[build-system] +build-backend = "pdm.backend" + +requires = [ + "pdm-backend", + "comver>=0.1.0", +] + +# Setup versioning for PDM +[tool.pdm.version] +source = "call" +getter = "comver.plugin:pdm" + +# Comver-specific settings +[tool.comver] +... +``` + +## [Hatch](https://hatch.pypa.io/latest/) + +To integrate with Hatch, edit your `pyproject.toml` as follows: + +```toml +# Register comver for the build process +[build-system] +build-backend = "hatchling.build" + +requires = [ + "comver>=0.1.0", + "hatchling", +] + +# Setup versioning for Hatchling +[tool.hatch.version] +source = "comver" + +# Comver-specific settings +[tool.comver] +... +``` + +> [!NOTE] +> You may alternatively place comver settings under +> `[tool.hatch.version]`, which will take precedence if specified. + +## [uv](https://docs.astral.sh/uv/) + +`uv` may use `hatchling` as its build backend, so the configuration +is identical as for `hatch`. + +See the [`uv` documentation](https://docs.astral.sh/uv/concepts/build-backend/#choosing-a-build-backend) +for details on setting the build backend. diff --git a/docs/tutorials/verification.md b/docs/tutorials/verification.md new file mode 100644 index 0000000..e6d5ed7 --- /dev/null +++ b/docs/tutorials/verification.md @@ -0,0 +1,81 @@ + + +# Verification + +`comver` supports release verification by: + +- Comparing the current configuration to the one used when calculating the version. +- Validating that the commit SHA matches the one from the release. + +## Why verify? + +> This process ensures that __the release being calculated__ is generated +> with __the same configuration__ and __from the same Git tree__ +> as the previous one. + +As a result, you can be confident that neither the Git history nor +the versioning settings have changed since the last release was generated. + +## Obtaining data + +To retrieve the current `version`, commit `SHA`, and the configuration +`checksum`, run: + +```sh +comver calculate --sha --checksum +``` + +This command outputs three space-separated values: + +```sh + +``` + +Iti s recommended to store this output (e.g. attach it to the +[GitHub release](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases)) +for later verification. + +To get the output in a machine-readable format, add `--format=json` +(also better for storing): + +```sh +comver calculate --sha --checksum --format=json +``` + +This will return a JSON-formatted result, ideal for automation. + +## Verifying + +To verify a previously published release, run: + +```sh +comver verify +``` + +> [!WARNING] +> comver verify will return a non-zero exit code and an error message +> if any discrepancy is found. + +If you’ve saved the output as a .json file (e.g., `input.json`), +you can automate the verification using the script below (requires jq): + +```sh +#!/bin/bash + +json=$(cat input.json) + +# Parse fields using jq +version=$(echo "$json" | jq -r '.version') +sha=$(echo "$json" | jq -r '.sha') +checksum=$(echo "$json" | jq -r '.checksum') + +# Call baz with the arguments in order: version, sha, checksum +comver verify "${version}" "${sha}" "${checksum}" +``` + +This method is especially useful when running verification in a CI pipeline. diff --git a/docs/tutorials/why.md b/docs/tutorials/why.md new file mode 100644 index 0000000..10ba182 --- /dev/null +++ b/docs/tutorials/why.md @@ -0,0 +1,68 @@ + + +# Why `comver`? + +`comver` aims to address several common issues with pure +[semver](https://semver.org/), as discussed in articles like +[Semantic Versioning Will Not Save You](https://hynek.me/articles/semver-will-not-save-you/) + +> [!NOTE] +> `comver` __is not__ and does __not claim to be__ +> a silver bullet for versioning challenges. Its goal +> is to improve upon current practices where possible. + +See the points below to understand the movitation and how +`comver` might help! + +## Strict `semver` adherence + +Projects that strictly follow `semver` +(e.g. [`setuptools`](https://pypi.org/project/setuptools/) +with over `80` major releases) exhibit: + +- Informative versioning that communicates + breaking changes and new features, but… +- Frequent releases can dilute the perceived + importance of each version to end users + +## Reluctance to version changes + +Conversely, some projects are hesitant to increment versions, especially +the `major` version, due to the perceived impact. This is especially visible +when moving from `0.x.y` to `1.x.y`, which implies stability. + +In some cases, breaking changes may go unacknowledged to avoid triggering +a `major` version bump, as it is often directly associated with +major announcements or formal release cycles. + +## Irrelevant commits for the user + +Large projects often include changes unrelated to the core +functionality such as CI workflows, tooling updates, or formatting adjustments. + +Changes like `fix: unify formatting` __do not__ impact the behavior of the +software but may still influence versioning in traditional schemes. + +## `comver` as an alternative + +`comver` provides a more focused and automated approach to versioning: + +- Enables strict `semver` adherence, with versions calculated + directly from commits +- Supports a "double versioning scheme": + use `comver` for the software version, while maintaining a separate, + tag-oriented versioning scheme (e.g., via [python-semantic-release](https://python-semantic-release.readthedocs.io/en/latest/)) + for broader release messaging +- Allows simplified user-facing (public release) versions (e.g. `1`, `2`, `3`) + while keeping a detailed internal version for software consistency +- Filters out irrelevant commits (e.g., those modifying `.github` workflows) +- Supports separate `comver` configurations for different parts of the project + (e.g., CI, packages, documentation) (__WIP__) + +> [!TIP] +> See [configuration](configuration.md) for practical setup examples. diff --git a/mkdocs.yml b/mkdocs.yml index 2fdc573..f862434 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,7 +41,6 @@ theme: - "navigation.instant" - "navigation.instant.progress" - "navigation.top" - - "toc.integrate" # Search - "search.suggest" - "search.highlight" @@ -100,10 +99,14 @@ plugins: - "src" options: show_bases: true - # show_category_heading: true show_root_full_path: true + show_root_toc_entry: true show_root_members_full_path: true show_object_full_path: true + show_symbol_type_heading: true + show_symbol_type_toc: true + show_source: true + show_docstring_functions: true inherited_members: true members_order: "source" filters: diff --git a/osv-scanner.toml b/osv-scanner.toml index f1cdd4c..4164287 100644 --- a/osv-scanner.toml +++ b/osv-scanner.toml @@ -35,6 +35,12 @@ reason = "Affects SVN, which is not used in this project." id = "GHSA-w596-4wvx-j9j6" reason = "Affects SVN or pytest<7.2.0, which are not used in this project." +[[PackageOverrides]] +ecosystem = "PyPI" +name = "trove-classifiers" +license.ignore = true +reason = "transitive dependency" + [[PackageOverrides]] ecosystem = "PyPI" name = "pytest-asyncio" diff --git a/pdm.lock b/pdm.lock index 9a0f35d..5e81588 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,10 +2,10 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "dev-all", "dev-citation", "dev-code", "dev-commit", "dev-docs", "dev-generation", "dev-github", "dev-ini", "dev-legal", "dev-markdown", "dev-pre-commit", "dev-pyproject", "dev-python", "dev-release", "dev-security", "dev-shell", "dev-tests", "dev-typing", "dev-yaml"] +groups = ["default", "dev-all", "dev-citation", "dev-code", "dev-commit", "dev-docs", "dev-generation", "dev-github", "dev-ini", "dev-legal", "dev-markdown", "dev-pre-commit", "dev-pyproject", "dev-python", "dev-release", "dev-security", "dev-shell", "dev-tests", "dev-typing", "dev-yaml", "hatchling"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:2d0731f70b20364e0a9cacb6734ce7b0df84860047e8eaf7ee4f070d107d8e1c" +content_hash = "sha256:52906b2dc0b7cfaa021b885e6b4f5a0937b173e8af0cfbe122063427c922641c" [[metadata.targets]] requires_python = ">=3.11" @@ -621,16 +621,13 @@ files = [ [[package]] name = "comm" -version = "0.2.2" +version = "0.2.3" requires_python = ">=3.8" summary = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." groups = ["dev-all", "dev-docs"] -dependencies = [ - "traitlets>=4", -] files = [ - {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, - {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, + {file = "comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417"}, + {file = "comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971"}, ] [[package]] @@ -646,58 +643,79 @@ files = [ [[package]] name = "coverage" -version = "7.9.2" +version = "7.10.1" requires_python = ">=3.9" summary = "Code coverage measurement for Python" groups = ["dev-all", "dev-tests", "dev-typing"] files = [ - {file = "coverage-7.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a7a56a2964a9687b6aba5b5ced6971af308ef6f79a91043c05dd4ee3ebc3e9ba"}, - {file = "coverage-7.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123d589f32c11d9be7fe2e66d823a236fe759b0096f5db3fb1b75b2fa414a4fa"}, - {file = "coverage-7.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:333b2e0ca576a7dbd66e85ab402e35c03b0b22f525eed82681c4b866e2e2653a"}, - {file = "coverage-7.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326802760da234baf9f2f85a39e4a4b5861b94f6c8d95251f699e4f73b1835dc"}, - {file = "coverage-7.9.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e7be4cfec248df38ce40968c95d3952fbffd57b400d4b9bb580f28179556d2"}, - {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b4a4cb73b9f2b891c1788711408ef9707666501ba23684387277ededab1097c"}, - {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2c8937fa16c8c9fbbd9f118588756e7bcdc7e16a470766a9aef912dd3f117dbd"}, - {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42da2280c4d30c57a9b578bafd1d4494fa6c056d4c419d9689e66d775539be74"}, - {file = "coverage-7.9.2-cp311-cp311-win32.whl", hash = "sha256:14fa8d3da147f5fdf9d298cacc18791818f3f1a9f542c8958b80c228320e90c6"}, - {file = "coverage-7.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:549cab4892fc82004f9739963163fd3aac7a7b0df430669b75b86d293d2df2a7"}, - {file = "coverage-7.9.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2667a2b913e307f06aa4e5677f01a9746cd08e4b35e14ebcde6420a9ebb4c62"}, - {file = "coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0"}, - {file = "coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3"}, - {file = "coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1"}, - {file = "coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615"}, - {file = "coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b"}, - {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9"}, - {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f"}, - {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d"}, - {file = "coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355"}, - {file = "coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0"}, - {file = "coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b"}, - {file = "coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038"}, - {file = "coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d"}, - {file = "coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3"}, - {file = "coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14"}, - {file = "coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6"}, - {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b"}, - {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d"}, - {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868"}, - {file = "coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a"}, - {file = "coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b"}, - {file = "coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694"}, - {file = "coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5"}, - {file = "coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b"}, - {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3"}, - {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8"}, - {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46"}, - {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584"}, - {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e"}, - {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac"}, - {file = "coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926"}, - {file = "coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd"}, - {file = "coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb"}, - {file = "coverage-7.9.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:8a1166db2fb62473285bcb092f586e081e92656c7dfa8e9f62b4d39d7e6b5050"}, - {file = "coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4"}, - {file = "coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b"}, + {file = "coverage-7.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b45e2f9d5b0b5c1977cb4feb5f594be60eb121106f8900348e29331f553a726f"}, + {file = "coverage-7.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a7a4d74cb0f5e3334f9aa26af7016ddb94fb4bfa11b4a573d8e98ecba8c34f1"}, + {file = "coverage-7.10.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d4b0aab55ad60ead26159ff12b538c85fbab731a5e3411c642b46c3525863437"}, + {file = "coverage-7.10.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dcc93488c9ebd229be6ee1f0d9aad90da97b33ad7e2912f5495804d78a3cd6b7"}, + {file = "coverage-7.10.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa309df995d020f3438407081b51ff527171cca6772b33cf8f85344b8b4b8770"}, + {file = "coverage-7.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cfb8b9d8855c8608f9747602a48ab525b1d320ecf0113994f6df23160af68262"}, + {file = "coverage-7.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:320d86da829b012982b414c7cdda65f5d358d63f764e0e4e54b33097646f39a3"}, + {file = "coverage-7.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dc60ddd483c556590da1d9482a4518292eec36dd0e1e8496966759a1f282bcd0"}, + {file = "coverage-7.10.1-cp311-cp311-win32.whl", hash = "sha256:4fcfe294f95b44e4754da5b58be750396f2b1caca8f9a0e78588e3ef85f8b8be"}, + {file = "coverage-7.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:efa23166da3fe2915f8ab452dde40319ac84dc357f635737174a08dbd912980c"}, + {file = "coverage-7.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:d12b15a8c3759e2bb580ffa423ae54be4f184cf23beffcbd641f4fe6e1584293"}, + {file = "coverage-7.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6b7dc7f0a75a7eaa4584e5843c873c561b12602439d2351ee28c7478186c4da4"}, + {file = "coverage-7.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:607f82389f0ecafc565813aa201a5cade04f897603750028dd660fb01797265e"}, + {file = "coverage-7.10.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f7da31a1ba31f1c1d4d5044b7c5813878adae1f3af8f4052d679cc493c7328f4"}, + {file = "coverage-7.10.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51fe93f3fe4f5d8483d51072fddc65e717a175490804e1942c975a68e04bf97a"}, + {file = "coverage-7.10.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e59d00830da411a1feef6ac828b90bbf74c9b6a8e87b8ca37964925bba76dbe"}, + {file = "coverage-7.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:924563481c27941229cb4e16eefacc35da28563e80791b3ddc5597b062a5c386"}, + {file = "coverage-7.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ca79146ee421b259f8131f153102220b84d1a5e6fb9c8aed13b3badfd1796de6"}, + {file = "coverage-7.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b225a06d227f23f386fdc0eab471506d9e644be699424814acc7d114595495f"}, + {file = "coverage-7.10.1-cp312-cp312-win32.whl", hash = "sha256:5ba9a8770effec5baaaab1567be916c87d8eea0c9ad11253722d86874d885eca"}, + {file = "coverage-7.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:9eb245a8d8dd0ad73b4062135a251ec55086fbc2c42e0eb9725a9b553fba18a3"}, + {file = "coverage-7.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:7718060dd4434cc719803a5e526838a5d66e4efa5dc46d2b25c21965a9c6fcc4"}, + {file = "coverage-7.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebb08d0867c5a25dffa4823377292a0ffd7aaafb218b5d4e2e106378b1061e39"}, + {file = "coverage-7.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f32a95a83c2e17422f67af922a89422cd24c6fa94041f083dd0bb4f6057d0bc7"}, + {file = "coverage-7.10.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c4c746d11c8aba4b9f58ca8bfc6fbfd0da4efe7960ae5540d1a1b13655ee8892"}, + {file = "coverage-7.10.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7f39edd52c23e5c7ed94e0e4bf088928029edf86ef10b95413e5ea670c5e92d7"}, + {file = "coverage-7.10.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab6e19b684981d0cd968906e293d5628e89faacb27977c92f3600b201926b994"}, + {file = "coverage-7.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5121d8cf0eacb16133501455d216bb5f99899ae2f52d394fe45d59229e6611d0"}, + {file = "coverage-7.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df1c742ca6f46a6f6cbcaef9ac694dc2cb1260d30a6a2f5c68c5f5bcfee1cfd7"}, + {file = "coverage-7.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40f9a38676f9c073bf4b9194707aa1eb97dca0e22cc3766d83879d72500132c7"}, + {file = "coverage-7.10.1-cp313-cp313-win32.whl", hash = "sha256:2348631f049e884839553b9974f0821d39241c6ffb01a418efce434f7eba0fe7"}, + {file = "coverage-7.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:4072b31361b0d6d23f750c524f694e1a417c1220a30d3ef02741eed28520c48e"}, + {file = "coverage-7.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:3e31dfb8271937cab9425f19259b1b1d1f556790e98eb266009e7a61d337b6d4"}, + {file = "coverage-7.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1c4f679c6b573a5257af6012f167a45be4c749c9925fd44d5178fd641ad8bf72"}, + {file = "coverage-7.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:871ebe8143da284bd77b84a9136200bd638be253618765d21a1fce71006d94af"}, + {file = "coverage-7.10.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:998c4751dabf7d29b30594af416e4bf5091f11f92a8d88eb1512c7ba136d1ed7"}, + {file = "coverage-7.10.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:780f750a25e7749d0af6b3631759c2c14f45de209f3faaa2398312d1c7a22759"}, + {file = "coverage-7.10.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:590bdba9445df4763bdbebc928d8182f094c1f3947a8dc0fc82ef014dbdd8324"}, + {file = "coverage-7.10.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b2df80cb6a2af86d300e70acb82e9b79dab2c1e6971e44b78dbfc1a1e736b53"}, + {file = "coverage-7.10.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d6a558c2725bfb6337bf57c1cd366c13798bfd3bfc9e3dd1f4a6f6fc95a4605f"}, + {file = "coverage-7.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e6150d167f32f2a54690e572e0a4c90296fb000a18e9b26ab81a6489e24e78dd"}, + {file = "coverage-7.10.1-cp313-cp313t-win32.whl", hash = "sha256:d946a0c067aa88be4a593aad1236493313bafaa27e2a2080bfe88db827972f3c"}, + {file = "coverage-7.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e37c72eaccdd5ed1130c67a92ad38f5b2af66eeff7b0abe29534225db2ef7b18"}, + {file = "coverage-7.10.1-cp313-cp313t-win_arm64.whl", hash = "sha256:89ec0ffc215c590c732918c95cd02b55c7d0f569d76b90bb1a5e78aa340618e4"}, + {file = "coverage-7.10.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:166d89c57e877e93d8827dac32cedae6b0277ca684c6511497311249f35a280c"}, + {file = "coverage-7.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bed4a2341b33cd1a7d9ffc47df4a78ee61d3416d43b4adc9e18b7d266650b83e"}, + {file = "coverage-7.10.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddca1e4f5f4c67980533df01430184c19b5359900e080248bbf4ed6789584d8b"}, + {file = "coverage-7.10.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:37b69226001d8b7de7126cad7366b0778d36777e4d788c66991455ba817c5b41"}, + {file = "coverage-7.10.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2f22102197bcb1722691296f9e589f02b616f874e54a209284dd7b9294b0b7f"}, + {file = "coverage-7.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1e0c768b0f9ac5839dac5cf88992a4bb459e488ee8a1f8489af4cb33b1af00f1"}, + {file = "coverage-7.10.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:991196702d5e0b120a8fef2664e1b9c333a81d36d5f6bcf6b225c0cf8b0451a2"}, + {file = "coverage-7.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae8e59e5f4fd85d6ad34c2bb9d74037b5b11be072b8b7e9986beb11f957573d4"}, + {file = "coverage-7.10.1-cp314-cp314-win32.whl", hash = "sha256:042125c89cf74a074984002e165d61fe0e31c7bd40ebb4bbebf07939b5924613"}, + {file = "coverage-7.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22c3bfe09f7a530e2c94c87ff7af867259c91bef87ed2089cd69b783af7b84e"}, + {file = "coverage-7.10.1-cp314-cp314-win_arm64.whl", hash = "sha256:ee6be07af68d9c4fca4027c70cea0c31a0f1bc9cb464ff3c84a1f916bf82e652"}, + {file = "coverage-7.10.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d24fb3c0c8ff0d517c5ca5de7cf3994a4cd559cde0315201511dbfa7ab528894"}, + {file = "coverage-7.10.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1217a54cfd79be20512a67ca81c7da3f2163f51bbfd188aab91054df012154f5"}, + {file = "coverage-7.10.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:51f30da7a52c009667e02f125737229d7d8044ad84b79db454308033a7808ab2"}, + {file = "coverage-7.10.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ed3718c757c82d920f1c94089066225ca2ad7f00bb904cb72b1c39ebdd906ccb"}, + {file = "coverage-7.10.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc452481e124a819ced0c25412ea2e144269ef2f2534b862d9f6a9dae4bda17b"}, + {file = "coverage-7.10.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9d6f494c307e5cb9b1e052ec1a471060f1dea092c8116e642e7a23e79d9388ea"}, + {file = "coverage-7.10.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fc0e46d86905ddd16b85991f1f4919028092b4e511689bbdaff0876bd8aab3dd"}, + {file = "coverage-7.10.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80b9ccd82e30038b61fc9a692a8dc4801504689651b281ed9109f10cc9fe8b4d"}, + {file = "coverage-7.10.1-cp314-cp314t-win32.whl", hash = "sha256:e58991a2b213417285ec866d3cd32db17a6a88061a985dbb7e8e8f13af429c47"}, + {file = "coverage-7.10.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e88dd71e4ecbc49d9d57d064117462c43f40a21a1383507811cf834a4a620651"}, + {file = "coverage-7.10.1-cp314-cp314t-win_arm64.whl", hash = "sha256:1aadfb06a30c62c2eb82322171fe1f7c288c80ca4156d46af0ca039052814bab"}, + {file = "coverage-7.10.1-py3-none-any.whl", hash = "sha256:fa2a258aa6bf188eb9a8948f7102a83da7c430a0dce918dbd8b60ef8fcb772d7"}, + {file = "coverage-7.10.1.tar.gz", hash = "sha256:ae2b4856f29ddfe827106794f3589949a57da6f0d38ab01e24ec35107979ba57"}, ] [[package]] @@ -745,7 +763,7 @@ files = [ [[package]] name = "cyclonedx-python-lib" -version = "10.4.1" +version = "10.5.0" requires_python = "<4.0,>=3.9" summary = "Python library for CycloneDX" groups = ["dev-all", "dev-release"] @@ -757,26 +775,26 @@ dependencies = [ "typing-extensions<5.0,>=4.6; python_version < \"3.13\"", ] files = [ - {file = "cyclonedx_python_lib-10.4.1-py3-none-any.whl", hash = "sha256:1073d5855cdab02b7367f037c47cb46f4c82cadbc745e063edbb9160290efda2"}, - {file = "cyclonedx_python_lib-10.4.1.tar.gz", hash = "sha256:ee017dee867ffb9b449b955161fd235a7c6245e87a5169998e10a0ce61292efb"}, + {file = "cyclonedx_python_lib-10.5.0-py3-none-any.whl", hash = "sha256:e9531555b751a5cb940b334e510141813f37e774929562d2679dbc4868b56d02"}, + {file = "cyclonedx_python_lib-10.5.0.tar.gz", hash = "sha256:49b8bdeb4c7aeea66e3c83739523ba88a3440fed5fe2c57ac942a9e335cf410a"}, ] [[package]] name = "cyclonedx-python-lib" -version = "10.4.1" +version = "10.5.0" extras = ["validation"] requires_python = "<4.0,>=3.9" summary = "Python library for CycloneDX" groups = ["dev-all", "dev-release"] dependencies = [ - "cyclonedx-python-lib==10.4.1", - "jsonschema[format]<5.0,>=4.18", + "cyclonedx-python-lib==10.5.0", + "jsonschema[format-nongpl]<5.0,>=4.25", "lxml<7,>=4", "referencing>=0.28.4", ] files = [ - {file = "cyclonedx_python_lib-10.4.1-py3-none-any.whl", hash = "sha256:1073d5855cdab02b7367f037c47cb46f4c82cadbc745e063edbb9160290efda2"}, - {file = "cyclonedx_python_lib-10.4.1.tar.gz", hash = "sha256:ee017dee867ffb9b449b955161fd235a7c6245e87a5169998e10a0ce61292efb"}, + {file = "cyclonedx_python_lib-10.5.0-py3-none-any.whl", hash = "sha256:e9531555b751a5cb940b334e510141813f37e774929562d2679dbc4868b56d02"}, + {file = "cyclonedx_python_lib-10.5.0.tar.gz", hash = "sha256:49b8bdeb4c7aeea66e3c83739523ba88a3440fed5fe2c57ac942a9e335cf410a"}, ] [[package]] @@ -1054,21 +1072,21 @@ files = [ [[package]] name = "git-cliff" -version = "2.9.1" +version = "2.10.0" requires_python = ">=3.7" summary = "A highly customizable changelog generator ⛰️" groups = ["dev-all", "dev-release"] files = [ - {file = "git_cliff-2.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:220620392ccd6c12a49f816baaea530c32d96b54fce0ebdb7b688b4ba38a0627"}, - {file = "git_cliff-2.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f0790924301176bcf6cab274d4d8b791ffc6791a6541929c29a2c81a10ca0d82"}, - {file = "git_cliff-2.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74afc8b388f32f4661e81c758218578cd7fef36fcc60ffe035027f8eb7b9f9c6"}, - {file = "git_cliff-2.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cee37c70b0ce28d39ad64bdc8eb9d09e415722fd040f61362c5a8c0a85a5161"}, - {file = "git_cliff-2.9.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:555493177f69b4471996f3a80c1f05c3203789080dde96c7b5a05f05aa3f7c49"}, - {file = "git_cliff-2.9.1-py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:45dfefb7f4589fa73a8ae95bfadb233988f64098a08ecd3724334ae58121e004"}, - {file = "git_cliff-2.9.1-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:067a5112999bdb917dcd9cd4b195f32cb0fcd1a0161035f443c02a3536252727"}, - {file = "git_cliff-2.9.1-py3-none-win32.whl", hash = "sha256:bd15d7a5b6c69546454fd5f89b091e6210fa9c24abc5c0d86c2662b7896ec55f"}, - {file = "git_cliff-2.9.1-py3-none-win_amd64.whl", hash = "sha256:840f02dff8adcaecb3dd76bb01cea947633b71341cf42716965d3611f3f315d9"}, - {file = "git_cliff-2.9.1.tar.gz", hash = "sha256:5b9e69b29f076984ebf88489bddedd45c87f54ad354866e7fc031d87a7686a25"}, + {file = "git_cliff-2.10.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:87eb39c6d2e14685d97329bced8965423d2c6a63a5b3940c67b4897defcf250d"}, + {file = "git_cliff-2.10.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f231ce77134bccc8fd1460322810105305ce40b0ece2afbfa8064c7114f33552"}, + {file = "git_cliff-2.10.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e7569bc299eb0f472d5953f4cccb327d70ddb85b000fa5bc55aa37dc68f512d"}, + {file = "git_cliff-2.10.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc2395a0eba440dabbeb7ecef48ed3467b21a59f50a40a529db762ce46ea9bf9"}, + {file = "git_cliff-2.10.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a723a03a95576daf8fccfc0fd19ac125e78bca59c604cc9dfa4cae1feb241326"}, + {file = "git_cliff-2.10.0-py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:996cd311989f4a98fd65d7aaef0b6e905ebaf3baeb5ccf282e543b975a12bde8"}, + {file = "git_cliff-2.10.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8cff49c8c950e8ee512e5e3256602f39a111a7121017a74e3a75fcad618babc5"}, + {file = "git_cliff-2.10.0-py3-none-win32.whl", hash = "sha256:23b7638ad3b62e3175044331b2faef8a918fdd52e5a222db818dbd5492632299"}, + {file = "git_cliff-2.10.0-py3-none-win_amd64.whl", hash = "sha256:0df87ff65bf484647290bb8bfb1104d5e283945eb44090a9e49ea3c520e8b64d"}, + {file = "git_cliff-2.10.0.tar.gz", hash = "sha256:35a196b879f5b3beb47859bffc24d2c2c8e672db4f1ddfc45c334d742a2a5071"}, ] [[package]] @@ -1076,7 +1094,7 @@ name = "gitdb" version = "4.0.12" requires_python = ">=3.7" summary = "Git Object Database" -groups = ["dev-all", "dev-docs", "dev-markdown"] +groups = ["default", "dev-all", "dev-docs", "dev-markdown"] dependencies = [ "smmap<6,>=3.0.1", ] @@ -1087,17 +1105,17 @@ files = [ [[package]] name = "gitpython" -version = "3.1.44" +version = "3.1.45" requires_python = ">=3.7" summary = "GitPython is a Python library used to interact with Git repositories" -groups = ["dev-all", "dev-docs", "dev-markdown"] +groups = ["default", "dev-all", "dev-docs", "dev-markdown"] dependencies = [ "gitdb<5,>=4.0.1", - "typing-extensions>=3.7.4.3; python_version < \"3.8\"", + "typing-extensions>=3.10.0.2; python_version < \"3.10\"", ] files = [ - {file = "GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110"}, - {file = "gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269"}, + {file = "gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77"}, + {file = "gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c"}, ] [[package]] @@ -1133,7 +1151,7 @@ files = [ [[package]] name = "griffe" -version = "1.7.3" +version = "1.9.0" requires_python = ">=3.9" summary = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." groups = ["dev-all", "dev-docs"] @@ -1141,8 +1159,26 @@ dependencies = [ "colorama>=0.4", ] files = [ - {file = "griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75"}, - {file = "griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b"}, + {file = "griffe-1.9.0-py3-none-any.whl", hash = "sha256:bcf90ee3ad42bbae70a2a490c782fc8e443de9b84aa089d857c278a4e23215fc"}, + {file = "griffe-1.9.0.tar.gz", hash = "sha256:b5531cf45e9b73f0842c2121cc4d4bcbb98a55475e191fc9830e7aef87a920a0"}, +] + +[[package]] +name = "hatchling" +version = "1.27.0" +requires_python = ">=3.8" +summary = "Modern, extensible Python build backend" +groups = ["hatchling"] +dependencies = [ + "packaging>=24.2", + "pathspec>=0.10.1", + "pluggy>=1.0.0", + "tomli>=1.2.2; python_version < \"3.11\"", + "trove-classifiers", +] +files = [ + {file = "hatchling-1.27.0-py3-none-any.whl", hash = "sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b"}, + {file = "hatchling-1.27.0.tar.gz", hash = "sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6"}, ] [[package]] @@ -1156,7 +1192,7 @@ files = [ [[package]] name = "hypothesis" -version = "6.136.0" +version = "6.136.6" requires_python = ">=3.9" summary = "A library for property-based testing" groups = ["dev-all", "dev-tests", "dev-typing"] @@ -1166,8 +1202,8 @@ dependencies = [ "sortedcontainers<3.0.0,>=2.1.0", ] files = [ - {file = "hypothesis-6.136.0-py3-none-any.whl", hash = "sha256:a288672440d94f9cd307e3889c4087d1f8c8b114285fcf07ae415b3c7d4a26ab"}, - {file = "hypothesis-6.136.0.tar.gz", hash = "sha256:1d7f0b61195369c043b90484cb2948a369ff83b3584970586d5c9a6f60641625"}, + {file = "hypothesis-6.136.6-py3-none-any.whl", hash = "sha256:1d6296dde36d42263bd44a084c74e91467e78186676e410167f920aa0374a9e7"}, + {file = "hypothesis-6.136.6.tar.gz", hash = "sha256:2ad2e4f2012be4d41c6515b0628d84d48af6e6c38b4db50840bd9ac0899f5856"}, ] [[package]] @@ -1253,28 +1289,28 @@ files = [ [[package]] name = "ipykernel" -version = "6.29.5" -requires_python = ">=3.8" +version = "6.30.0" +requires_python = ">=3.9" summary = "IPython Kernel for Jupyter" groups = ["dev-all", "dev-docs"] dependencies = [ - "appnope; platform_system == \"Darwin\"", + "appnope>=0.1.2; platform_system == \"Darwin\"", "comm>=0.1.1", "debugpy>=1.6.5", "ipython>=7.23.1", - "jupyter-client>=6.1.12", + "jupyter-client>=8.0.0", "jupyter-core!=5.0.*,>=4.12", "matplotlib-inline>=0.1", - "nest-asyncio", - "packaging", - "psutil", - "pyzmq>=24", - "tornado>=6.1", + "nest-asyncio>=1.4", + "packaging>=22", + "psutil>=5.7", + "pyzmq>=25", + "tornado>=6.2", "traitlets>=5.4.0", ] files = [ - {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, - {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, + {file = "ipykernel-6.30.0-py3-none-any.whl", hash = "sha256:fd2936e55c4a1c2ee8b1e5fa6a372b8eecc0ab1338750dee76f48fa5cca1301e"}, + {file = "ipykernel-6.30.0.tar.gz", hash = "sha256:b7b808ddb2d261aae2df3a26ff3ff810046e6de3dfbc6f7de8c98ea0a6cb632c"}, ] [[package]] @@ -1422,7 +1458,7 @@ files = [ [[package]] name = "jsonschema" version = "4.25.0" -extras = ["format"] +extras = ["format-nongpl"] requires_python = ">=3.9" summary = "An implementation of JSON Schema validation for Python" groups = ["dev-all", "dev-release"] @@ -1433,9 +1469,10 @@ dependencies = [ "jsonpointer>1.13", "jsonschema==4.25.0", "rfc3339-validator", - "rfc3987", + "rfc3986-validator>0.1.0", + "rfc3987-syntax>=1.1.0", "uri-template", - "webcolors>=1.11", + "webcolors>=24.6.0", ] files = [ {file = "jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716"}, @@ -1507,9 +1544,20 @@ files = [ {file = "jupytext-1.17.2.tar.gz", hash = "sha256:772d92898ac1f2ded69106f897b34af48ce4a85c985fa043a378ff5a65455f02"}, ] +[[package]] +name = "lark" +version = "1.2.2" +requires_python = ">=3.8" +summary = "a modern parsing library" +groups = ["dev-all", "dev-release"] +files = [ + {file = "lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c"}, + {file = "lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80"}, +] + [[package]] name = "license-expression" -version = "30.4.3" +version = "30.4.4" requires_python = ">=3.9" summary = "license-expression is a comprehensive utility library to parse, compare, simplify and normalize license expressions (such as SPDX license expressions) using boolean logic." groups = ["dev-all", "dev-legal", "dev-release"] @@ -1517,8 +1565,19 @@ dependencies = [ "boolean-py>=4.0", ] files = [ - {file = "license_expression-30.4.3-py3-none-any.whl", hash = "sha256:fd3db53418133e0eef917606623bc125fbad3d1225ba8d23950999ee87c99280"}, - {file = "license_expression-30.4.3.tar.gz", hash = "sha256:49f439fea91c4d1a642f9f2902b58db1d42396c5e331045f41ce50df9b40b1f2"}, + {file = "license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4"}, + {file = "license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd"}, +] + +[[package]] +name = "loadfig" +version = "0.1.0" +requires_python = ">=3.11" +summary = "One-liner Python pyproject config loader. Lightweight, simple, and VCS-aware with root auto-discovery." +groups = ["default"] +files = [ + {file = "loadfig-0.1.0-py3-none-any.whl", hash = "sha256:3c8d15fbb79888782e1e86e64a937881defd00df89a6f7990044f2f865fc41c0"}, + {file = "loadfig-0.1.0.tar.gz", hash = "sha256:dcf14e9f1daa027b90d4811314f97531de1f00aa3520e477f5025de36afbc158"}, ] [[package]] @@ -2010,7 +2069,7 @@ files = [ [[package]] name = "mkdocs-link-marker" -version = "0.1.3" +version = "0.2.0" requires_python = ">=3.4" summary = "MkDocs plugin for marking external or mail links in your documentation." groups = ["dev-all", "dev-docs"] @@ -2019,8 +2078,8 @@ dependencies = [ "mkdocs>=1.0", ] files = [ - {file = "mkdocs-link-marker-0.1.3.tar.gz", hash = "sha256:6d8760c819a376650675f02c7de82f5fbc0a992e7ed3239955cc01021a1af779"}, - {file = "mkdocs_link_marker-0.1.3-py3-none-any.whl", hash = "sha256:798a6bfbb059ce49a81f724e84306bec8f1d5241aace89e54ae7d9088beeab5b"}, + {file = "mkdocs_link_marker-0.2.0-py3-none-any.whl", hash = "sha256:1a6b92b3bc67349d8d49eb18278a4ad9e1a9c0037f4dcc76bbb9e4aaf969539d"}, + {file = "mkdocs_link_marker-0.2.0.tar.gz", hash = "sha256:8d194fd1666368ecf588f942bcc920071723776ef777961c72b7dc16da7c41a5"}, ] [[package]] @@ -2039,7 +2098,7 @@ files = [ [[package]] name = "mkdocs-material" -version = "9.6.15" +version = "9.6.16" requires_python = ">=3.8" summary = "Documentation that simply works" groups = ["dev-all", "dev-docs"] @@ -2057,8 +2116,8 @@ dependencies = [ "requests~=2.26", ] files = [ - {file = "mkdocs_material-9.6.15-py3-none-any.whl", hash = "sha256:ac969c94d4fe5eb7c924b6d2f43d7db41159ea91553d18a9afc4780c34f2717a"}, - {file = "mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5"}, + {file = "mkdocs_material-9.6.16-py3-none-any.whl", hash = "sha256:8d1a1282b892fe1fdf77bfeb08c485ba3909dd743c9ba69a19a40f637c6ec18c"}, + {file = "mkdocs_material-9.6.16.tar.gz", hash = "sha256:d07011df4a5c02ee0877496d9f1bfc986cfb93d964799b032dd99fe34c0e9d19"}, ] [[package]] @@ -2133,7 +2192,7 @@ files = [ [[package]] name = "mkdocstrings" -version = "0.29.1" +version = "0.30.0" requires_python = ">=3.9" summary = "Automatic documentation from sources, for MkDocs." groups = ["dev-all", "dev-docs"] @@ -2147,8 +2206,8 @@ dependencies = [ "pymdown-extensions>=6.3", ] files = [ - {file = "mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6"}, - {file = "mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42"}, + {file = "mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2"}, + {file = "mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444"}, ] [[package]] @@ -2362,7 +2421,7 @@ files = [ [[package]] name = "nodejs-wheel-binaries" -version = "22.17.0" +version = "22.17.1" requires_python = ">=3.7" summary = "unoffical Node.js package" groups = ["dev-all", "dev-typing"] @@ -2370,15 +2429,15 @@ dependencies = [ "typing-extensions; python_version < \"3.8\"", ] files = [ - {file = "nodejs_wheel_binaries-22.17.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:6545a6f6d2f736d9c9e2eaad7e599b6b5b2d8fd4cbd2a1df0807cbcf51b9d39b"}, - {file = "nodejs_wheel_binaries-22.17.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:4bea5b994dd87c20f8260031ea69a97c3d282e2d4472cc8908636a313a830d00"}, - {file = "nodejs_wheel_binaries-22.17.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:885508615274a22499dd5314759c1cf96ba72de03e6485d73b3e5475e7f12662"}, - {file = "nodejs_wheel_binaries-22.17.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f38ce034a602bcab534d55cbe0390521e73e5dcffdd1c4b34354b932172af2"}, - {file = "nodejs_wheel_binaries-22.17.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5eed087855b644c87001fe04036213193963ccd65e7f89949e9dbe28e7743d9b"}, - {file = "nodejs_wheel_binaries-22.17.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:715f413c81500f0770ea8936ef1fc2529b900da8054cbf6da67cec3ee308dc76"}, - {file = "nodejs_wheel_binaries-22.17.0-py2.py3-none-win_amd64.whl", hash = "sha256:51165630493c8dd4acfe1cae1684b76940c9b03f7f355597d55e2d056a572ddd"}, - {file = "nodejs_wheel_binaries-22.17.0-py2.py3-none-win_arm64.whl", hash = "sha256:fae56d172227671fccb04461d3cd2b26a945c6c7c7fc29edb8618876a39d8b4a"}, - {file = "nodejs_wheel_binaries-22.17.0.tar.gz", hash = "sha256:529142012fb8fd20817ef70e2ef456274df4f49933292e312c8bbc7285af6408"}, + {file = "nodejs_wheel_binaries-22.17.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:1f4d208c0c087a2909b6e9e6e0735da083dc997aa74e9b302703b0798b2faa4c"}, + {file = "nodejs_wheel_binaries-22.17.1-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:457ada98c6e3e03c7fd3f7d6a55572b70af7155c8dd908246373c63697226db6"}, + {file = "nodejs_wheel_binaries-22.17.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79a87aeb2f1dfc3d36cd595921f7671a7342467f8b224b56e9049771e5ec20b"}, + {file = "nodejs_wheel_binaries-22.17.1-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05c4eafec348e439d069cd5a114f893c5f7ea898752e34a8aa43aacd39fcf9a3"}, + {file = "nodejs_wheel_binaries-22.17.1-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fc9690ed95b186ef4bb8dd316e83878d016a0b6072454f8f68fa843c1016f85b"}, + {file = "nodejs_wheel_binaries-22.17.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8200379c4c5ec957230285d2e7844f94de87ec0e8316b72b7a5f923cf19e88f2"}, + {file = "nodejs_wheel_binaries-22.17.1-py2.py3-none-win_amd64.whl", hash = "sha256:dadc1cf0d5dfacb4dbf2f339d8c070c58e48b37328a44f845de1d27fbbf2381f"}, + {file = "nodejs_wheel_binaries-22.17.1-py2.py3-none-win_arm64.whl", hash = "sha256:fde8e767272620c58cb882df104462d8f62859223dbf9ab50678d9f0c09dbbf5"}, + {file = "nodejs_wheel_binaries-22.17.1.tar.gz", hash = "sha256:0a8bf2a9d319988b8fa8b8b7bb5a7d986527672e6d6352735714f768af9828d0"}, ] [[package]] @@ -2543,7 +2602,7 @@ name = "packaging" version = "25.0" requires_python = ">=3.8" summary = "Core utilities for Python packages" -groups = ["dev-all", "dev-docs", "dev-python", "dev-release", "dev-security", "dev-tests", "dev-typing"] +groups = ["dev-all", "dev-docs", "dev-python", "dev-release", "dev-security", "dev-tests", "dev-typing", "hatchling"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -2586,7 +2645,7 @@ name = "pathspec" version = "0.12.1" requires_python = ">=3.8" summary = "Utility library for gitignore style pattern matching of file paths." -groups = ["dev-all", "dev-docs", "dev-yaml"] +groups = ["dev-all", "dev-docs", "dev-yaml", "hatchling"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -2733,7 +2792,7 @@ name = "pluggy" version = "1.6.0" requires_python = ">=3.9" summary = "plugin and hook calling mechanisms for python" -groups = ["dev-all", "dev-tests", "dev-typing"] +groups = ["dev-all", "dev-tests", "dev-typing", "hatchling"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -2924,7 +2983,7 @@ files = [ [[package]] name = "py-serializable" -version = "2.0.0" +version = "2.1.0" requires_python = "<4.0,>=3.8" summary = "Library for serializing and deserializing Python Objects to and from JSON and XML." groups = ["dev-all", "dev-release"] @@ -2932,8 +2991,8 @@ dependencies = [ "defusedxml<0.8.0,>=0.7.1", ] files = [ - {file = "py_serializable-2.0.0-py3-none-any.whl", hash = "sha256:1721e4c0368adeec965c183168da4b912024702f19e15e13f8577098b9a4f8fe"}, - {file = "py_serializable-2.0.0.tar.gz", hash = "sha256:e9e6491dd7d29c31daf1050232b57f9657f9e8a43b867cca1ff204752cf420a5"}, + {file = "py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304"}, + {file = "py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103"}, ] [[package]] @@ -3149,7 +3208,7 @@ files = [ [[package]] name = "pymdown-extensions" -version = "10.16" +version = "10.16.1" requires_python = ">=3.9" summary = "Extension pack for Python Markdown." groups = ["dev-all", "dev-docs"] @@ -3158,8 +3217,8 @@ dependencies = [ "pyyaml", ] files = [ - {file = "pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2"}, - {file = "pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de"}, + {file = "pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d"}, + {file = "pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91"}, ] [[package]] @@ -3558,13 +3617,28 @@ files = [ ] [[package]] -name = "rfc3987" -version = "1.3.8" -summary = "Parsing and validation of URIs (RFC 3986) and IRIs (RFC 3987)" +name = "rfc3986-validator" +version = "0.1.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +summary = "Pure python rfc3986 validator" groups = ["dev-all", "dev-release"] files = [ - {file = "rfc3987-1.3.8-py2.py3-none-any.whl", hash = "sha256:10702b1e51e5658843460b189b185c0366d2cf4cff716f13111b0ea9fd2dce53"}, - {file = "rfc3987-1.3.8.tar.gz", hash = "sha256:d3c4d257a560d544e9826b38bc81db676890c79ab9d7ac92b39c7a253d5ca733"}, + {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, + {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +requires_python = ">=3.9" +summary = "Helper functions to syntactically validate strings according to RFC 3987." +groups = ["dev-all", "dev-release"] +dependencies = [ + "lark>=1.2.2", +] +files = [ + {file = "rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f"}, + {file = "rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d"}, ] [[package]] @@ -3741,29 +3815,29 @@ files = [ [[package]] name = "ruff" -version = "0.12.4" +version = "0.12.5" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." groups = ["dev-all", "dev-python"] files = [ - {file = "ruff-0.12.4-py3-none-linux_armv6l.whl", hash = "sha256:cb0d261dac457ab939aeb247e804125a5d521b21adf27e721895b0d3f83a0d0a"}, - {file = "ruff-0.12.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:55c0f4ca9769408d9b9bac530c30d3e66490bd2beb2d3dae3e4128a1f05c7442"}, - {file = "ruff-0.12.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a8224cc3722c9ad9044da7f89c4c1ec452aef2cfe3904365025dd2f51daeae0e"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9949d01d64fa3672449a51ddb5d7548b33e130240ad418884ee6efa7a229586"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:be0593c69df9ad1465e8a2d10e3defd111fdb62dcd5be23ae2c06da77e8fcffb"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7dea966bcb55d4ecc4cc3270bccb6f87a337326c9dcd3c07d5b97000dbff41c"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afcfa3ab5ab5dd0e1c39bf286d829e042a15e966b3726eea79528e2e24d8371a"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c057ce464b1413c926cdb203a0f858cd52f3e73dcb3270a3318d1630f6395bb3"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64b90d1122dc2713330350626b10d60818930819623abbb56535c6466cce045"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abc48f3d9667fdc74022380b5c745873499ff827393a636f7a59da1515e7c57"}, - {file = "ruff-0.12.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2b2449dc0c138d877d629bea151bee8c0ae3b8e9c43f5fcaafcd0c0d0726b184"}, - {file = "ruff-0.12.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:56e45bb11f625db55f9b70477062e6a1a04d53628eda7784dce6e0f55fd549eb"}, - {file = "ruff-0.12.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:478fccdb82ca148a98a9ff43658944f7ab5ec41c3c49d77cd99d44da019371a1"}, - {file = "ruff-0.12.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0fc426bec2e4e5f4c4f182b9d2ce6a75c85ba9bcdbe5c6f2a74fcb8df437df4b"}, - {file = "ruff-0.12.4-py3-none-win32.whl", hash = "sha256:4de27977827893cdfb1211d42d84bc180fceb7b72471104671c59be37041cf93"}, - {file = "ruff-0.12.4-py3-none-win_amd64.whl", hash = "sha256:fe0b9e9eb23736b453143d72d2ceca5db323963330d5b7859d60d101147d461a"}, - {file = "ruff-0.12.4-py3-none-win_arm64.whl", hash = "sha256:0618ec4442a83ab545e5b71202a5c0ed7791e8471435b94e655b570a5031a98e"}, - {file = "ruff-0.12.4.tar.gz", hash = "sha256:13efa16df6c6eeb7d0f091abae50f58e9522f3843edb40d56ad52a5a4a4b6873"}, + {file = "ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92"}, + {file = "ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a"}, + {file = "ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf"}, + {file = "ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73"}, + {file = "ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac"}, + {file = "ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08"}, + {file = "ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4"}, + {file = "ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b"}, + {file = "ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a"}, + {file = "ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a"}, + {file = "ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5"}, + {file = "ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293"}, + {file = "ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb"}, + {file = "ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb"}, + {file = "ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9"}, + {file = "ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5"}, + {file = "ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805"}, + {file = "ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e"}, ] [[package]] @@ -3779,7 +3853,7 @@ files = [ [[package]] name = "semgrep" -version = "1.128.1" +version = "1.130.0" requires_python = ">=3.9" summary = "Lightweight static analysis for many languages. Find bug variants with patterns that look like source code." groups = ["dev-all", "dev-security"] @@ -3809,12 +3883,12 @@ dependencies = [ "wcmatch~=8.3", ] files = [ - {file = "semgrep-1.128.1-cp39.cp310.cp311.py39.py310.py311-none-macosx_10_14_x86_64.whl", hash = "sha256:1c52c1ed2ebcc60052bccd9b6f8e814189ed54d400323828ca05bc1166840e1a"}, - {file = "semgrep-1.128.1-cp39.cp310.cp311.py39.py310.py311-none-macosx_11_0_arm64.whl", hash = "sha256:858fdb9d64c64a0ab594958f2ff22dd9decce81c49b7c1b0f06f860004a39f69"}, - {file = "semgrep-1.128.1-cp39.cp310.cp311.py39.py310.py311-none-musllinux_1_0_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d78aa08f045ac694ac24d4a39a370ba5974338aaea32a3304a239c328230249"}, - {file = "semgrep-1.128.1-cp39.cp310.cp311.py39.py310.py311-none-musllinux_1_0_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c592629c3a2a5efb3571f82dbc3134aaf64b2404faf95843d4f024d0ed52c13"}, - {file = "semgrep-1.128.1-cp39.cp310.cp311.py39.py310.py311-none-win_amd64.whl", hash = "sha256:b52882bd238f4d25cd518018d962d79e6af352b51b70dfd09ba367f105252350"}, - {file = "semgrep-1.128.1.tar.gz", hash = "sha256:ce51d80c0e667442c05a27d118c197fb278eff33685f0ae30aa4f91f128f0e5f"}, + {file = "semgrep-1.130.0-cp39.cp310.cp311.py39.py310.py311-none-macosx_10_14_x86_64.whl", hash = "sha256:36c791a32276eae6b580e077f11ebabb7e742c9af44286c718d7f306697daa98"}, + {file = "semgrep-1.130.0-cp39.cp310.cp311.py39.py310.py311-none-macosx_11_0_arm64.whl", hash = "sha256:21725b2c6adef4af66350c3ff63196e396463722cf3c14839590be0560840ebb"}, + {file = "semgrep-1.130.0-cp39.cp310.cp311.py39.py310.py311-none-musllinux_1_0_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ece9d5406ab75fc08e81abd8b6a01b488d58dcbca5c719f3e0cf2c6b6b45e1d"}, + {file = "semgrep-1.130.0-cp39.cp310.cp311.py39.py310.py311-none-musllinux_1_0_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67628b6882ade3c3004420f6a5ff4bbd0865501963b97581e7738f572448155"}, + {file = "semgrep-1.130.0-cp39.cp310.cp311.py39.py310.py311-none-win_amd64.whl", hash = "sha256:fc42554a30bdd863ee267a0b12178ec9133a3c5988ab2f199684bd44a67cfa17"}, + {file = "semgrep-1.130.0.tar.gz", hash = "sha256:deeffec007e3961de9c175e86689e5f926c8272d0b721808a71cf7396d25e9da"}, ] [[package]] @@ -3859,7 +3933,7 @@ name = "smmap" version = "5.0.2" requires_python = ">=3.7" summary = "A pure Python implementation of a sliding window memory map manager" -groups = ["dev-all", "dev-docs", "dev-markdown"] +groups = ["default", "dev-all", "dev-docs", "dev-markdown"] files = [ {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, @@ -4027,6 +4101,16 @@ files = [ {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, ] +[[package]] +name = "trove-classifiers" +version = "2025.5.9.12" +summary = "Canonical source for classifiers on PyPI (pypi.org)." +groups = ["hatchling"] +files = [ + {file = "trove_classifiers-2025.5.9.12-py3-none-any.whl", hash = "sha256:e381c05537adac78881c8fa345fd0e9970159f4e4a04fcc42cfd3129cca640ce"}, + {file = "trove_classifiers-2025.5.9.12.tar.gz", hash = "sha256:7ca7c8a7a76e2cd314468c677c69d12cc2357711fcab4a60f87994c1589e5cb5"}, +] + [[package]] name = "types-python-dateutil" version = "2.9.0.20250708" @@ -4122,7 +4206,7 @@ files = [ [[package]] name = "virtualenv" -version = "20.31.2" +version = "20.32.0" requires_python = ">=3.8" summary = "Virtual Python Environment builder" groups = ["dev-all", "dev-pre-commit"] @@ -4133,8 +4217,8 @@ dependencies = [ "platformdirs<5,>=3.9.1", ] files = [ - {file = "virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11"}, - {file = "virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"}, + {file = "virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56"}, + {file = "virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 6b84eaf..e1c6794 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,12 @@ dynamic = [ "version" ] ############################################################################### dependencies = [ + "loadfig>=0.1.0", + "GitPython>=3", +] + +optional-dependencies.hatchling = [ + "hatchling>=1.27.0", ] urls.Changelog = "https://github.com/open-nudge/comver/blob/master/CHANGELOG.md" @@ -79,6 +85,9 @@ urls.Homepage = "https://open-nudge.github.io/comver" urls.Issues = "https://github.com/open-nudge/comver/issues" urls.Repository = "https://github.com/open-nudge/comver" +[project.scripts] +comver = "comver._cli:main" + [dependency-groups] dev-tests = [ # Fuzz/property testing for Python @@ -194,7 +203,7 @@ dev-legal = [ dev-release = [ "spdx-tools>=0.8.3", - "cyclonedx-bom>=4", + "cyclonedx-bom>=7", "git-cliff>=2", "reuse>=4", ] @@ -488,6 +497,10 @@ exclude = [ ] [tool.basedpyright] +executionEnvironments = [ + { root = "src", pythonVersion = "3.11" } +] + exclude = [ "**/node_modules", "**/__pycache__", @@ -529,6 +542,11 @@ reportMissingTypeStubs = "none" # A lot of false positives reportPrivateLocalImportUsage = "none" reportPrivateImportUsage = "none" +# Checked by other linters better +reportPrivateUsage = "none" +# Highlights non-issues in many cases and +# hard to turn off file by file +reportImportCycles = "none" extraPaths = [ # @@ -797,14 +815,12 @@ sort_commits = "oldest" ############################################################################### tests = { composite = [ + "pdm run refresh", "pdm run coverage run -m pytest -x --pretty {args:tests}", "pdm run coverage report", ] } tests-all = { composite = [ - "setup-environment", - "pdm install --prod -G:all --no-editable", - "pdm install -G dev-tests --no-editable", # # DO NOT EDIT UNTIL end marker # @@ -1131,7 +1147,12 @@ fix-all = { composite = [ lock = "pdm lock -G:all" -setup-environment = { composite = [ +refresh = { composite = [ + "pdm install --prod -G:all --no-editable", + "pdm install -G dev-tests --no-editable", +] } + +setup = { composite = [ "pdm self update", # # DO NOT EDIT UNTIL end marker @@ -1158,16 +1179,13 @@ setup-environment = { composite = [ # for version in cogeol.scientific()[:-1]: # cycle = version["cycle"] # cog.outl(f' "pdm use {cycle}",') - # cog.outl(f' "pdm install --prod -G:all --no-editable",') - # cog.outl(f' "pdm install -G dev-tests --no-editable",') + # cog.outl(f' "pdm run refresh",') # ]]] "pdm use 3.13", - "pdm install --prod -G:all --no-editable", - "pdm install -G dev-tests --no-editable", + "pdm run refresh", "pdm use 3.12", - "pdm install --prod -G:all --no-editable", - "pdm install -G dev-tests --no-editable", - # [[[end]]] (sum: S4abFf8hbc) + "pdm run refresh", + # [[[end]]] (sum: 7Y0OJ5MOhi) # # DO NOT EDIT UNTIL end marker # @@ -1180,10 +1198,6 @@ setup-environment = { composite = [ # ]]] "pdm use 3.11", # [[[end]]] (sum: aXoMTrp8k2) -] } - -setup = { composite = [ - "setup-environment", "pdm install -G:all --no-editable", "pdm run pre-commit install --install-hooks", ] } @@ -1206,8 +1220,7 @@ pre-commit-all = "pre-commit run {args:''} --all-files" sbom-python = { shell = """ bash -c \ 'pdm run cyclonedx-py environment $(pdm info --python) \ - --PEP-639 --mc-type library \ - --schema-version 1.6' + --mc-type library' """ } sbom-legal = { shell = """ diff --git a/src/comver/__init__.py b/src/comver/__init__.py index 710aecd..7cb3a05 100644 --- a/src/comver/__init__.py +++ b/src/comver/__init__.py @@ -3,17 +3,31 @@ # # SPDX-License-Identifier: Apache-2.0 -"""Official documentation.""" +"""comver internal API reference. + +This section of documentation should be mainly considered by people who: + +- are curious how the project works under the hood +- want to provide integration of `comver` with third part tooling. + +Important: + Check out guidelines and tutorials for information about CLI/plugin + as this is a more common starting point. + +""" from __future__ import annotations -from importlib.metadata import version +from comver import error, plugin, type_definitions +from comver._version import Version, _version -__version__ = version("comver") +__version__ = _version """Current comver version.""" -del version - __all__: list[str] = [ + "Version", "__version__", + "error", + "plugin", + "type_definitions", ] diff --git a/src/comver/__main__.py b/src/comver/__main__.py new file mode 100644 index 0000000..ed90293 --- /dev/null +++ b/src/comver/__main__.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""The `python -m comver` entrypoint.""" + +from __future__ import annotations # pragma: no cover + +if __name__ == "__main__": # pragma: no cover + from comver._cli import main + + main() diff --git a/src/comver/_cli.py b/src/comver/_cli.py new file mode 100644 index 0000000..845f7a7 --- /dev/null +++ b/src/comver/_cli.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""CLI entrypoint of comver.""" + +from __future__ import annotations + +import sys + +from comver import _parser, _subcommand + + +def main(args: list[str] | None = None) -> None: + """Command-line entry point of the `comver`. + + Parses arguments and dispatches execution to the appropriate subcommand + based on user input. If no arguments are provided explicitly, the arguments + from `sys.argv[1:]` are used instead. + + Args: + args: + CLI arguments passed, if any (used mainly during testing). + + """ + parsed_args = _parser.root().parse_args(args) + subcommand = getattr(_subcommand, parsed_args.subcommand, None) + + # Cannot be `None`, but left to make pyright feel at peace + if subcommand is None: # pragma: no cover + print( # noqa: T201 + "Unknown command chosen", + file=sys.stderr, + ) + sys.exit(1) + + subcommand(parsed_args) diff --git a/src/comver/_parser.py b/src/comver/_parser.py new file mode 100644 index 0000000..bb800e5 --- /dev/null +++ b/src/comver/_parser.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Parser of the `comver` CLI.""" + +from __future__ import annotations + +import argparse +import textwrap + +from comver._version import _version + + +def root() -> argparse.ArgumentParser: + """Create and return the top-level CLI parser. + + This parser defines all available subcommands and any global flags. + Each subcommand is parsed with its corresponding arguments and stored + in the `subcommand` attribute for later dispatching. + + Returns: + The argument parser configured with all CLI subcommands. + + """ + parser = argparse.ArgumentParser( + description="Tool CLI with support for subcommands.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + _ = parser.add_argument( + "--version", + action="version", + version=_version, + help="Show the tool version and exit.", + ) + + subparsers = parser.add_subparsers( + dest="subcommand", + required=True, + ) + _calculate(subparsers) + _verify(subparsers) + + return parser + + +def _calculate(subparsers) -> None: # noqa: ANN001 # pyright: ignore [reportUnknownParameterType, reportMissingParameterType] + """Create `calculate` subcommand subparser. + + Args: + subparsers: + Object where this subparser is registered. + + """ + parser = subparsers.add_parser( + "calculate", + description=textwrap.dedent("""\ + Calculate semantic version based on commits. + + NOTE: + + - This command runs on the git-tree found in current + working directory. + - Each value (e.g. version and sha) is space separated + """), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument( + "--format", + choices=["line", "json"], + default="line", + help="Format of the output (default: line, each output space separated)", + ) + + parser.add_argument( + "--sha", + action="store_true", + required=False, + help=( + "Return sha of the commit related to the version (usable for verification)" + ), + ) + + parser.add_argument( + "--checksum", + action="store_true", + required=False, + help="Return checksum of the configuration (usable for verification)", + ) + + +def _verify(subparsers) -> None: # noqa: ANN001 # pyright: ignore [reportUnknownParameterType, reportMissingParameterType] + """Create `verify` subcommand subparser. + + Args: + subparsers: + Object where this subparser is registered. + + """ + parser = subparsers.add_parser( + "verify", + description=textwrap.dedent(""" + Verify version consistency. + + NOTE: + + - This command runs on the git-tree found in current + working directory. + - You can feed the output of the `calculate` command here + """), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument( + "version", + help="Version to check (e.g. `1.37.21`).", + ) + + parser.add_argument( + "sha", + help="Sha of the commit to compare against.", + ) + + parser.add_argument( + "checksum", + help="Checksum of the configuration to check against.", + ) + + return parser diff --git a/src/comver/_regex/__init__.py b/src/comver/_regex/__init__.py new file mode 100644 index 0000000..37552c4 --- /dev/null +++ b/src/comver/_regex/__init__.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Regex related internal functionalities.""" + +from __future__ import annotations + +from comver._regex import match, semantic +from comver._regex._process import process + +__all__ = [ + "match", + "process", + "semantic", +] diff --git a/src/comver/_regex/_process.py b/src/comver/_regex/_process.py new file mode 100644 index 0000000..a159be4 --- /dev/null +++ b/src/comver/_regex/_process.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Compilation/performance related regex functionalities.""" + +from __future__ import annotations + +import re +import typing + +if typing.TYPE_CHECKING: + from comver.type_definitions import OptionalStringsOrPatterns + + +def process( + regexes: OptionalStringsOrPatterns, default: str | None = None +) -> re.Pattern[str] | None: + """Cached compilation of multiple regexes. + + > [!IMPORTANT] + > `compile` should use internal cache, see + > here: https://github.com/python/cpython/blob/v3.12.0/Lib/re/__init__.py#L271-L329 + + `None` is used, as the regex matching might be slow + and is avoided whenever possible throughout + the whole `comver`. + + Arguments: + regexes: + Regexes which should be compiled together + (if provided). + default: + Default regex, if provided. + """ + if not regexes: + if default is None: + return None + return re.compile(default) + return re.compile(r"(" + r"|".join(rf"({r})" for r in regexes) + r")") diff --git a/src/comver/_regex/match.py b/src/comver/_regex/match.py new file mode 100644 index 0000000..3b65e71 --- /dev/null +++ b/src/comver/_regex/match.py @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: © 2025 nosludge +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Regex matching functions.""" + +from __future__ import annotations + +import typing + +if typing.TYPE_CHECKING: + import re + + import git + + +def item( + what: str, include: re.Pattern[str] | None, exclude: re.Pattern[str] | None +) -> bool: + """Check if the `what` is included based on the `include` and `exclude`. + + Note: + Empty `include` are treated as include-everything. + + Note: + Empty `exclude` are treated as include-everything. + + Warning: + Exclude regexes take precedence over include regexes. + + Args: + what: + The string to check. + include: + The regex to include the string. + exclude: + The regex to exclude. + + Returns: + True if the `what` is included, False otherwise. + + """ + return (include is None or include.search(what) is not None) and ( + exclude is None or exclude.search(what) is None + ) + + +def path( + commit: git.Commit, + include: re.Pattern[str] | None, + exclude: re.Pattern[str] | None, +) -> bool: + """Check if the commit touched the file based regexes. + + Note: + Empty `include` are treated as include-everything. + + Note: + Empty `exclude` are treated as include-everything. + + Warning: + Exclude regexes take precedence over include regexes. + + Args: + commit: + The commit to check. + include: + The regex to include the file. + exclude: + The regex to exclude the file. + + Returns: + True if the commit touched the file, False otherwise. + """ + if include is None and exclude is None: + return True + + return not commit.diff() or any( + item(diff.a_path, include, exclude) if diff.a_path else True + for diff in commit.diff() + ) diff --git a/src/comver/_regex/semantic.py b/src/comver/_regex/semantic.py new file mode 100644 index 0000000..04276d4 --- /dev/null +++ b/src/comver/_regex/semantic.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: © 2025 nosludge +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Semantic versioning related regexes.""" + +from __future__ import annotations + +import re +import typing + +from comver._regex._process import process + +if typing.TYPE_CHECKING: + from comver.type_definitions import OptionalStringsOrPatterns + +Key = typing.TypeVar("Key") + +Semantic = tuple[Key, re.Pattern[str] | None] +Major = Semantic[typing.Literal["major"]] +Minor = Semantic[typing.Literal["minor"]] +Patch = Semantic[typing.Literal["patch"]] + +StringOrPattern = str | re.Pattern[str] + + +def components( + major_regexes: OptionalStringsOrPatterns, + minor_regexes: OptionalStringsOrPatterns, + patch_regexes: OptionalStringsOrPatterns, +) -> tuple[Major, Minor, Patch]: + """Grouped semantic versioning regexes. + + This allows iteration over all necessary components + with one function. + + Args: + major_regexes: + The regexes finding the `major` version. + minor_regexes: + The regexes finding the `minor` version. + patch_regexes: + The regexes finding the `patch` version. + + Returns: + Dictionary mapping element of semantic versioning to the regex. + + """ + return ( + ("major", major(major_regexes)), + ("minor", minor(minor_regexes)), + ("patch", patch(patch_regexes)), + ) + + +def major(regexes: OptionalStringsOrPatterns) -> re.Pattern[str] | None: + """Return compiled `major` regex. + + Args: + regexes: + Specified `major` version element regexes. + + Returns: + Compiled regular expression (either default or provided). + + """ + return process(regexes, r".*BREAKING CHANGE.*|^(feat|fix)(\(.*?\))?!: .*") + + +def minor(regexes: OptionalStringsOrPatterns) -> re.Pattern[str] | None: + """Return compiled `mior` regex. + + Args: + regexes: + Specified `minor` version element regexes. + + Returns: + Compiled regular expression (either default or provided). + + """ + return process(regexes, "^feat(\\(.*?\\))?: .*") + + +def patch(regexes: OptionalStringsOrPatterns) -> re.Pattern[str] | None: + """Return compiled `patch` regex. + + Args: + regexes: + Specified `patch` version element regexes. + + Returns: + Compiled regular expression (either default or provided). + + """ + return process(regexes, "^fix(\\(.*?\\))?: .*") diff --git a/src/comver/_subcommand.py b/src/comver/_subcommand.py new file mode 100644 index 0000000..5114f52 --- /dev/null +++ b/src/comver/_subcommand.py @@ -0,0 +1,174 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Subcommands of the CLI `comver`.""" + +from __future__ import annotations + +import collections +import hashlib +import json +import sys +import typing + +import loadfig + +from comver._version import Version, VersionCommit + +if typing.TYPE_CHECKING: + import argparse + + +def calculate(args: argparse.Namespace) -> typing.NoReturn: + """Calculate semantic versioning based on commit messages. + + Outputs version and (optionally) sha of a commit + related to this version (the last one in commit chain). + + This output allows to later compare git trees and inferred + versions if necessary. + + Args: + args: + Arguments from the CLI. + + """ + print(_calculate(args)) # noqa: T201 + sys.exit(0) + + +def verify(args: argparse.Namespace) -> typing.NoReturn: + """Verify commit sha and inferred version match. + + Given `version` (as string, e.g. `1.2.3`) and + commit sha verify whether this version was created + from this commit chain. + + Args: + args: + Arguments from the CLI. + + """ + sys.exit(_verify(args)) + + +def _calculate(args: argparse.Namespace) -> str: # noqa: C901 + """Implementation of calculate cli command. + + Args: + args: + Arguments from the CLI. + + Returns: + Either formatted dictionary (if `args.json`) string + or space separated "version sha". `sha` component + is optional based on `args.sha` flag. + + """ + version = VersionCommit() + for version in Version.from_git_configured(): # noqa: B007 + pass + + sha = version.commit.hexsha if version.commit is not None else None + checksum = _checksum_config() + + version = str(version.version) + + if args.format == "line": + if args.sha: + version = f"{version} {sha}" + if args.checksum: + version = f"{version} {checksum}" + return version + + output = {"version": str(version)} + if args.sha and isinstance(sha, str): + output["sha"] = sha + if args.checksum: + output["checksum"] = checksum + + return json.dumps(output, indent=4) + + +def _verify(args: argparse.Namespace) -> bool: + """Verify commit sha and inferred version match. + + Warning: + This subcommand also outputs messages for the end user + + Args: + args: + Arguments from the CLI. + + Returns: + Code status of the command (`True` for error, `False` + on successful verification). + + """ + checksum = _checksum_config() + + if args.checksum != checksum: + print( # noqa: T201 + "Provided checksum and the checksum of configuration do not match.", + file=sys.stderr, + ) + return True + + for output in Version.from_git_configured(): + version, commit = output.version, output.commit + + sha = commit.hexsha if commit is not None else None + + if args.version == version: + if sha is not None and sha == args.sha: + return False + + print( # noqa: T201 + f"Specified version: `{args.version}` has sha: `{sha}`, while expected sha is: `{args.sha}`", + file=sys.stderr, + ) # pragma: no cover + return True # pragma: no cover + + if args.sha == sha: + print( # noqa: T201 + f"Specified sha: `{args.sha}` corresponds to version: `{version}`, while expected version is: `{args.version}`", # noqa: E501 + file=sys.stderr, + ) + return True + + print( # noqa: T201 + f"Neither specified sha: `{args.sha}` nor its corresponding version: `{args.version}` was found in the git tree", # noqa: E501 + file=sys.stderr, + ) + return True + + +def _checksum_config() -> str: + """Get checksum of config. + + Returns: + Checksum of subconfig. + + """ + config = collections.defaultdict(lambda: None, loadfig.config("comver")) + keys = [ + "message_includes", + "message_excludes", + "path_includes", + "path_excludes", + "author_name_includes", + "author_name_excludes", + "author_email_includes", + "author_email_excludes", + "major_regexes", + "minor_regexes", + "patch_regexes", + "unrecognized_message", + ] + + # Get only relevant sections of the dict + subconfig = {k: config[k] for k in keys} + stringified = json.dumps(subconfig, sort_keys=True) + return hashlib.sha256(stringified.encode()).hexdigest() diff --git a/src/comver/_version.py b/src/comver/_version.py new file mode 100644 index 0000000..be994df --- /dev/null +++ b/src/comver/_version.py @@ -0,0 +1,771 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Versioning related core functionality of `comver`. + +This module is responsible for heavy-lifting with respect to: + +- version calculations based on multiple factors (from `string`s, + `git` commits etc.) +- filtering based on user/config provided info (e.g. exclusion of + specific committer name) + +Usually `Version.from_git_configured()` is an entrypoint one +should be interested in. + +Important: + Check out guidelines and tutorials for information about CLI/plugin + usage and suggested configuration. This section should be of interest + to people wanting to use the API directly (e.g. new integrations). + +""" + +from __future__ import annotations + +import collections +import dataclasses +import functools +import typing + +import git +import loadfig + +from comver import _regex, error + +if typing.TYPE_CHECKING: + from collections.abc import Iterable, Iterator + + from comver.type_definitions import OptionalStringsOrPatterns + +from importlib.metadata import version + +_version = version("comver") +"""Current comver version.""" + +del version + +T = typing.TypeVar("T") + + +@functools.total_ordering +@dataclasses.dataclass(frozen=True) +class Version: + """Immutable class creating and keeping commit-based semantic versioning. + + Tip: + This class will likely be instantiated by one of creation + `classmethod`s, these are ordered by an abstraction level + and the higher ups are composed of from the ones below them. + + Warning: + This class is immutable, generator methods + __will return new instances__. + + Attributes: + major: + The major version. + minor: + The minor version. + patch: + The patch version. + """ + + major: int = 0 + minor: int = 0 + patch: int = 0 + + # Add commit message includes and commit message excludes + @classmethod + def from_git_configured( # noqa: PLR0913 + cls, + message_includes: OptionalStringsOrPatterns = None, + message_excludes: OptionalStringsOrPatterns = None, + path_includes: OptionalStringsOrPatterns = None, + path_excludes: OptionalStringsOrPatterns = None, + author_name_includes: OptionalStringsOrPatterns = None, + author_name_excludes: OptionalStringsOrPatterns = None, + author_email_includes: OptionalStringsOrPatterns = None, + author_email_excludes: OptionalStringsOrPatterns = None, + major_regexes: OptionalStringsOrPatterns = None, + minor_regexes: OptionalStringsOrPatterns = None, + patch_regexes: OptionalStringsOrPatterns = None, + unrecognized_message: typing.Literal["ignore", "error"] | None = None, + repository: str | git.Repo | None = None, + ) -> Iterator[VersionCommit]: + r"""Yield version and its respective commit. + + Important: + This `classmethod`'s arguments, if not provided, will be + inferred from `[tool.comver]` section in `pyproject.toml` + (see [loadfig](https://github.com/open-nudge/loadfig) + for more information) + + Example configuration (`pyproject.toml` in your project's git root): + + ```toml + [tool.comver] + # All commits will be included EXCEPT the ones with matching scopes + # e.g. ("feat: add versioning [no version]") + # Note: These are raw strings + message_excludes = [ + ".*\[no version\].*", + ".*\[skip version\].*", + ".*\[version skip\].*", + ] + # Only changes to the src/* folder count or `pyproject.toml` + # For version calculations + path_includes = [ + "src/*", + "pyproject.toml", + ] + + # Commits done by GitHub bot are excluded from versioning + author_name_excludes = [ + "github-actions[bot]", + ] + ``` + + Example usage: + + ```python + import comver + + # Every value taken from the configuration (if available) + for output in comver.Version.git_configured(): + print(output.commit.hexsha, output.version) + ``` + + Commit messages are used to calculate versions based on the + regexes (`major_regexes`, `minor_regexes` and `patch_regexes`). + + Tip: + You can also configure this function via `.comver.toml` + instead of `pyproject.toml`, in such case + remove the `[tool.comver]` header, rest stays the same. + + Warning: + `*_exclude` regexes take precedence over `*_include` regexes, + the `*_include` regexes are checked first, then the `*_exclude` + regexes might disinclude the `*_include` match + + Warning: + `author_name`, `author_email` and `path` exclusions + __will exclude them from output__ (unlike message based + filtering). If the commit does not match it will not be yielded. + + Warning: + Message based filtering will not change the version + (the same will be returned), + __but the Version-Commit pair will be returned__. + + Args: + message_includes: + Commit message regexes against which the commit is included. + Default: From config OR all paths are included. + message_excludes: + Commit message regexes against which the commit is excluded. + Default: From config OR no paths are excluded. + path_includes: + Path regexes against which the commit is included. + Default: From config OR all paths are included. + path_excludes: + Path regexes against which the commit is excluded. + Default: From config OR no paths are excluded. + author_name_includes: + Commit author names regexes against + which the commit is included. + Default: From config OR all names are included. + author_name_excludes: + Commit author names regexes against + which the commit is excluded. + Default: From config OR no names are excluded. + author_email_includes: + Commit author email regexes against + which the commit is included. + Default: From config OR all emails are included. + author_email_excludes: + Commit author email regexes against + which the commit is excluded. + Default: From config OR no emails are excluded. + major_regexes: + The regexes by which the major version is found. + Default: From config OR matches `feat!:` and `fix!:` + messages OR `BREAKING CHANGE` anywhere in the message. + minor_regexes: + The regex for the minor version. + Default: From config OR matches messages starting with `feat:`. + patch_regexes: + The regex for the patch version. + Default: From config OR matches messages starting with `fix:`. + unrecognized_message: + The behavior for unrecognized messages. It can be + either "exclude" or "error". + Default: From config OR "ignore" + repository: + The `git` repository. + Default: From config OR will be searched + in the parent directories. + + Yields: + Version and its respective commit + + """ + config = collections.defaultdict(lambda: None, loadfig.config("comver")) + + yield from cls.from_git( + message_includes=message_includes or config["message_includes"], + message_excludes=message_excludes or config["message_excludes"], + path_includes=path_includes or config["path_includes"], + path_excludes=path_excludes or config["path_excludes"], + author_name_includes=author_name_includes + or config["author_name_includes"], + author_name_excludes=author_name_excludes + or config["author_name_excludes"], + author_email_includes=author_email_includes + or config["author_email_includes"], + author_email_excludes=author_email_excludes + or config["author_email_excludes"], + major_regexes=major_regexes or config["major_regexes"], + minor_regexes=minor_regexes or config["minor_regexes"], + patch_regexes=patch_regexes or config["patch_regexes"], + unrecognized_message=unrecognized_message + or config["unrecognized_message"], + repository=repository, + ) + + @classmethod + def from_git( # noqa: PLR0913 + cls, + message_includes: OptionalStringsOrPatterns = None, + message_excludes: OptionalStringsOrPatterns = None, + path_includes: OptionalStringsOrPatterns = None, + path_excludes: OptionalStringsOrPatterns = None, + author_name_includes: OptionalStringsOrPatterns = None, + author_name_excludes: OptionalStringsOrPatterns = None, + author_email_includes: OptionalStringsOrPatterns = None, + author_email_excludes: OptionalStringsOrPatterns = None, + major_regexes: OptionalStringsOrPatterns = None, + minor_regexes: OptionalStringsOrPatterns = None, + patch_regexes: OptionalStringsOrPatterns = None, + unrecognized_message: typing.Literal["ignore", "error"] | None = None, + repository: str | git.Repo | None = None, + ) -> Iterator[VersionCommit]: + """Yield version and its respective commit. + + Commit messages are used to calculate versions based on the + regexes (`major_regexes`, `minor_regexes` and `patch_regexes`). + + Example usage: + + ```python + import comver + + # Will not take commits done by anyone with @foo.com email + # into the account when creating versions + for output in comver.Version.from_git( + author_email_excludes=(r".*@foo.com",) + ): + print(output.commit.hexsha, output.version) + ``` + + Warning: + `author_name`, `author_email` and `path` exclusions + __will exclude them from output__ (unlike message based + filtering). If the commit does not match it will not be yielded. + + Warning: + Message based filtering will not change the version + (the same will be returned), + __but the Version-Commit pair will be returned__. + + Warning: + `*_exclude` regexes take precedence over `*_include` regexes, + the `*_include` regexes are checked first, then the `*_exclude` + regexes might disinclude the `*_include` match + + Args: + message_includes: + Commit message regexes against which the commit is included. + Default: All paths are included. + message_excludes: + Commit message regexes against which the commit is + excluded. Default: No paths are excluded. + path_includes: + Path regexes against which the commit is included. + Default: All paths are included. + path_excludes: + Path regexes against which the commit is excluded. + Default: No paths are excluded. + author_name_includes: + Commit author names regexes against + which the commit is included. + Default: All names are included. + author_name_excludes: + Commit author names regexes against + which the commit is excluded. + Default: No names are excluded. + author_email_includes: + Commit author email regexes against + which the commit is included. + Default: All emails are included. + author_email_excludes: + Commit author email regexes against + which the commit is excluded. + Default: No emails are excluded. + major_regexes: + The regexes by which the major version is found. + Default: Commit messages starting with `feat!:` and `fix!:` + OR `BREAKING CHANGE` anywhere in the message. + minor_regexes: + The regex for the minor version. + Default: Messages starting with `feat:`. + patch_regexes: + The regex for the patch version. + Default: Matches messages starting with `fix:`. + unrecognized_message: + The behavior for unrecognized messages. It can be + either "exclude" or "error". + Default: "ignore" + repository: + The `git` repository. + Default: Searched in the parent directories. + + Yields: + Version and its respective commit + + """ + if isinstance(repository, str): + repository = git.Repo(repository) + if repository is None: + repository = git.Repo(search_parent_directories=True) + + commits = [ + commit + for commit in repository.iter_commits(reverse=True) + if _include_commit( + commit, + path_includes, + path_excludes, + author_name_includes, + author_name_excludes, + author_email_includes, + author_email_excludes, + ) + ] + + for version, commit in zip( + cls.from_messages( + messages=(str(commit.message) for commit in commits), + message_includes=message_includes, + message_excludes=message_excludes, + major_regexes=major_regexes, + minor_regexes=minor_regexes, + patch_regexes=patch_regexes, + unrecognized_message=unrecognized_message, + ), + commits, + strict=False, + ): + yield VersionCommit(version, commit) + + @classmethod + def from_messages( # noqa: PLR0913 + cls, + messages: Iterable[str], + message_includes: OptionalStringsOrPatterns = None, + message_excludes: OptionalStringsOrPatterns = None, + major_regexes: OptionalStringsOrPatterns = None, + minor_regexes: OptionalStringsOrPatterns = None, + patch_regexes: OptionalStringsOrPatterns = None, + unrecognized_message: typing.Literal["ignore", "error"] | None = None, + ) -> Iterator[Version]: + """Yield versions from an iterable of messages. + + Warning: + Every message will be returned, even if it is excluded + (e.g. by `message_excludes` patterns). In such case + the version __will not change__ (the same, previous one, + is returned). + + Warning: + `*_exclude` regexes take precedence over `*_include` regexes, + the `*_include` regexes are checked first, then the `*_exclude` + regexes might disinclude the `*_include` match + + Args: + messages: + Iterable containing messages from which versions + are calculated. + message_includes: + Commit message regexes against which the commit is included. + Default: All paths are included. + message_excludes: + Commit message regexes against which the commit is + excluded. Default: No paths are excluded. + major_regexes: + The regexes by which the major version is found. + Default: Commit messages starting with `feat!:` and `fix!:` + OR `BREAKING CHANGE` anywhere in the message. + minor_regexes: + The regex for the minor version. + Default: Messages starting with `feat:`. + patch_regexes: + The regex for the patch version. + Default: Matches messages starting with `fix:`. + unrecognized_message: + The behavior for unrecognized messages. It can be + either "exclude" or "error". + Default: "ignore" + + Yields: + Version (one for each message). + """ + version = None + + for message in messages: + version = cls.from_message( + message, + message_includes, + message_excludes, + major_regexes, + minor_regexes, + patch_regexes, + unrecognized_message, + version=version, + ) + yield version + + @classmethod + def from_message( # noqa: PLR0913 + cls, + message: str, + message_includes: OptionalStringsOrPatterns = None, + message_excludes: OptionalStringsOrPatterns = None, + major_regexes: OptionalStringsOrPatterns = None, + minor_regexes: OptionalStringsOrPatterns = None, + patch_regexes: OptionalStringsOrPatterns = None, + unrecognized_message: typing.Literal["ignore", "error"] | None = None, + version: Version | None = None, + ) -> Version: + """Bump the version based on a message. + + Warning: + The message will be returned, even if it should be excluded + (e.g. by `message_excludes` patterns). In such case + the version __will not change__ (the same, previous one, + is returned). + + Args: + message: + Message from which the version is calculated. + message_includes: + Commit message regexes against which the commit is included. + Default: All paths are included. + message_excludes: + Commit message regexes against which the commit is + excluded. Default: No paths are excluded. + major_regexes: + The regexes by which the major version is found. + Default: Commit messages starting with `feat!:` and `fix!:` + OR `BREAKING CHANGE` anywhere in the message. + minor_regexes: + The regex for the minor version. + Default: Messages starting with `feat:`. + patch_regexes: + The regex for the patch version. + Default: Matches messages starting with `fix:`. + unrecognized_message: + The behavior for unrecognized messages. It can be + either "exclude" or "error". + Default: "ignore" + version: + Starting version from which a new version is calculated + (version from which to bump). Default: `0.0.0` version. + + Raises: + MessageUnrecognizedError: If the message is not recognized + by any of the regexes and `unrecognized_message` + is set to "error". + + Returns: + Version corresponding to the message (possibly starting from + initial version provided). + """ + version = cls() if version is None else version + + if not _regex.match.item( + what=message, + include=_regex.process(message_includes), + exclude=_regex.process(message_excludes), + ): + return version + + for semantic_component, regex in _regex.semantic.components( + major_regexes, minor_regexes, patch_regexes + ): + if regex is not None and regex.match(message): + return getattr(version, f"bump_{semantic_component}")() + + if unrecognized_message == "error": + raise error.MessageUnrecognizedError(message) + + return version + + @classmethod + def from_string(cls, version: str) -> Version: + """Create a new version from a string. + + Version should be provided in `MAJOR.MINOR.PATCH` format. + + Args: + version: + The version as a string. + + Raises: + VersionFormatError: + When `version` does not comprise of `3` elements + (e.g. `1.3` instead of `1.3.0`) + VersionNotNumericError: + When `version` elements are not numeric + (e.g. `1.3.2a1` instead of `1.3.2`) + + Returns: + Version object + """ + try: + major, minor, patch = version.split(".") + except ValueError as e: + raise error.VersionFormatError(version) from e + try: + return cls( + major=int(major), + minor=int(minor), + patch=int(patch), + ) + except ValueError as e: + raise error.VersionNotNumericError(version) from e + + def bump_major(self) -> Version: + """Bump the major version. + + Returns: + Version with major bumped and the rest zeroed out. + + """ + return type(self)(1 + self.major, 0, 0) + + def bump_minor(self) -> Version: + """Bump the minor version. + + Returns: + Version with minor bumped and the patch zeroed out. + + """ + return type(self)(self.major, 1 + self.minor, 0) + + def bump_patch(self) -> Version: + """Bump the patch version. + + Returns: + Version with patch bumped. + + """ + return type(self)(self.major, self.minor, 1 + self.patch) + + def __str__(self) -> str: # pyright: ignore [reportImplicitOverride] + """Return the version as a string. + + Returns: + String representation of version (e.g. `"1.37.21"`). + + """ + return f"{self.major}.{self.minor}.{self.patch}" + + def __hash__(self) -> int: # pyright: ignore [reportImplicitOverride] + """Unique hash of the version. + + Returns: + Hashed major, minor and patch + """ + return hash((self.major, self.minor, self.patch)) + + def __eq__(self, other: object) -> bool: # pyright: ignore [reportImplicitOverride] + """Check if two versions are equal. + + Important: + Versions are equal when their `major`, `minor` + and `patch` are equal. + + Args: + other: + Object to compare against. Should be + an instance of `Version` or string + (e.g. `"2.3.7"`) + + Raises: + NotImplementedError: + Raised when comparing to object which is neither + of `str`, nor `Version` type. + + Returns: + Whether the object is equal + + """ + other = self._cast(other) + + return ( + self.major == other.major + and self.minor == other.minor + and self.patch == other.patch + ) + + def __lt__(self, other: object) -> bool: + """Check if this version is smaller than `other`. + + Important: + Version is smaller if its `major` is smaller + or `fix` is smaller (and `major` equal) or `patch` is smaller + (and `major` and `fix` equal) + + Args: + other: + Object to compare against. Should be + an instance of `Version` or string + (e.g. `"2.3.7"`). + + Raises: + NotImplementedError: + Raised when comparing to object which is neither + of `str`, nor `Version` type. + + Returns: + Whether the object is smaller + + """ + other = self._cast(other) + + return ( + self.major < other.major + or (self.major == other.major and self.minor < other.minor) + or ( + self.major == other.major + and self.minor == other.minor + and self.patch < other.patch + ) + ) + + def _cast(self, other: typing.Any) -> Version: + """Cast `other` to `Version` (if possible). + + Raises: + NotImplementedError: + Raised when comparing to object which is neither + of `str`, nor `Version` type. + + Returns: + Casted version + + """ + if isinstance(other, str): + return type(self).from_string(other) + if not isinstance(other, Version): + error = "Comparison of Version only works for strings (e.g. `2.37.1`) and other Version objects" + raise NotImplementedError(error) + + return other + + +@dataclasses.dataclass(frozen=True) +class VersionCommit: + """POD containing `Version` and its respective `git.Commit`. + + This container is returned from `git` related functionalities + of `Version`. + """ + + version: Version = Version(0, 0, 0) + commit: git.Commit | None = None + + +def _include_commit( # noqa: PLR0913 + commit: git.Commit, + path_includes: OptionalStringsOrPatterns = None, + path_excludes: OptionalStringsOrPatterns = None, + author_name_includes: OptionalStringsOrPatterns = None, + author_name_excludes: OptionalStringsOrPatterns = None, + author_email_includes: OptionalStringsOrPatterns = None, + author_email_excludes: OptionalStringsOrPatterns = None, +) -> bool: + """Check whether to include a given commit. + + Args: + commit: + Commit to verify. + path_includes: + Path regexes against which the commit is included. + Default: All paths are included. + path_excludes: + Path regexes against which the commit is excluded. + Default: No paths are excluded. + author_name_includes: + Commit author names regexes against + which the commit is included. + Default: All names are included. + author_name_excludes: + Commit author names regexes against + which the commit is excluded. + Default: No names are excluded. + author_email_includes: + Commit author email regexes against + which the commit is included. + Default: All emails are included. + author_email_excludes: + Commit author email regexes against + which the commit is excluded. + Default: No emails are excluded. + + """ + return ( + _maybe_match( + commit.author.name, author_name_includes, author_name_excludes + ) + and _maybe_match( + commit.author.email, author_email_includes, author_email_excludes + ) + and _regex.match.path( + commit, _regex.process(path_includes), _regex.process(path_excludes) + ) + ) + + +def _maybe_match( + variable: str | None, + includes: OptionalStringsOrPatterns, + excludes: OptionalStringsOrPatterns, +) -> bool: + """Optionally match variable against includes and excludes. + + Important: + `None` is considered as matching. + + Args: + variable: + Variable to be matched + includes: + Optional list of includes + excludes: + Optional list of excludes + + Returns: + `True` if the variable matches the constraints. + + """ + # Escape hatch if commit's author name or email is missing + # This situation is highly unlikely to happen + if variable is None: # pragma: no cover + return True + return _regex.match.item( + variable, _regex.process(includes), _regex.process(excludes) + ) diff --git a/src/comver/error.py b/src/comver/error.py new file mode 100644 index 0000000..c84ce0c --- /dev/null +++ b/src/comver/error.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Custom exceptions of `comver`.""" + +from __future__ import annotations + + +class ComverError(Exception): + """Base class for all exceptions raised by `comver`.""" + + +class MessageUnrecognizedError(ComverError): + """Raised when the message is not recognized by any regexes.""" + + def __init__(self, message: str) -> None: + """Initialize the error. + + Args: + message: + The message which was not recognized by any regexes. + + """ + self.message: str = message + + super().__init__( + f"Message '{message}' is not recognized by any of the provided regexes." + ) + + +class VersionFormatError(ComverError): + """Raised when the string version is not properly formatted. + + Proper format should consist of three dot-separated elements, + e.g. `7.23.1`. + + """ + + def __init__(self, version: str) -> None: + """Initialize the error. + + Args: + version: + String version with incorrect formatting. + + """ + self.version: str = version + + super().__init__( + f"Version should consist of three dot separated elements (MAJOR.MINOR.PATCH), got: {version}" + ) + + +class VersionNotNumericError(ComverError): + """Raised when the string version contains non-numeric elements. + + Proper format should consist of three dot-separated numeric elements, + e.g. `7.23.1`. This error is raised, if there is a version like + `1.2.0a1`. + + Warning: + Only + [semantic Python versioning](https://packaging.python.org/en/latest/discussions/versioning/) + is allowed, e.g. no `1.2.0a1` for alpha releases + + """ + + def __init__(self, version: str) -> None: + """Initialize the error. + + Args: + version: + Properly formatted string version with non-numeric elements. + + """ + self.version: str = version + + super().__init__( + f"One of the MAJOR, MINOR, PATCH is not an integer. Expected .., got: {version}" + ) diff --git a/src/comver/plugin.py b/src/comver/plugin.py new file mode 100644 index 0000000..9968486 --- /dev/null +++ b/src/comver/plugin.py @@ -0,0 +1,241 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Extensions and plugins integrating `comver` with third party systems. + +Most of the extensions are tailored to the Python ecosystem, and its +packagae managers specifically. + +__Currently supported third party tools:__ + +- [`pdm`](https://pdm-project.org/en/latest/) package manager. +- [`hatch`](https://hatch.pypa.io/1.9/plugins/version-source/reference/) + package manager. + +Warning: + Check out guidelines and tutorials for information about CLI/plugin + usage and suggested configuration. This section should be of interest + to people wanting to use the API directly (e.g. new integrations). + +""" + +from __future__ import annotations + +import typing + +from importlib.util import find_spec + +from comver._version import Version, VersionCommit + +if typing.TYPE_CHECKING: + import git + + from comver.type_definitions import OptionalStringsOrPatterns + + +def pdm( # noqa: PLR0913 + message_includes: OptionalStringsOrPatterns = None, + message_excludes: OptionalStringsOrPatterns = None, + path_includes: OptionalStringsOrPatterns = None, + path_excludes: OptionalStringsOrPatterns = None, + author_name_includes: OptionalStringsOrPatterns = None, + author_name_excludes: OptionalStringsOrPatterns = None, + author_email_includes: OptionalStringsOrPatterns = None, + author_email_excludes: OptionalStringsOrPatterns = None, + major_regexes: OptionalStringsOrPatterns = None, + minor_regexes: OptionalStringsOrPatterns = None, + patch_regexes: OptionalStringsOrPatterns = None, + unrecognized_message: typing.Literal["ignore", "error"] | None = None, + repository: str | git.Repo | None = None, +) -> str: + """Entrypoint for `pdm`'s `[tool.pdm.version]` `pyproject.toml` specifier. + + Example `pyproject.toml` usage: + + ```toml + [build-system] + build-backend = "pdm.backend" + + requires = [ + "comver>=0.1.0", + "pdm-backend>=2", + ] + + [tool.pdm.version] + source = "call" + getter = "commition.plugin.pdm:git" + ``` + + This function uses + [`Version.from_git_configured`][comver._version.Version.from_git_configured] + under the hood, __but only outputs the last yielded version `iterable`__. + + Tip: + Plugin can be configured by `[tool.comver]` section. + + Args: + message_includes: + Commit message regexes against which the commit is included. + Default: From config OR all paths are included. + message_excludes: + Commit message regexes against which the commit is excluded. + Default: From config OR no paths are excluded. + path_includes: + Path regexes against which the commit is included. + Default: From config OR all paths are included. + path_excludes: + Path regexes against which the commit is excluded. + Default: From config OR no paths are excluded. + author_name_includes: + Commit author names regexes against + which the commit is included. + Default: From config OR all names are included. + author_name_excludes: + Commit author names regexes against + which the commit is excluded. + Default: From config OR no names are excluded. + author_email_includes: + Commit author email regexes against + which the commit is included. + Default: From config OR all emails are included. + author_email_excludes: + Commit author email regexes against + which the commit is excluded. + Default: From config OR no emails are excluded. + major_regexes: + The regexes by which the major version is found. + Default: From config OR matches `feat!:` and `fix!:` + messages OR `BREAKING CHANGE` anywhere in the message. + minor_regexes: + The regex for the minor version. + Default: From config OR matches messages starting with `feat:`. + patch_regexes: + The regex for the patch version. + Default: From config OR matches messages starting with `fix:`. + unrecognized_message: + The behavior for unrecognized messages. It can be + either "exclude" or "error". + Default: From config OR "ignore" + repository: + The `git` repository. + Default: From config OR will be searched + in the parent directories. + + Returns: + Calculated version as string (compatible with `pdm` interface). + + """ + version = VersionCommit() + for version in Version.from_git_configured( # noqa: B007 + message_includes=message_includes, + message_excludes=message_excludes, + path_includes=path_includes, + path_excludes=path_excludes, + author_name_includes=author_name_includes, + author_name_excludes=author_name_excludes, + author_email_includes=author_email_includes, + author_email_excludes=author_email_excludes, + major_regexes=major_regexes, + minor_regexes=minor_regexes, + patch_regexes=patch_regexes, + unrecognized_message=unrecognized_message, + repository=repository, + ): + pass + + return str(version.version) + + +if find_spec("hatchling"): + from hatchling.plugin import hookimpl + from hatchling.version.source.plugin.interface import VersionSourceInterface + + class ComverVersionSource(VersionSourceInterface): + """Get the project version for the `hatchling` build backend. + + This class implements the `VersionSourceInterface` from Hatchling + and allows integration of the `comver` plugin with Hatch-based + projects. + + Tip: + See [plugins](https://hatch.pypa.io/1.13/plugins/version-source/reference/) + for more information + + This function uses + [`Version.from_git_configured`][comver._version.Version.from_git_configured] + under the hood, __but only outputs the last version + yielded from `iterable`__. + + > [!IMPORTANT] + > Plugin can also be configured by `[tool.hatch.version]`, not only + > `[tool.comver`]. The former takes precedence if both exist. + + """ + + PLUGIN_NAME: str = "comver" # pyright: ignore [reportIncompatibleUnannotatedOverride] + + def get_version_data(self) -> dict[str, str]: # pyright: ignore [reportImplicitOverride] + """Get the version data to be used by Hatchling. + + Returns: + A dictionary with the resolved version string, + under the "version" key. + """ + version = VersionCommit() + for version in Version.from_git_configured( # noqa: B007 + message_includes=self.config.get("message_includes"), # pyright: ignore [reportUnknownArgumentType] + message_excludes=self.config.get("message_excludes"), # pyright: ignore [reportUnknownArgumentType] + path_includes=self.config.get("path_includes"), # pyright: ignore [reportUnknownArgumentType] + path_excludes=self.config.get("path_excludes"), # pyright: ignore [reportUnknownArgumentType] + author_name_includes=self.config.get("author_name_includes"), # pyright: ignore [reportUnknownArgumentType] + author_name_excludes=self.config.get("author_name_excludes"), # pyright: ignore [reportUnknownArgumentType] + author_email_includes=self.config.get("author_email_includes"), # pyright: ignore [reportUnknownArgumentType] + author_email_excludes=self.config.get("author_email_excludes"), # pyright: ignore [reportUnknownArgumentType] + repository=self.root, + ): + pass + + return {"version": str(version.version)} + + def set_version( # pyright: ignore [reportIncompatibleMethodOverride, reportImplicitOverride] + self, + _: str, + __: dict[str, str], + ) -> typing.NoReturn: # pragma: no cover + """Setting the version is not supported by the comver plugin. + + This method exists to fulfill the interface, but always raises + `NotImplementedError`. + + Args: + _: + The version string (ignored). + __: + Additional data (ignored). + + Raises: + NotImplementedError: + Always raised to indicate that setting the + version is unsupported. + """ + error = "comver plugin does not support setting the version." + raise NotImplementedError(error) + + @hookimpl + def hatch_register_version_source() -> type[ComverVersionSource]: + """Automatically register hatchling plugin. + + Note: + This function is called implicitly to register + `comver` for `hatchling` backend. + + Returns: + ComverVersionSource class + + """ + return ComverVersionSource + +else: # pragma: no cover + pass diff --git a/src/comver/type_definitions.py b/src/comver/type_definitions.py new file mode 100644 index 0000000..4b530b9 --- /dev/null +++ b/src/comver/type_definitions.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Type definitions of `comver`. + +Important: + This module is a simplification for `typing` of complex types + and should be used __eventually__ by third party plugin developers. + +""" + +from __future__ import annotations + +import re + +from collections.abc import Iterable + +StringOrPattern = str | re.Pattern[str] +"""Either `string` or compiled `re.Pattern`.""" + +StringsOrPatterns = Iterable[StringOrPattern] +"""Iterable of `StringOrPattern`.""" + +OptionalStringsOrPatterns = Iterable[StringOrPattern] | None +"""Iterable of `StringOrPattern` or `None`.""" diff --git a/tests/README.md b/tests/README.md index 3525bdf..ea12dfd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -5,10 +5,6 @@ SPDX-FileContributor: szymonmaszke SPDX-License-Identifier: Apache-2.0 --> -# Tests of comver - -- `test_smoke.py` - generic - [smoke tests](https://grafana.com/blog/2024/01/30/smoke-testing/) - to check if the package is importable +# Tests of commition diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0b7e386 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Shared tests functionality. + +Each shared functionality should be placed in this file and +added to the `pytest` namespace (later reused by other tests). + +""" + +from __future__ import annotations + +import argparse +import dataclasses +import typing + +import pytest + + +@dataclasses.dataclass +class ComverVersionTester: + """Stripped-down version tester for `comver`. + + Attributes: + major: + Major version number. + minor: + Minor version number. + patch: + Patch version + + """ + + major: int = 0 + minor: int = 0 + patch: int = 0 + + def to_tuple(self) -> tuple[int, int, int]: + """Return version as tuple.""" + return self.major, self.minor, self.patch + + def bump( + self, commit_type: typing.Literal["fix", "feat", "feat!", "fix!"] + ) -> None: + """Bump version based on commit type. + + Args: + commit_type: + Commit type to bump version for. + + """ + # Either of these may not be ran during fuzz-testing + if commit_type in {"fix!", "feat!"}: # pragma: no cover + self.major += 1 + self.minor, self.patch = 0, 0 + elif commit_type == "feat": # pragma: no cover + self.minor += 1 + self.patch = 0 + elif commit_type == "fix": # pragma: no cover + self.patch += 1 + + +pytest.ComverVersionTester = ComverVersionTester # pyright: ignore [reportAttributeAccessIssue] +"""Hack making `ComverVersionTester` globally test available.""" + +ARGS = argparse.Namespace() +ARGS.sha = True +ARGS.checksum = True +ARGS.format = "line" +pytest.ComverCalculateArgs = ARGS # pyright: ignore [reportAttributeAccessIssue] +"""Hack making CLI args for calculate subcommand globally available.""" diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..9778357 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,102 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Smoke test CLI entrypoint.""" + +from __future__ import annotations + +import typing + +import pytest + +from comver import _cli, _subcommand + + +@pytest.mark.parametrize("format", ("line", "json")) +@pytest.mark.parametrize("sha", (True, False)) +@pytest.mark.parametrize("checksum", (True, False)) +def test_smoke_calculate( + format: typing.Literal["line", "json"], # noqa: A002 + sha: bool, # noqa: FBT001 + checksum: bool, # noqa: FBT001 +) -> None: + """Smoke test calculate subcommand. + + Args: + format: + Either `line` or `json` corresponding to `calculate`'s + CLI arguments. + sha: + Whether to output `sha` as well. + checksum: + Whether to output `checksum` as well. + + """ + args = ["calculate", "--format", format] + if sha: + args.append("--sha") + if checksum: + args.append("--checksum") + try: + _cli.main(args) + except SystemExit as e: + assert e.code == 0 # noqa: PT017 + + +@pytest.mark.parametrize( + ("version", "sha", "checksum", "code"), + ( + # Neither version, nor sha, nor checksum will exist in this git tree + ( + "99999.99999.99999", + "randomShaNonExistent", + "randomChecksumNonExistent", + 1, + ), + # Neither version, nor sha will exist in this git tree + ( + "99999.99999.99999", + "randomShaNonExistent", + _subcommand._calculate(pytest.ComverCalculateArgs).split()[2], # noqa: SLF001 # pyright: ignore [reportUnknownArgumentType, reportAttributeAccessIssue] + 1, + ), + # Obtain current commit sha (which is guaranteed to be within the tree) + # Assign random version which has small chance of real life occurrence + # Should return 1 but for different reasons + ( + "99999.99999.99999", + *_subcommand._calculate(pytest.ComverCalculateArgs).split()[1:], # noqa: SLF001 # pyright: ignore [reportUnknownArgumentType, reportAttributeAccessIssue] + 1, + ), + # Calculate current version and its sha counterpart + # Verify should not return an error in such case + (*(_subcommand._calculate(pytest.ComverCalculateArgs).split()), 0), # noqa: SLF001 # pyright: ignore [reportUnknownArgumentType, reportAttributeAccessIssue] + ), +) +def test_verify( + version: str, + sha: str, + checksum: str, + code: typing.Literal[0, 1], +) -> None: + """Test `verify` command. + + Actual exit command will be compared to the one assumed by the test. + + Args: + version: + Version to test. + sha: + Sha to verify version against. + checksum: + Checksum of the config file. + code: + Either `0` (proper execution) or `1` (error). + + """ + try: + _cli.main(["verify", version, sha, checksum]) + except SystemExit as e: + assert e.code == code # noqa: PT017 diff --git a/tests/test_plugin.py b/tests/test_plugin.py new file mode 100644 index 0000000..7f8c8dd --- /dev/null +++ b/tests/test_plugin.py @@ -0,0 +1,213 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +# pyright: reportUnusedCallResult=false + +"""Test `comver.plugin` module tests.""" + +from __future__ import annotations + +import pathlib +import shutil +import tempfile +import typing +import uuid + +import git + +import pytest + +from hypothesis import given, settings +from hypothesis import strategies as st + +import comver + + +@st.composite +def repository( + draw: st.DrawFn, *, n_commits: int = 10 +) -> tuple[str, comver.Version]: + """Create repository with `n_commits` commits. + + Commits will have the same text, but different scope, + all are changing the same file over and over. + + Args: + draw: + Hypothesis draw function. + n_commits: + Number of commits to create. + + Returns: + Repository with `n_commits` commits. + """ + directory = pathlib.Path(tempfile.mkdtemp()) + repo = git.Repo.init(directory) + file = directory / "file.txt" + + version = pytest.ComverVersionTester() # pyright: ignore [reportAttributeAccessIssue] + + for _ in range(n_commits): + commit_type = draw(st.sampled_from(["fix", "feat", "feat!", "fix!"])) + author = draw(st.sampled_from(["Alice", "Bob"])) + email = draw(st.sampled_from(["alice@example.com", "bob@example.com"])) + + with file.open("w") as f: + f.write(str(uuid.uuid4())) + + repo.index.add(file) + repo.index.commit( + f"{commit_type}: bla bla bla [no version]", + author=git.Actor(author, email), + ) + + version.bump(commit_type) + + if repo.working_tree_dir is not None: + return str(repo.working_tree_dir), comver.Version(*version.to_tuple()) # pyright: ignore [reportUnknownArgumentType] + return ( + "", + comver.Version( + *version.to_tuple() # pragma: no cover # pyright: ignore [reportUnknownArgumentType] + ), + ) + + +def _hatchling(repository: str, **kwargs: dict[str, list[str] | None]) -> str: + """Dummy function unifying `hatchling` plugin interface with `pdm`. + + This allows for later usage in `pytest.mark.parametrize` in `test_plugin` + function. + + Args: + repository: + Path to the repository given as a string + **kwargs: + Configuration passed to the plugin (akin to the one + which would be passed from `[tool.hatch.version]` + in `pyproject.toml` configuration). + + Returns: + Version as string + + """ + return comver.plugin.ComverVersionSource( + root=repository, config=kwargs + ).get_version_data()["version"] + + +@pytest.mark.parametrize("plugin", (comver.plugin.pdm, _hatchling)) +@pytest.mark.parametrize("message_includes", (None, (".*", "whatever"))) +@pytest.mark.parametrize("message_excludes", (None, (r".*\[no version\].*",))) +@pytest.mark.parametrize("path_includes", (None, (".*", "whatever"))) +@pytest.mark.parametrize("path_excludes", (None, (".*", "whatever"))) +@pytest.mark.parametrize("author_name_includes", (None, (".*", "whatever"))) +@pytest.mark.parametrize("author_name_excludes", (None, ("Alice", "Bob"))) +@pytest.mark.parametrize("author_email_includes", (None, (".*@example.com",))) +@pytest.mark.parametrize("author_email_excludes", (None, (".*@example.com",))) +@settings(max_examples=2) +@given(repository_test_version=repository()) +def test_plugin( # noqa: PLR0913 + plugin: typing.Callable[..., str], # Takes all arguments below + message_includes: tuple[str, ...] | None, + message_excludes: tuple[str, ...] | None, + path_includes: tuple[str, ...] | None, + path_excludes: tuple[str, ...] | None, + author_name_includes: tuple[str, ...] | None, + author_name_excludes: tuple[str, ...] | None, + author_email_includes: tuple[str, ...] | None, + author_email_excludes: tuple[str, ...] | None, + repository_test_version: tuple[str, comver.Version], +) -> None: + """Test `comver.plugin.pdm` function. + + Tests if `comver.plugin.pdm.git` function returns + correct version and whether regular expression based filtering works. + + See `comver.plugin.pdm` docs for more details. + + Args: + plugin: + Plugin function + message_includes: + Commit message regexes against which the commit is included. + Default: From config OR all paths are included. + message_excludes: + Commit message regexes against which the commit is excluded. + Default: From config OR no paths are excluded. + path_includes: + Path regexes against which the commit is included. + Default: From config OR all paths are included. + path_excludes: + Path regexes against which the commit is excluded. + Default: From config OR no paths are excluded. + author_name_includes: + Commit author names regexes against + which the commit is included. + Default: From config OR all names are included. + author_name_excludes: + Commit author names regexes against + which the commit is excluded. + Default: From config OR no names are excluded. + author_email_includes: + Commit author email regexes against + which the commit is included. + Default: From config OR all emails are included. + author_email_excludes: + Commit author email regexes against + which the commit is excluded. + Default: From config OR no emails are excluded. + repository_test_version: + Path to repository and its corresponding version. + + """ + repository, test_version = repository_test_version + comver_version = plugin( + message_includes=message_includes, + message_excludes=message_excludes, + path_includes=path_includes, + path_excludes=path_excludes, + author_name_includes=author_name_includes, + author_name_excludes=author_name_excludes, + author_email_includes=author_email_includes, + author_email_excludes=author_email_excludes, + repository=repository, + ) + + if author_name_excludes or author_email_excludes or message_excludes: + assert comver_version == "0.0.0" + # In case of path_excludes, we will get either 0.1.0, 1.0.0 or 0.0.1 + elif path_excludes: + assert comver_version in ( + comver.Version(0, 1, 0), + comver.Version(1, 0, 0), + comver.Version(0, 0, 1), + ) + else: + assert comver_version == test_version + + shutil.rmtree(repository) + + +def test_smoke_pdm() -> None: + """Test `comver.plugin.pdm` function on current repository. + + This is a smoke test to check if `comver.plugin.pdm` + function works on current repository. + + Parsing commits from this project is complex and requires the same work + as the library itself, so we only check if the function runs without + errors. + + """ + comver.plugin.pdm() + + +def test_smoke_hatchling() -> None: + """Smoke test `comver.plugin.ComverVersionSource` hook.""" + assert ( + comver.plugin.ComverVersionSource + == comver.plugin.hatch_register_version_source() + ) diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..9a32883 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,148 @@ +# SPDX-FileCopyrightText: © 2025 open-nudge +# SPDX-FileContributor: szymonmaszke +# +# SPDX-License-Identifier: Apache-2.0 + +"""Test `comver.Version` class.""" + +from __future__ import annotations + +import typing + +import pytest + +from hypothesis import given +from hypothesis import strategies as st + +import comver + + +def create_version(commit_types: list[str]) -> comver.Version: + """Create version based on commit types. + + Args: + commit_types: + List of commit types. Contain only `fix` and `feat` types + with optional `!` at the end. + + Returns: + Version based on commit types. + + """ + version = pytest.ComverVersionTester() # pyright: ignore [reportAttributeAccessIssue] + for commit_type in commit_types: + version.bump(commit_type) + + return comver.Version(*version.to_tuple()) # pyright: ignore [reportUnknownArgumentType] + + +@pytest.mark.parametrize("major_regexes", (None, (".*", "bbb"))) +@pytest.mark.parametrize("minor_regexes", (None, (".*", "ccc"))) +@pytest.mark.parametrize("patch_regexes", (None, (".*", "ddd"))) +@given(commit_types=st.lists(st.sampled_from(["fix", "feat", "feat!", "fix!"]))) +def test_version_from_messages( + commit_types: list[str], + major_regexes: tuple[str] | None, + minor_regexes: tuple[str] | None, + patch_regexes: tuple[str] | None, +) -> None: + """Test `comver.Version.from_messages` method. + + This test focuses on regular expressions arguments and order + of commit evaluation (from `major`, through `minor` to `patch`). + + > [!IMPORTANT] + > This test function was separated from `test_plugin` + > as it is __way faster__ to run than creating actual `git` repos. + + Args: + commit_types: + List of commit types. Contain only `fix` and `feat` types + with optional `!` at the end. + major_regexes: + Regular expression for major version. + minor_regexes: + Regular expression for minor version. + patch_regexes: + Regular expression for patch version. + """ + messages = [f"{commit_type}: bla bla bla" for commit_type in commit_types] + test_version = create_version(commit_types) + + version = comver.Version() + # Iterate over iterator to get the last element + for version in comver.Version.from_messages( # noqa: B007 + messages, + major_regexes=major_regexes, + minor_regexes=minor_regexes, + patch_regexes=patch_regexes, + ): + pass + + # ".*" swallows all commits, all should be major versions + if major_regexes: + assert version.major == len(messages) + # We can only assert something about major if minor swallows all + elif not major_regexes and minor_regexes: + assert version.major == test_version.major + # If none are specified, it should be a standard version + else: + assert comver.Version.from_string(str(test_version)) == version + + +def test_unrecognized_commit_type() -> None: + """Test `comver.Version.bump` method with unrecognized commit type.""" + with pytest.raises(comver.error.MessageUnrecognizedError): + _ = comver.Version.from_message( + "placeholder", + unrecognized_message="error", + ) + + +def test_hash() -> None: + """Smoke test __hash__ function.""" + assert hash( + comver.Version.from_string( + "0.1.0", + ) + ) == hash(comver.Version(0, 1, 0)) + + +@pytest.mark.parametrize("other", ("0.27.31", comver.Version(0, 0, 23))) +def test_comparison(other: str | comver.Version) -> None: + """Generic test of version comparisons. + + > [!IMPORTANT] + > This test goes through implemented `__lt__` and `__eq__` + > at the same time due to functools.total_ordering + > which uses under the hood `__le__` and `__eq__` + + """ + assert other <= comver.Version.from_string( + "0.31.27", + ) + + +@pytest.mark.parametrize( + ("other", "error"), + ( + ("HakunaMatata", comver.error.VersionFormatError), + ("0.1.245b+02435", comver.error.VersionNotNumericError), + (12, NotImplementedError), + ), +) +def test_incorrect_comparison( + other: typing.Any, error: type[Exception] +) -> None: + """Generic test of version comparisons. + + > [!IMPORTANT] + > This test goes through implemented `__lt__` and `__eq__` + > at the same time due to functools.total_ordering + > which uses under the hood `__le__` and `__eq__` + + """ + with pytest.raises(error): + assert other == comver.Version.from_string( + "0.31.27", + )