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`:
+
+
+
+| PDM |
+Hatch |
+
+
+|
+
+```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",
+ )