pax_global_header 0000666 0000000 0000000 00000000064 15016047771 0014522 g ustar 00root root 0000000 0000000 52 comment=7bfe430c2ad577054d2c5ed5d4c00bdc5196a52d
awesomeversion-25.5.0/ 0000775 0000000 0000000 00000000000 15016047771 0014661 5 ustar 00root root 0000000 0000000 awesomeversion-25.5.0/.codecov.yml 0000664 0000000 0000000 00000000417 15016047771 0017106 0 ustar 00root root 0000000 0000000 comment: false
codecov:
branch: main
coverage:
precision: 2
status:
patch: off
project:
default:
target: 100%
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
ignore:
- "tests" awesomeversion-25.5.0/.devcontainer.json 0000664 0000000 0000000 00000002056 15016047771 0020316 0 ustar 00root root 0000000 0000000 {
"name": "AwesomeVersion",
"image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.11",
"postCreateCommand": "make requirements",
"customizations": {
"vscode": {
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
},
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"GitHub.copilot"
]
}
}
}
awesomeversion-25.5.0/.gitattributes 0000664 0000000 0000000 00000000022 15016047771 0017546 0 ustar 00root root 0000000 0000000 * text=auto eol=lf awesomeversion-25.5.0/.github/ 0000775 0000000 0000000 00000000000 15016047771 0016221 5 ustar 00root root 0000000 0000000 awesomeversion-25.5.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15016047771 0020404 5 ustar 00root root 0000000 0000000 awesomeversion-25.5.0/.github/ISSUE_TEMPLATE/bug.yml 0000664 0000000 0000000 00000002132 15016047771 0021702 0 ustar 00root root 0000000 0000000 name: "Report a bug with AwesomeVersion"
description: Report an issue with AwesomeVersion
labels: "bug"
body:
- type: textarea
attributes:
label: The problem
placeholder: >-
Describe the issue you are experiencing here to communicate to the
maintainers. Tell us what you were trying to do and what happened.
validations:
required: true
- type: markdown
attributes:
value: |
## Environment
- type: input
attributes:
label: Operating system
validations:
required: true
- type: dropdown
validations:
required: true
attributes:
label: Python version
options:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- type: textarea
validations:
required: true
attributes:
label: Problem-relevant code
description: >-
A minimal example to reproduce the bug
render: python
- type: textarea
attributes:
label: Traceback/Error logs
description: >-
If you come across any trace or error logs, please provide them.
render: txt
- type: textarea
attributes:
label: Additional information awesomeversion-25.5.0/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000000033 15016047771 0022370 0 ustar 00root root 0000000 0000000 blank_issues_enabled: false awesomeversion-25.5.0/.github/ISSUE_TEMPLATE/feature_request.yml 0000664 0000000 0000000 00000001224 15016047771 0024331 0 ustar 00root root 0000000 0000000 name: "Feature request for AwesomeVersion"
description: Suggest a feature/change to be added to AwesomeVersion
labels: "enhancement"
body:
- type: textarea
validations:
required: true
attributes:
label: The idea
description: >
A good description of what you are suggesting.
- type: textarea
validations:
required: true
attributes:
label: Implementation
description: >
How do you see this being implemented?
- type: textarea
attributes:
label: Alternatives
description: >
Are there any alternative solutions or features you've considered?
- type: textarea
attributes:
label: Additional context awesomeversion-25.5.0/.github/PULL_REQUEST_TEMPLATE.md 0000664 0000000 0000000 00000003353 15016047771 0022026 0 ustar 00root root 0000000 0000000
## Proposed change
## Type of change
- [ ] Dependency upgrade
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (which adds functionalit)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests
## Additional information
- This PR fixes or closes issue: fixes #
- This PR is related to issue:
- Link to documentation pull request:
## Checklist
- [ ] The code change is tested and works locally.
- [ ] Local tests pass.
- [ ] There is no commented out code in this PR.
- [ ] The code has been formatted using Black (`make lint`)
- [ ] Tests have been added to verify that the new code works.
awesomeversion-25.5.0/.github/dependabot.yml 0000664 0000000 0000000 00000000534 15016047771 0021053 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
time: "06:00"
open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: daily
time: "06:00"
open-pull-requests-limit: 10
groups:
artifacts:
patterns:
- "actions/*-artifact" awesomeversion-25.5.0/.github/release.yml 0000664 0000000 0000000 00000000127 15016047771 0020364 0 ustar 00root root 0000000 0000000 changelog:
exclude:
labels:
- dependencies
- repository
- test
awesomeversion-25.5.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15016047771 0020256 5 ustar 00root root 0000000 0000000 awesomeversion-25.5.0/.github/workflows/actions.yml 0000664 0000000 0000000 00000004065 15016047771 0022446 0 ustar 00root root 0000000 0000000 name: Actions
on:
pull_request:
branches:
- main
push:
branches:
- main
permissions: {}
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: π₯ Checkout the repository
uses: actions/checkout@v4.2.2
- name: π Set up Python 3
uses: actions/setup-python@v5
id: python
with:
python-version: 3.x
- name: π¦ Install dependencies
run: make requirements
- name: π€ Lint with Black
run: make black-check
- name: π Lint with mypy
run: make mypy
- name: β¨ Lint with pylint
run: make pylint
- name: π€ Lint with isort
run: make isort-check
test:
name: Test with Python ${{ matrix.entry.version }}
needs: lint
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
entry:
- version: "3.9"
- version: "3.10"
- version: "3.11"
- version: "3.12"
- version: "3.13"
steps:
- name: π₯ Checkout the repository
uses: actions/checkout@v4.2.2
- name: π οΈ Set up Python ${{ matrix.entry.version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.entry.version }}
allow-prereleases: ${{ matrix.entry.prereleases || false }}
- name: π¦ Install dependencies
run: make requirements
- name: π Run tests
run: make test
- name: π Build
run: make build
coverage:
name: Upload coverage to Codecov
needs: test
runs-on: ubuntu-latest
steps:
- name: π₯ Checkout the repository
uses: actions/checkout@v4.2.2
with:
fetch-depth: 0
- name: π οΈ Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- name: π¦ Install dependencies
run: make requirements
- name: π€ Upload coverage to Codecov
run: |
make coverage
curl -sfSL https://codecov.io/bash | bash - awesomeversion-25.5.0/.github/workflows/codspeed.yml 0000664 0000000 0000000 00000001340 15016047771 0022565 0 ustar 00root root 0000000 0000000 name: CodSpeed
on:
push:
branches:
- "main"
pull_request:
branches:
- "main"
permissions: {}
jobs:
benchmarks:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: π Set up Python 3
uses: actions/setup-python@v5.6.0
id: python
with:
python-version: 3.x
- name: Install dependencies
run: make requirements
- name: Run benchmarks
uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3.5.0
with:
token: ${{ secrets.CODSPEED_TOKEN }}
run: poetry run pytest -x --no-cov -vvvvv --codspeed benchmarks
awesomeversion-25.5.0/.github/workflows/demo.yml 0000664 0000000 0000000 00000001304 15016047771 0021723 0 ustar 00root root 0000000 0000000 name: Deploy demo
on:
push:
branches:
- "main"
permissions: {}
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
permissions:
contents: read
pages: write
id-token: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Setup Pages
uses: actions/configure-pages@v5.0.0
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: 'demo'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
awesomeversion-25.5.0/.github/workflows/release.yml 0000664 0000000 0000000 00000002361 15016047771 0022423 0 ustar 00root root 0000000 0000000 name: Release
on:
release:
types:
- published
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
name: Build package
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Set up Python 3.x
uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Install poetry
run: make install-poetry
- name: Set version number
run: |
poetry version "${{ github.event.release.tag_name }}"
- name: Build
run: poetry build --no-interaction
- name: Upload dists
uses: actions/upload-artifact@v4.6.2
with:
name: "dist"
path: "dist/"
if-no-files-found: error
retention-days: 5
publish:
name: Publish release to PyPI
runs-on: ubuntu-latest
needs: "build"
environment:
name: release
url: https://pypi.org/project/awesomeversion
permissions:
id-token: write
steps:
- name: Download dists
uses: actions/download-artifact@v4.3.0
with:
name: "dist"
path: "dist/"
- name: Publish dists to PyPI
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 awesomeversion-25.5.0/.gitignore 0000664 0000000 0000000 00000000147 15016047771 0016653 0 ustar 00root root 0000000 0000000 __pycache__
__target__
.codspeed
.mypy_cache
.pytest_cache
.coverage
coverage.xml
dist
build
*.egg-info awesomeversion-25.5.0/LICENCE.md 0000664 0000000 0000000 00000002064 15016047771 0016247 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2020-2022 Joakim SΓΈrensen
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. awesomeversion-25.5.0/Makefile 0000664 0000000 0000000 00000002757 15016047771 0016334 0 ustar 00root root 0000000 0000000 .DEFAULT_GOAL := help
help: ## Shows this help message
@printf "\033[1m%s\033[36m %s\033[32m %s\033[0m \n\n" "Development environment for" "ludeeus/awesomeversion" "";
@awk 'BEGIN {FS = ":.*##";} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m make %-25s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST);
@echo
requirements: install-poetry ## Install requirements
@poetry install
@poetry check
install: ## Install awesomeversion
@poetry install
install-poetry:
@curl -sSL https://install.python-poetry.org | python3 -
test: ## Run all tests
@poetry run pytest --timeout=10 tests -rxf -x -vv -l -s --cov=./ --cov-report=xml
build: ## Build the package
@poetry build
lint: isort black mypy pylint ## Lint all files
snapshot-update: ## Update test snapshot files
@poetry run pytest tests --snapshot-update --timeout=10
benchmark:
@poetry run pytest -x --no-cov -vvvvv benchmarks
coverage: ## Check the coverage of the package
@poetry run pytest tests --timeout=10 -rxf -x -v -l --cov=./ --cov-report=xml > /dev/null
@poetry run coverage report
isort:
@poetry run isort awesomeversion tests benchmarks
isort-check:
@poetry run isort awesomeversion tests benchmarks --check-only
black:
@poetry run black --fast awesomeversion tests benchmarks
black-check:
@poetry run black --check --fast awesomeversion tests benchmarks
mypy:
@poetry run mypy --strict awesomeversion tests benchmarks
pylint:
@poetry run pylint awesomeversion tests benchmarks awesomeversion-25.5.0/README.md 0000664 0000000 0000000 00000017330 15016047771 0016144 0 ustar 00root root 0000000 0000000 # AwesomeVersion
[](https://codecov.io/gh/ludeeus/awesomeversion)


[](https://pypi.org/project/awesomeversion)

_One version package to rule them all, One version package to find them, One version package to bring them all, and in the darkness bind them._
Make anything a version object, and compare against a vast section of other version formats.
## Installation
```bash
python3 -m pip install awesomeversion
```
## AwesomeVersion class
The AwesomeVersion class takes a version as the first argument, you can also pass in additional kwargs to customize the version object.
Argument | Description
--- | ---
`version` | The version string to parse.
`ensure_strategy` | Match the `AwesomeVersion` object against spesific strategies when creating if. If it does not match `AwesomeVersionStrategyException` will be raised
`find_first_match` | If True, the version given will be scanned for the first match of the given `ensure_strategy`. Raises `AwesomeVersionStrategyException` If it is not found for any of the given strategies.
## AwesomeVersion methods
AwesomeVersion.in_range
This is a helper method to check if the version is in a range.
This method takes two arguments, `lowest` and `highest`, both are required, and returns a boolean.
> **Note** This method is the same as doing `lowest <= AwesomeVersion <= highest`
Example:
```python
from awesomeversion import AwesomeVersion
print(AwesomeVersion("1.2.2").in_range("1.2.1", "1.3"))
> True
print(AwesomeVersion("1.2.0").in_range("1.2.1", "1.3"))
> False
```
AwesomeVersion.diff
This is a helper method to get the difference between two versions.
This method takes one argument which is the version to compare against, and returns a `AwesomeVersionDiff` object.
> **Note** This method is the same as doing `AwesomeVersion - version`
Example:
```python
from awesomeversion import AwesomeVersion
> print(AwesomeVersion("1.0").diff("2.1"))
AwesomeVersionDiff(major=True, minor=True, patch=False, modifier=False, strategy=False)
```
AwesomeVersion.section
This is a helper method to get a section of the version.
This method takes one argument which is the section to get, and returns an integer representing it (or 0 if it does not exist).
Example:
```python
from awesomeversion import AwesomeVersion
> print(AwesomeVersion("1.0").section(0))
1
```
## AwesomeVersion properties
Argument | Description
--- | ---
`alpha` | This is a boolean representing if the version is an alpha version.
`beta` | This is a boolean representing if the version is a beta version.
`dev` | This is a boolean representing if the version is a dev version.
`major` | This is an `AwesomeVersion` object representing the major version or `None` if not present.
`micro` | This is an `AwesomeVersion` object representing the micro version or `None` if not present.
`minor` | This is an `AwesomeVersion` object representing the minor version or `None` if not present.
`modifier_type` | This is a string representing the modifier type of the version or `None` if not present.
`modifier` | This is a string representing the modifier of the version or `None` if not present.
`patch` | This is an `AwesomeVersion` object representing the patch version or `None` if not present.
`prefix` | This is the prefix of the version or `None` if not present.
`release_candidate` | This is a boolean representing if the version is a release candidate version.
`simple` | This is a boolean representing if the version is a simple version.
`strategy_description` | This is a `AwesomeVersionStrategyDescription` object representing the strategy description of the version.
`strategy` | This is a `AwesomeVersionStrategy` object representing the strategy of the version.
`string` | This is the string representation of the version (without the v prefix if present).
`valid` | This is a boolean representing if the version is valid (not unknown strategy).
`year` | This is alias to `major`, and is an `AwesomeVersion` object representing the year.
## Example usage
Here are some examples of how you can use this package, more examples can be found in the `tests` directory.
Basic compare
```python
from awesomeversion import AwesomeVersion
current = AwesomeVersion("1.2.2")
upstream = AwesomeVersion("1.2.3")
print(upstream > current)
> True
```
Compare beta version
```python
from awesomeversion import AwesomeVersion
current = AwesomeVersion("2021.1.0")
upstream = AwesomeVersion("2021.1.0b2")
print(current > upstream)
> True
```
Check if version is a beta version
```python
from awesomeversion import AwesomeVersion
print(AwesomeVersion("1.2.3b0").beta)
> True
print(AwesomeVersion("1.2.3").beta)
> False
```
Use AwesomeVersion with with ...
```python
from awesomeversion import AwesomeVersion
with AwesomeVersion("20.12.0") as current:
with AwesomeVersion("20.12.1") as upstream:
print(upstream > current)
> True
```
Compare AwesomeVersion with other non-AwesomeVersion formats
```python
from awesomeversion import AwesomeVersion
base = AwesomeVersion("20.12.0")
print(base > "20.12.1")
> False
print(base > "19")
> True
print(base > 5)
> True
```
## General behavior
You can test your versions on the [demo page][awesomeversion_demo].
### Modifiers
When comparing versions with modifiers, if the base version is the same the modifier will be used to determine the order.
If one of the versions do not have a modifier, the one without will be considered newer.
The order of the modifiers are:
- No modifier
- RC
- Beta
- Alpha
- Dev
Examples
```python
from awesomeversion import AwesomeVersion
print(AwesomeVersion("1.0.0") > AwesomeVersion("1.0.0b6"))
> True
print(AwesomeVersion("1.0.0") > AwesomeVersion("1.0.0.dev6"))
> True
print(AwesomeVersion("1.0.0.dev19") > AwesomeVersion("1.0.0b4"))
> False
```
### Special versions (container)
There are some special versions for container that are handled differently than typical version formats.
The special versions are in the following order:
- `dev` (newest)
- `latest`
- `beta`
- `stable` (oldest)
If only the first version is this special version, it will be considered newer.
If only the second version is this special version, it will be considered older.
Examples
```python
from awesomeversion import AwesomeVersion
print(AwesomeVersion("latest") > AwesomeVersion("1.0.0b6"))
> True
print(AwesomeVersion("1.0.0") > AwesomeVersion("latest"))
> False
print(AwesomeVersion("stable") > AwesomeVersion("latest"))
> False
print(AwesomeVersion("beta") > AwesomeVersion("dev"))
> False
```
## Contribute
**All** contributions are welcome!
1. Fork the repository
2. Clone the repository locally and open the devcontainer or use GitHub codespaces
3. Do your changes
4. Lint the files with `make lint`
5. Ensure all tests passes with `make test`
6. Ensure 100% coverage with `make coverage`
7. Commit your work, and push it to GitHub
8. Create a PR against the `main` branch
[awesomeversion_demo]: https://ludeeus.github.io/awesomeversion/ awesomeversion-25.5.0/awesomeversion/ 0000775 0000000 0000000 00000000000 15016047771 0017727 5 ustar 00root root 0000000 0000000 awesomeversion-25.5.0/awesomeversion/__init__.py 0000664 0000000 0000000 00000001031 15016047771 0022033 0 ustar 00root root 0000000 0000000 """Initialize the AwesomeVersion package."""
from .awesomeversion import AwesomeVersion, AwesomeVersionDiff
from .exceptions import (
AwesomeVersionCompareException,
AwesomeVersionException,
AwesomeVersionStrategyException,
)
from .strategy import COMPARABLE_STRATEGIES, AwesomeVersionStrategy
__all__ = [
"AwesomeVersion",
"AwesomeVersionCompareException",
"AwesomeVersionDiff",
"AwesomeVersionException",
"AwesomeVersionStrategy",
"AwesomeVersionStrategyException",
"COMPARABLE_STRATEGIES",
]
awesomeversion-25.5.0/awesomeversion/awesomeversion.py 0000664 0000000 0000000 00000042051 15016047771 0023351 0 ustar 00root root 0000000 0000000 """AwesomeVersion."""
from __future__ import annotations
from functools import cached_property
from typing import TYPE_CHECKING, Any, Dict
from warnings import warn
from .comparehandlers.container import compare_handler_container
from .comparehandlers.modifier import compare_handler_semver_modifier
from .comparehandlers.sections import compare_handler_sections
from .comparehandlers.simple import compare_handler_simple
from .exceptions import AwesomeVersionCompareException, AwesomeVersionStrategyException
from .strategy import (
VERSION_STRATEGIES,
VERSION_STRATEGIES_DICT,
AwesomeVersionStrategy,
AwesomeVersionStrategyDescription,
)
from .utils.regex import (
RE_DIGIT,
RE_MODIFIER,
RE_SIMPLE,
compile_regex,
generate_full_string_regex,
)
if TYPE_CHECKING:
from .typing import EnsureStrategyIterableType, EnsureStrategyType, VersionType
class AwesomeVersion(str):
"""
AwesomeVersion class.
"""
_version: str = ""
_modifier: str | None = None
_modifier_type: str | None = None
_sections: int | None = None
_ensure_strategy: EnsureStrategyIterableType = []
def __init__(
self, # pylint: disable=unused-argument
version: VersionType,
*,
ensure_strategy: EnsureStrategyType = None,
find_first_match: bool = False,
**kwargs: Any,
) -> None:
"""
Initialize AwesomeVersion.
**args**:
version:
The version to create a AwesomeVersion object from
**kwargs**:
ensure_strategy:
Match the AwesomeVersion object against spesific
strategies when creating if. If it does not match
AwesomeVersionStrategyException will be raised
find_first_match:
If True, the version given will be scanned for the first
match of the given ensure_strategy. Raises
AwesomeVersionStrategyException If it is not found
for any of the given strategies.
"""
self._version = (
version._version if isinstance(version, AwesomeVersion) else str(version)
)
if isinstance(self._version, str):
self._version = self._version.strip()
if find_first_match and not ensure_strategy:
warn(
"Can not use find_first_match without ensure_strategy, "
"this is ignored and will start raising an exception in 2025.",
stacklevel=2,
)
if ensure_strategy is not None:
self._ensure_strategy = ensure_strategy = (
ensure_strategy
if isinstance(ensure_strategy, (list, tuple))
else [ensure_strategy]
)
if AwesomeVersionStrategy.UNKNOWN in ensure_strategy:
raise AwesomeVersionStrategyException(
f"Can't use {AwesomeVersionStrategy.UNKNOWN.value} as ensure_strategy"
)
if find_first_match:
for strategy in self._ensure_strategy or []:
description = VERSION_STRATEGIES_DICT[strategy]
match = compile_regex(description.regex_string).search(
self._version
)
if match is not None:
self._version = match.group(0)
break
if self.strategy not in ensure_strategy:
raise AwesomeVersionStrategyException(
f"Strategy {self.strategy.value} does not match "
f"{[strategy.value for strategy in ensure_strategy]} for {version}"
)
if self._version and self._version[-1] == ".":
self._version = self._version[:-1]
str.__init__(self._version)
def __new__(
cls,
version: str,
*_: Any,
**__: Any,
) -> AwesomeVersion:
"""Create a new AwesomeVersion object."""
return super().__new__(cls, version)
def __enter__(self) -> AwesomeVersion:
return self
def __exit__(self, *_: Any, **__: Any) -> None:
pass
def __repr__(self) -> str:
return f""
def __str__(self) -> str:
return str(self._version)
def __eq__(self, compareto: VersionType) -> bool:
"""Check if equals to."""
if isinstance(compareto, (str, float, int)):
compareto = AwesomeVersion(compareto)
if not isinstance(compareto, AwesomeVersion):
raise AwesomeVersionCompareException("Not a valid AwesomeVersion object")
return self.string == compareto.string
def __lt__(self, compareto: VersionType) -> bool:
"""Check if less than."""
if isinstance(compareto, (str, float, int)):
compareto = AwesomeVersion(compareto)
if not isinstance(compareto, AwesomeVersion):
raise AwesomeVersionCompareException("Not a valid AwesomeVersion object")
if self.string == compareto.string:
return False
if AwesomeVersionStrategy.UNKNOWN in (self.strategy, compareto.strategy):
raise AwesomeVersionCompareException(
f"Can't compare <{self.strategy.value} {self._version}> and "
f"<{compareto.strategy.value} {compareto._version}>"
)
return self._compare_versions(compareto, self)
def __gt__(self, compareto: VersionType) -> bool:
"""Check if greater than."""
if isinstance(compareto, (str, float, int)):
compareto = AwesomeVersion(compareto)
if not isinstance(compareto, AwesomeVersion):
raise AwesomeVersionCompareException("Not a valid AwesomeVersion object")
if self.string == compareto.string:
return False
if AwesomeVersionStrategy.UNKNOWN in (self.strategy, compareto.strategy):
raise AwesomeVersionCompareException(
f"Can't compare <{self.strategy.value} {self._version}> and "
f"<{compareto.strategy.value} {compareto._version}>"
)
return self._compare_versions(self, compareto)
def __ne__(self, compareto: object) -> bool:
return not self.__eq__(compareto)
def __le__(self, compareto: object) -> bool:
return self.__eq__(compareto) or self.__lt__(compareto)
def __ge__(self, compareto: object) -> bool:
return self.__eq__(compareto) or self.__gt__(compareto)
def __sub__(self, compareto: object) -> AwesomeVersionDiff:
return self.diff(compareto)
def __hash__(self) -> int:
return str.__hash__(self.string)
def diff(self, compareto: VersionType) -> AwesomeVersionDiff:
"""Return a dictionary with differences between 2 AwesomeVersion objects."""
if isinstance(compareto, (str, float, int)):
compareto = AwesomeVersion(compareto)
if not isinstance(compareto, AwesomeVersion):
raise AwesomeVersionCompareException("Not a valid AwesomeVersion object")
return AwesomeVersionDiff(
{
"major": str(self.major) != str(compareto.major),
"minor": str(self.minor) != str(compareto.minor),
"patch": str(self.patch) != str(compareto.patch),
"modifier": self.modifier != compareto.modifier,
"strategy": self.strategy != compareto.strategy,
}
)
def in_range(self, lowest: VersionType, highest: VersionType) -> bool:
"""Check if version is in range."""
if isinstance(lowest, (str, float, int)):
lowest = AwesomeVersion(lowest)
if isinstance(highest, (str, float, int)):
highest = AwesomeVersion(highest)
if not isinstance(lowest, AwesomeVersion):
raise AwesomeVersionCompareException("Lowest version is not valid")
if not isinstance(highest, AwesomeVersion):
raise AwesomeVersionCompareException("Highest version is not valid")
return lowest <= self <= highest
def section(self, idx: int) -> int:
"""Return the value of the specified section of the version."""
if self.strategy == AwesomeVersionStrategy.HEXVER:
return int(self.string, 16) if idx == 0 else 0
if self.sections >= (idx + 1):
match = RE_DIGIT.match(self.string.split(".")[idx] or "")
if match and match.groups():
return int(match.group(1) or 0)
return 0
@staticmethod
def _compare_versions(version_a: AwesomeVersion, version_b: AwesomeVersion) -> bool:
"""Compare versions."""
for handler in (
compare_handler_container,
compare_handler_simple,
compare_handler_sections,
compare_handler_semver_modifier,
):
result = handler(version_a, version_b)
if result is not None:
return result
return False
@property
def string(self) -> str:
"""Return a string representation of the version."""
if not self._version:
return self._version
prefix = self.prefix
if prefix is None:
return self._version
return self._version[len(prefix) :]
@cached_property
def prefix(self) -> str | None:
"""Return the version prefix if any"""
version = self._version
for prefix in ("v", "V", "v.", "V."):
if version.startswith(prefix):
return prefix
return None
@property
def alpha(self) -> bool:
"""Return a bool to indicate alpha version."""
return "a" in self.modifier if self.modifier else False
@property
def beta(self) -> bool:
"""Return a bool to indicate beta version."""
return "b" in self.modifier if self.modifier else "beta" in self.string
@property
def dev(self) -> bool:
"""Return a bool to indicate dev version."""
return "d" in self.modifier if self.modifier else "dev" in self.string
@property
def release_candidate(self) -> bool:
"""Return a bool to indicate release candidate version."""
return "rc" in self.modifier if self.modifier else "rc" in self.string
@property
def sections(self) -> int:
"""Return a int representation of the number of sections in the version."""
if self._sections is not None:
return self._sections
if self.strategy == AwesomeVersionStrategy.SEMVER:
self._sections = 3
else:
modifier = self.modifier
self._sections = len(
[
section.split(self.modifier_type)[-1]
for section in self.string.split(".")
if section and (modifier is None or section != modifier)
]
)
return self._sections
@cached_property
def major(self) -> AwesomeVersion | None:
"""
Return a AwesomeVersion representation of the major version.
Will return None if the versions is not semver/buildver/calver/simplever/pep440.
"""
if self.strategy not in (
AwesomeVersionStrategy.SEMVER,
AwesomeVersionStrategy.BUILDVER,
AwesomeVersionStrategy.CALVER,
AwesomeVersionStrategy.SIMPLEVER,
AwesomeVersionStrategy.PEP440,
):
return None
return AwesomeVersion(self.section(0))
@cached_property
def minor(self) -> AwesomeVersion | None:
"""
Return a AwesomeVersion representation of the minor version.
Will return None if the versions is not semver/simplever/calver/pep440
Will return None if the version does not have at least 2 sections.
"""
if (
self.strategy
not in (
AwesomeVersionStrategy.SEMVER,
AwesomeVersionStrategy.CALVER,
AwesomeVersionStrategy.SIMPLEVER,
AwesomeVersionStrategy.PEP440,
)
or self.sections < 2
):
return None
return AwesomeVersion(self.section(1))
@cached_property
def patch(self) -> AwesomeVersion | None:
"""
Return a AwesomeVersion representation of the patch version.
Will return None if the versions is not semver/simplever/calver/pep440
Will return None if the version does not have at least 3 sections.
"""
if (
self.strategy
not in (
AwesomeVersionStrategy.SEMVER,
AwesomeVersionStrategy.CALVER,
AwesomeVersionStrategy.SIMPLEVER,
AwesomeVersionStrategy.PEP440,
)
or self.sections < 3
):
return None
return AwesomeVersion(self.section(2))
@property
def micro(self) -> AwesomeVersion | None:
"""Alias to self.patch"""
return self.patch
@property
def year(self) -> AwesomeVersion | None:
"""Alias to self.major, here to provide a better name for use in CalVer."""
return self.major
@property
def valid(self) -> bool:
"""Return True if the version is not UNKNOWN."""
return self.strategy != AwesomeVersionStrategy.UNKNOWN
@property
def modifier(self) -> str | None:
"""Return the modifier of the version if any."""
if self._modifier is not None:
return self._modifier
if self.strategy in (
AwesomeVersionStrategy.SPECIALCONTAINER,
AwesomeVersionStrategy.HEXVER,
):
return None
modifier_string = None
if (
self.strategy_description is not None
and self.strategy_description.strategy == AwesomeVersionStrategy.SEMVER
):
match = self.strategy_description.pattern.match(self.string)
if match and len(match.groups()) >= 4:
self._modifier = modifier_string = match.group(4)
else:
modifier_string = self.string.split(".")[-1]
if not modifier_string:
return None
match = RE_MODIFIER.match(modifier_string)
if match and len(match.groups()) >= 2:
self._modifier = match.group(2)
return self._modifier
@property
def modifier_type(self) -> str | None:
"""Return the modifier type of the version if any."""
if self._modifier_type is not None:
return self._modifier_type
if self.strategy == AwesomeVersionStrategy.HEXVER:
return None
match = RE_MODIFIER.match(self.modifier or "")
if match and len(match.groups()) >= 3:
self._modifier_type = match.group(3)
return self._modifier_type
@property
def strategy_description(self) -> AwesomeVersionStrategyDescription | None:
"""Return a string representation of the strategy."""
if self.strategy == AwesomeVersionStrategy.UNKNOWN:
return None
return VERSION_STRATEGIES_DICT[self.strategy]
@cached_property
def strategy(self) -> AwesomeVersionStrategy:
"""Return the version strategy."""
version_strategies: dict[
AwesomeVersionStrategy, AwesomeVersionStrategyDescription
] = {}
for strategy in self._ensure_strategy or []:
version_strategies[strategy] = VERSION_STRATEGIES_DICT[strategy]
for description in VERSION_STRATEGIES:
if description.strategy not in version_strategies:
version_strategies[description.strategy] = description
for description in version_strategies.values():
if description.pattern.match(self.string) is not None and (
description.validate is None or description.validate(self.string)
):
return description.strategy
return AwesomeVersionStrategy.UNKNOWN
@cached_property
def simple(self) -> bool:
"""Return True if the version string is simple."""
return generate_full_string_regex(RE_SIMPLE).match(self.string) is not None
class AwesomeVersionDiff:
"""Structured output of AwesomeVersion.diff"""
def __init__(self, changes: Dict[str, bool]) -> None:
"""Initialize the AwesomeVersionDiff."""
self._changes = changes
def __repr__(self) -> str:
return (
f"AwesomeVersionDiff(major={self.major}, minor={self.minor}, "
f"patch={self.patch}, modifier={self.modifier}, strategy={self.strategy})"
)
@property
def major(self) -> bool:
"""Return True if the major version has changed."""
return self._changes["major"]
@property
def minor(self) -> bool:
"""Return True if the minor version has changed."""
return self._changes["minor"]
@property
def patch(self) -> bool:
"""Return True if the patch version has changed."""
return self._changes["patch"]
@property
def modifier(self) -> bool:
"""Return True if the modifier version has changed."""
return self._changes["modifier"]
@property
def strategy(self) -> bool:
"""Return True if the strategy has changed."""
return self._changes["strategy"]
awesomeversion-25.5.0/awesomeversion/comparehandlers/ 0000775 0000000 0000000 00000000000 15016047771 0023076 5 ustar 00root root 0000000 0000000 awesomeversion-25.5.0/awesomeversion/comparehandlers/__init__.py 0000664 0000000 0000000 00000000071 15016047771 0025205 0 ustar 00root root 0000000 0000000 """Initialize the comparehandlers for AwesomeVersion."""
awesomeversion-25.5.0/awesomeversion/comparehandlers/container.py 0000664 0000000 0000000 00000001522 15016047771 0025432 0 ustar 00root root 0000000 0000000 """Special handler for container."""
from __future__ import annotations
from typing import TYPE_CHECKING
from ..strategy import AwesomeVersionStrategy
CONTAINER_VERSION_MAP = {"stable": 1, "beta": 2, "latest": 3, "dev": 4}
if TYPE_CHECKING:
from awesomeversion import AwesomeVersion
def compare_handler_container(
version_a: AwesomeVersion,
version_b: AwesomeVersion,
) -> bool | None:
"""Compare handler container."""
if version_a.strategy == AwesomeVersionStrategy.SPECIALCONTAINER:
if version_b.strategy != AwesomeVersionStrategy.SPECIALCONTAINER:
return True
return (
CONTAINER_VERSION_MAP[version_a.string]
> CONTAINER_VERSION_MAP[version_b.string]
)
if version_b.strategy == AwesomeVersionStrategy.SPECIALCONTAINER:
return False
return None
awesomeversion-25.5.0/awesomeversion/comparehandlers/modifier.py 0000664 0000000 0000000 00000003454 15016047771 0025254 0 ustar 00root root 0000000 0000000 """Special handler for modifier."""
from __future__ import annotations
from typing import TYPE_CHECKING
from ..strategy import VERSION_STRATEGIES_DICT, AwesomeVersionStrategy
from ..utils.regex import RE_MODIFIER
SEMVER_MODIFIER_MAP = {"dev": 0, "alpha": 1, "beta": 2, "rc": 3}
if TYPE_CHECKING:
from awesomeversion import AwesomeVersion
def compare_handler_semver_modifier(
version_a: AwesomeVersion,
version_b: AwesomeVersion,
) -> bool | None:
"""Compare handler sections."""
if AwesomeVersionStrategy.SEMVER not in (
version_a.strategy,
version_b.strategy,
) or (version_a.modifier_type is None or version_b.modifier_type is None):
return None
if version_a.modifier_type != version_b.modifier_type:
mod_a = SEMVER_MODIFIER_MAP.get(version_a.modifier_type)
mod_b = SEMVER_MODIFIER_MAP.get(version_b.modifier_type)
if mod_a is not None and mod_b is not None:
return mod_a > mod_b
ver_a_modifier, ver_b_modifier = None, None
semver_pattern = VERSION_STRATEGIES_DICT[AwesomeVersionStrategy.SEMVER].pattern
semver_match = semver_pattern.match(version_a.string)
if semver_match and len(semver_match.groups()) >= 4:
modifier_match = RE_MODIFIER.match(semver_match.group(4))
if modifier_match and len(modifier_match.groups()) >= 4:
ver_a_modifier = modifier_match.group(4)
semver_match = semver_pattern.match(version_b.string)
if semver_match and len(semver_match.groups()) >= 4:
modifier_match = RE_MODIFIER.match(semver_match.group(4))
if modifier_match and len(modifier_match.groups()) >= 4:
ver_b_modifier = modifier_match.group(4)
if not ver_a_modifier or not ver_b_modifier:
return True
return int(ver_a_modifier) > int(ver_b_modifier)
awesomeversion-25.5.0/awesomeversion/comparehandlers/sections.py 0000664 0000000 0000000 00000004776 15016047771 0025315 0 ustar 00root root 0000000 0000000 """Special handler for sections."""
from __future__ import annotations
from typing import TYPE_CHECKING
from ..utils.regex import RE_IS_SINGLE_DIGIT, RE_MODIFIER
if TYPE_CHECKING:
from ..awesomeversion import AwesomeVersion
MODIFIERS = {"rc": 3, "beta": 2, "b": 2, "alpha": 1, "a": 1, "dev": 0, "d": 0}
def compare_handler_sections(
version_a: AwesomeVersion,
version_b: AwesomeVersion,
) -> bool | None:
"""Compare handler sections."""
base = compare_base_sections(version_a, version_b)
if base is not None:
return base
return compare_modifier_section(version_a, version_b)
def compare_base_sections(
version_a: AwesomeVersion,
version_b: AwesomeVersion,
) -> bool | None:
"""Compare base sections between two AwesomeVersion objects."""
biggest = (
version_a.sections
if version_a.sections >= version_b.sections
else version_b.sections
)
for section in range(0, biggest):
ver_a_section = version_a.section(section)
ver_b_section = version_b.section(section)
if ver_a_section == ver_b_section:
continue
if ver_a_section > ver_b_section:
return True
if ver_a_section < ver_b_section:
return False
return None
def compare_modifier_section(
version_a: AwesomeVersion,
version_b: AwesomeVersion,
) -> bool | None:
"""Compare modifiers between two AwesomeVersion objects."""
if version_a.modifier is None and version_b.modifier is not None:
return True
if version_a.modifier is not None and version_b.modifier is not None:
version_a_modifier = RE_MODIFIER.match(version_a.string.split(".")[-1])
version_b_modifier = RE_MODIFIER.match(version_b.string.split(".")[-1])
if version_a_modifier and version_b_modifier:
if version_a_modifier.group(3) == version_b_modifier.group(3):
return int(version_a_modifier.group(4) or 0) > int(
version_b_modifier.group(4) or 0
)
mod_a = MODIFIERS.get(version_a_modifier.group(3))
mod_b = MODIFIERS.get(version_b_modifier.group(3))
if mod_a is not None and mod_b is not None:
return mod_a > mod_b
return version_a_modifier.group(3) > version_a_modifier.group(3)
if RE_IS_SINGLE_DIGIT.match(version_a.modifier) and RE_IS_SINGLE_DIGIT.match(
version_b.modifier
):
return int(version_a.modifier) > int(version_b.modifier)
return None
awesomeversion-25.5.0/awesomeversion/comparehandlers/simple.py 0000664 0000000 0000000 00000000737 15016047771 0024750 0 ustar 00root root 0000000 0000000 """Special handler for simple."""
from __future__ import annotations
from typing import TYPE_CHECKING
from .sections import compare_base_sections
if TYPE_CHECKING:
from awesomeversion import AwesomeVersion
def compare_handler_simple(
version_a: AwesomeVersion,
version_b: AwesomeVersion,
) -> bool | None:
"""Compare handler simple."""
if version_a.simple and version_b.simple:
return compare_base_sections(version_a, version_b)
return None
awesomeversion-25.5.0/awesomeversion/exceptions.py 0000664 0000000 0000000 00000000550 15016047771 0022462 0 ustar 00root root 0000000 0000000 """Exceptions for AwesomeVersion."""
class AwesomeVersionException(Exception):
"""Base AwesomeVersion exception."""
class AwesomeVersionCompareException(AwesomeVersionException):
"""Thrown when compare is not possible."""
class AwesomeVersionStrategyException(AwesomeVersionException):
"""Thrown when the expected strategy does not match."""
awesomeversion-25.5.0/awesomeversion/py.typed 0000664 0000000 0000000 00000000000 15016047771 0021414 0 ustar 00root root 0000000 0000000 awesomeversion-25.5.0/awesomeversion/strategy.py 0000664 0000000 0000000 00000005223 15016047771 0022145 0 ustar 00root root 0000000 0000000 """Strategies for AwesomeVersion."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
from typing import Pattern, Tuple
from .utils.regex import (
RE_BUILDVER,
RE_CALVER,
RE_HEXVER,
RE_PEP440,
RE_SEMVER,
RE_SIMPLE,
RE_SPECIAL_CONTAINER,
generate_full_string_regex,
)
from .utils.validate import value_is_base16
class AwesomeVersionStrategy(str, Enum):
"""Strategy enum."""
BUILDVER = "BuildVer"
CALVER = "CalVer"
HEXVER = "HexVer"
SEMVER = "SemVer"
SIMPLEVER = "SimpleVer"
PEP440 = "PEP 440"
UNKNOWN = "unknown"
SPECIALCONTAINER = "SpecialContainer"
@dataclass
class AwesomeVersionStrategyDescription:
"""Description of a strategy."""
strategy: AwesomeVersionStrategy
regex_string: str
pattern: Pattern[str]
validate: Callable[[str], bool] | None = None
COMPARABLE_STRATEGIES = [
strategy
for strategy in AwesomeVersionStrategy
if strategy
not in (AwesomeVersionStrategy.UNKNOWN, AwesomeVersionStrategy.SPECIALCONTAINER)
]
VERSION_STRATEGIES: Tuple[AwesomeVersionStrategyDescription, ...] = (
AwesomeVersionStrategyDescription(
strategy=AwesomeVersionStrategy.BUILDVER,
regex_string=RE_BUILDVER,
pattern=generate_full_string_regex(RE_BUILDVER),
),
AwesomeVersionStrategyDescription(
strategy=AwesomeVersionStrategy.CALVER,
regex_string=RE_CALVER,
pattern=generate_full_string_regex(RE_CALVER),
),
AwesomeVersionStrategyDescription(
strategy=AwesomeVersionStrategy.HEXVER,
regex_string=RE_HEXVER,
pattern=generate_full_string_regex(RE_HEXVER),
validate=value_is_base16,
),
AwesomeVersionStrategyDescription(
strategy=AwesomeVersionStrategy.SEMVER,
regex_string=RE_SEMVER,
pattern=generate_full_string_regex(RE_SEMVER),
),
AwesomeVersionStrategyDescription(
strategy=AwesomeVersionStrategy.SPECIALCONTAINER,
regex_string=RE_SPECIAL_CONTAINER,
pattern=generate_full_string_regex(RE_SPECIAL_CONTAINER),
),
AwesomeVersionStrategyDescription(
strategy=AwesomeVersionStrategy.SIMPLEVER,
regex_string=RE_SIMPLE,
pattern=generate_full_string_regex(RE_SIMPLE),
),
AwesomeVersionStrategyDescription(
strategy=AwesomeVersionStrategy.PEP440,
regex_string=RE_PEP440,
pattern=generate_full_string_regex(RE_PEP440),
),
)
VERSION_STRATEGIES_DICT: dict[
AwesomeVersionStrategy, AwesomeVersionStrategyDescription
] = {description.strategy: description for description in VERSION_STRATEGIES}
awesomeversion-25.5.0/awesomeversion/typing.py 0000664 0000000 0000000 00000000763 15016047771 0021621 0 ustar 00root root 0000000 0000000 """Custom types for AwesomeVersion."""
from __future__ import annotations
from typing import TYPE_CHECKING, List, Tuple, Union
from .strategy import AwesomeVersionStrategy
if TYPE_CHECKING:
from .awesomeversion import AwesomeVersion
VersionType = Union[str, float, int, object, "AwesomeVersion"]
EnsureStrategyIterableType = Union[
List[AwesomeVersionStrategy], Tuple[AwesomeVersionStrategy, ...]
]
EnsureStrategyType = Union[AwesomeVersionStrategy, EnsureStrategyIterableType, None]
awesomeversion-25.5.0/awesomeversion/utils/ 0000775 0000000 0000000 00000000000 15016047771 0021067 5 ustar 00root root 0000000 0000000 awesomeversion-25.5.0/awesomeversion/utils/__init__.py 0000664 0000000 0000000 00000000000 15016047771 0023166 0 ustar 00root root 0000000 0000000 awesomeversion-25.5.0/awesomeversion/utils/regex.py 0000664 0000000 0000000 00000002533 15016047771 0022556 0 ustar 00root root 0000000 0000000 """Regex utils for AwesomeVersion."""
import re
from typing import Pattern
# General purpose patterns
RE_IS_SINGLE_DIGIT = re.compile(r"^\d{1}$")
RE_DIGIT = re.compile(r"[a-z]*(\d+)[a-z]*")
RE_MODIFIER = re.compile(r"^((?:\d+\-|\d|))(([a-z]+)\.?(\d*))$")
# Version patterns
RE_CALVER = r"(\d{2}|\d{4})\.\d{1,2}?(\.?\d{1,2}?\.?)?(\.\d)?(\d*(\w+\d+)?)"
RE_SEMVER = (
r"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?"
)
RE_PEP440 = (
r"([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*" # Main segment
r"([-_\.]?(alpha|beta|c|pre|preview|a|b|rc)(0|[1-9][0-9]*))?" # Pre-release segment
r"([-_\.]?(post|r|rev)(0|[1-9][0-9]*))?" # Post-release segment
r"([-_\.]?(d|dev)(0|[1-9][0-9]*))?" # Development release segment
r"(?:\+([a-z0-9]+(?:[-_\.][a-z0-9]+)*))?" # Local version segment
)
RE_BUILDVER = r"\d+"
RE_HEXVER = r"0x[A-Fa-f0-9]+"
RE_SPECIAL_CONTAINER = r"(latest|dev|stable|beta)"
RE_SIMPLE = r"[v|V]?((\d+)(\.\d+)+)"
def compile_regex(pattern: str) -> Pattern[str]:
"""Compile a regex."""
return re.compile(pattern)
def generate_full_string_regex(string: str) -> Pattern[str]:
"""Generate a regex that matches the full string."""
return compile_regex(r"^" + string + r"$")
awesomeversion-25.5.0/awesomeversion/utils/validate.py 0000664 0000000 0000000 00000000306 15016047771 0023231 0 ustar 00root root 0000000 0000000 """Utils to validate."""
def value_is_base16(value: str) -> bool:
"""Check if a value is base16."""
try:
int(value, 16)
except ValueError:
return False
return True
awesomeversion-25.5.0/benchmarks/ 0000775 0000000 0000000 00000000000 15016047771 0016776 5 ustar 00root root 0000000 0000000 awesomeversion-25.5.0/benchmarks/__init__.py 0000664 0000000 0000000 00000000037 15016047771 0021107 0 ustar 00root root 0000000 0000000 """Init benchmarks package."""
awesomeversion-25.5.0/benchmarks/const.py 0000664 0000000 0000000 00000000061 15016047771 0020473 0 ustar 00root root 0000000 0000000 """Benchmark constants."""
DEFAULT_RUNS = 1_000
awesomeversion-25.5.0/benchmarks/test_compare.py 0000664 0000000 0000000 00000002643 15016047771 0022042 0 ustar 00root root 0000000 0000000 """Compare benchmarks for AwesomeVersion."""
import pytest
from pytest_codspeed import BenchmarkFixture
from awesomeversion import AwesomeVersion
from .const import DEFAULT_RUNS
@pytest.mark.parametrize(
"input_a,operator, input_b",
(
pytest.param("9999", ">", "1", id="9999>1"),
pytest.param("9999", ">", "2020.1.1", id="9999>2020.1.1"),
pytest.param("1.2.3b6", ">", "1.2.3.dev4", id="1.2.3b6>1.2.3.dev4"),
pytest.param("1.2.3", "==", "1.2.3", id="1.2.3==1.2.3"),
pytest.param("1.2.3", "!=", "3.2.1", id="1.2.3!=3.2.1"),
),
)
def test_compare(
benchmark: BenchmarkFixture,
input_a: str,
operator: str,
input_b: str,
) -> None:
"""Benchmark for AwesomeVersion comparison."""
obj = AwesomeVersion(input_a)
if operator == ">":
@benchmark
def _run_benchmark() -> None:
for _ in range(DEFAULT_RUNS):
assert obj > input_b
elif operator == "<":
@benchmark
def _run_benchmark() -> None:
for _ in range(DEFAULT_RUNS):
assert obj < input_b
elif operator == "==":
@benchmark
def _run_benchmark() -> None:
for _ in range(DEFAULT_RUNS):
assert obj == input_b
elif operator == "!=":
@benchmark
def _run_benchmark() -> None:
for _ in range(DEFAULT_RUNS):
assert obj != input_b
awesomeversion-25.5.0/benchmarks/test_constructor.py 0000664 0000000 0000000 00000002336 15016047771 0023000 0 ustar 00root root 0000000 0000000 """Constructor benchmarks for AwesomeVersion."""
from __future__ import annotations
from typing import Any
import pytest
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
from .const import DEFAULT_RUNS
semver_first = {
"ensure_strategy": AwesomeVersionStrategy.SEMVER,
"find_first_match": True,
}
@pytest.mark.benchmark
@pytest.mark.parametrize(
"version,constructor_kv",
[
pytest.param("9999", {}, id="9999-default"),
pytest.param("v1.2.3", {}, id="v1.2.3-default"),
pytest.param(
"lorem_ipsum1.2.3",
semver_first,
id="lorem_ipsum1.2.3-semver-first",
),
pytest.param("dev", {}, id="dev-default"),
pytest.param(1, {}, id="1-as-int-default"),
pytest.param("1", {}, id="1-as-str-default"),
pytest.param(3.14, {}, id="3.14-as-float-default"),
pytest.param(
"1.2.3",
semver_first,
id="1.2.3-semver-first",
),
],
)
def test_constructor(
version: str | int | float, constructor_kv: dict[str, Any]
) -> None:
"""Benchmark for AwesomeVersion constructor."""
for _ in range(DEFAULT_RUNS):
assert AwesomeVersion(version, **constructor_kv)
awesomeversion-25.5.0/benchmarks/test_properties.py 0000664 0000000 0000000 00000002515 15016047771 0022606 0 ustar 00root root 0000000 0000000 """Property benchmarks for AwesomeVersion."""
from __future__ import annotations
import pytest
from pytest_codspeed import BenchmarkFixture
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
from .const import DEFAULT_RUNS
semver_first = {
"ensure_strategy": AwesomeVersionStrategy.SEMVER,
"find_first_match": True,
}
@pytest.mark.parametrize(
"version,class_property",
(
*[(version, "prefix") for version in ("v1.2.3", "v.1.2.3", "1.2.3")],
*[(version, "modifier") for version in ("1.2.3-dev2", "1.2.3dev2")],
*[(version, "modifier_type") for version in ("1.2.3.dev0", "1.2.3.beta0")],
*[(version, "strategy") for version in ("1.2.3", "2099.1.1", "999")],
*[
(version, "strategy_description")
for version in ("1.2.3", "2099.1.1", "999")
],
*[
(version, segment)
for version in ("1.2.3", "123", "0.1.2.3")
for segment in ("major", "minor", "patch")
],
),
)
def test_property(
benchmark: BenchmarkFixture,
version: str | int | float,
class_property: str,
) -> None:
"""Benchmark for AwesomeVersion properties."""
obj = AwesomeVersion(version)
@benchmark
def _run_benchmark() -> None:
for _ in range(DEFAULT_RUNS):
getattr(obj, class_property)
awesomeversion-25.5.0/demo/ 0000775 0000000 0000000 00000000000 15016047771 0015605 5 ustar 00root root 0000000 0000000 awesomeversion-25.5.0/demo/index.html 0000664 0000000 0000000 00000014555 15016047771 0017614 0 ustar 00root root 0000000 0000000
AwesomeVersion Demo
{
"packages": ["awesomeversion"]
}
from awesomeversion import AwesomeVersion
import json
def _diff(versions: dict[str, AwesomeVersion]):
return json.dumps(
{
f"{versions['a'].string}>{versions['b'].string}": safe_compare(
versions["a"], versions["b"]
),
f"{versions['b'].string}>{versions['a'].string}": safe_compare(
versions["b"], versions["a"]
),
"diff": {
"major": versions["a"].major != versions["b"].major,
"minor": versions["a"].minor != versions["b"].minor,
"patch": versions["a"].patch != versions["b"].patch,
"modifier": versions["a"].modifier != versions["b"].modifier,
"strategy": versions["a"].strategy != versions["b"].strategy,
},
},
indent=2,
)
def safe_compare(a, b):
try:
return a > b
except Exception:
return None
def version_json(version):
return json.dumps(
{
"string": version.string,
"alpha": version.alpha,
"beta": version.beta,
"dev": version.dev,
"major": version.major,
"micro": version.micro,
"minor": version.minor,
"modifier_type": version.modifier_type,
"modifier": version.modifier,
"patch": version.patch,
"prefix": version.prefix,
"release_candidate": version.release_candidate,
"sections": version.sections,
"simple": version.simple,
"strategy": version.strategy,
"valid": version.valid,
"year": version.year,
},
indent=2,
)
def extract_awesomeversion_properties(*args, **kwargs):
versions = {
"a": AwesomeVersion(Element("version-input-a").element.value.strip() or None),
"b": AwesomeVersion(Element("version-input-b").element.value.strip() or None),
}
for key, version in versions.items():
if version.string == "None":
Element(f"properties-output-{key}").element.innerText = ""
Element("properties-output-diff").element.innerText = ""
continue
Element(
f"properties-output-{key}"
).element.innerText = (
f"Version {key.upper()} properties:\n{version_json(version)}"
)
if versions["a"].string == "None" or versions["b"].string == "None":
Element("properties-output-diff").element.innerText = ""
return
if "unknown" not in (versions["a"].strategy, versions["b"].strategy):
Element("properties-output-diff").element.innerText = f"Compare:\n{_diff(versions)}"
else:
Element("properties-output-diff").element.innerText = "Not valid for compare"