├── tests ├── unit │ ├── __init__.py │ ├── test_main.py │ └── test_dunamai.py ├── integration │ ├── __init__.py │ └── test_dunamai.py └── archival │ ├── hg-tagged │ ├── .hgtags │ └── .hg_archival.txt │ ├── hg-untagged │ └── .hg_archival.txt │ ├── git-untagged │ └── .git_archival.json │ ├── git-tagged-post │ └── .git_archival.json │ └── git-tagged │ └── .git_archival.json ├── .gitattributes ├── docs ├── index.md ├── requirements.txt └── dunamai.1 ├── dunamai ├── py.typed └── __main__.py ├── .coveragerc ├── .flake8 ├── .editorconfig ├── .readthedocs.yaml ├── .vscode ├── settings.json └── extensions.json ├── mkdocs.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── CONTRIBUTING.md ├── pyproject.toml ├── tasks.py ├── .github └── workflows │ └── main.yaml ├── README.md ├── CHANGELOG.md └── poetry.legacy.lock /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | text=auto 2 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ::: dunamai 4 | -------------------------------------------------------------------------------- /dunamai/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | dunamai 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .venv 3 | max-line-length = 100 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocstrings[python] 2 | mkdocs-material 3 | -------------------------------------------------------------------------------- /tests/archival/hg-tagged/.hgtags: -------------------------------------------------------------------------------- 1 | d80ce21ea5d88b577ee32661ffd3a5b3834f14e0 v0.1.0 2 | 5255faf9dfb276bbe189562e9a1194f43506c21c v0.1.1 3 | 1662f75d4df8f05506c2bc9d9801f80d879bf3f0 foo bar 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.{feature,json,md,yaml,yml}] 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /tests/archival/hg-tagged/.hg_archival.txt: -------------------------------------------------------------------------------- 1 | repo: 25e474af1332ed4fff9351c70ef8f36352c013f2 2 | node: cf36273384e558411364a3a973aaa0cc08e48aea 3 | branch: default 4 | latesttag: foo bar 5 | latesttagdistance: 1 6 | changessincelatesttag: 1 7 | -------------------------------------------------------------------------------- /tests/archival/hg-untagged/.hg_archival.txt: -------------------------------------------------------------------------------- 1 | repo: 25e474af1332ed4fff9351c70ef8f36352c013f2 2 | node: 25e474af1332ed4fff9351c70ef8f36352c013f2 3 | branch: default 4 | latesttag: null 5 | latesttagdistance: 1 6 | changessincelatesttag: 1 7 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | mkdocs: 9 | configuration: mkdocs.yaml 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | -------------------------------------------------------------------------------- /tests/archival/git-untagged/.git_archival.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash-full": "8fe614dbf9e767e70442ab8f56e99bd08d7e782d", 3 | "hash-short": "8fe614d", 4 | "timestamp": "2022-11-07T07:07:50+08:00", 5 | "refs": "HEAD -> master, feature/foo", 6 | "describe": "" 7 | } 8 | -------------------------------------------------------------------------------- /tests/archival/git-tagged-post/.git_archival.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash-full": "1b57ff77e01728b301088b9d57e3922d156c7ec3", 3 | "hash-short": "1b57ff7", 4 | "timestamp": "2022-11-07T07:16:59+08:00", 5 | "refs": "HEAD -> master", 6 | "describe": "v0.1.0-1-g1b57ff7" 7 | } 8 | -------------------------------------------------------------------------------- /tests/archival/git-tagged/.git_archival.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash-full": "8fe614dbf9e767e70442ab8f56e99bd08d7e782d", 3 | "hash-short": "8fe614d", 4 | "timestamp": "2022-11-07T07:07:50+08:00", 5 | "refs": "HEAD -> master, feature/foo", 6 | "describe": "v0.1.0" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.defaultInterpreterPath": "${workspaceFolder}/.venv", 3 | "yaml.format.enable": true, 4 | "python.testing.pytestArgs": [ 5 | "." 6 | ], 7 | "python.testing.unittestEnabled": false, 8 | "python.testing.pytestEnabled": true, 9 | } 10 | -------------------------------------------------------------------------------- /mkdocs.yaml: -------------------------------------------------------------------------------- 1 | site_name: Dunamai Docs 2 | 3 | theme: 4 | name: material 5 | 6 | plugins: 7 | - search 8 | - mkdocstrings: 9 | handlers: 10 | python: 11 | options: 12 | show_root_toc_entry: false 13 | docstring_style: sphinx 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .cache/ 3 | .coverage 4 | .coverage.* 5 | .env/ 6 | .idea/ 7 | .mypy_cache/ 8 | .pytest_cache/ 9 | .tox/ 10 | .venv/ 11 | *.egg-info/ 12 | /setup.py 13 | build/ 14 | dist/ 15 | htmlcov/ 16 | pip-wheel-metadata/ 17 | # Used by github codespaces 18 | pythonenv*/ 19 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "codezombiech.gitignore", 4 | "EditorConfig.EditorConfig", 5 | "ms-python.mypy-type-checker", 6 | "ms-python.python", 7 | "redhat.vscode-yaml", 8 | "sidneys1.gitconfig", 9 | "streetsidesoftware.code-spell-checker", 10 | "tamasfe.even-better-toml", 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.2.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - repo: https://github.com/Lucas-C/pre-commit-hooks 8 | rev: v1.1.13 9 | hooks: 10 | - id: forbid-tabs 11 | - repo: https://github.com/mtkennerly/pre-commit-hooks 12 | rev: v0.3.0 13 | hooks: 14 | - id: poetry-black 15 | - id: poetry-mypy 16 | - id: poetry-ruff 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Matthew T. Kennerly (mtkennerly) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | This project is managed using [Poetry](https://poetry.eustace.io). 3 | Development requires Python 3.7+. 4 | 5 | * If you want to take advantage of the default VSCode integration, 6 | then first configure Poetry to make its virtual environment in the repository: 7 | ``` 8 | poetry config virtualenvs.in-project true 9 | ``` 10 | * After cloning the repository, activate the tooling: 11 | ``` 12 | poetry install 13 | poetry run pre-commit install 14 | ``` 15 | * Run unit tests: 16 | ``` 17 | poetry run pytest --cov 18 | ``` 19 | * Render documentation: 20 | ``` 21 | pipx install mkdocs 22 | pipx runpip mkdocs install -r docs/requirements.txt 23 | mkdocs serve 24 | ``` 25 | 26 | ## VCS setup 27 | Some of the VCS tools tested require a minimum configuration to work. 28 | Here is an example of how to configure them: 29 | 30 | * Git: 31 | ``` 32 | git config --global user.name "foo" 33 | git config --global user.email "foo@example.com" 34 | ``` 35 | * Darcs: 36 | * Set the `DARCS_EMAIL` environment variable (e.g., `foo `). 37 | * Bazaar: 38 | ``` 39 | bzr whoami 'foo ' 40 | ``` 41 | * Pijul: 42 | ``` 43 | pijul key generate test 44 | ``` 45 | 46 | ## Release 47 | * Run `invoke prerelease` 48 | * Verify the changes and `git add` them 49 | * Run `invoke release` 50 | * Create a release on GitHub for the new tag and attach the artifacts from `dist` 51 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "dunamai" 3 | version = "1.25.0" 4 | description = "Dynamic version generation" 5 | license = "MIT" 6 | authors = ["Matthew T. Kennerly "] 7 | readme = "README.md" 8 | repository = "https://github.com/mtkennerly/dunamai" 9 | keywords = ["version", "versioning", "dynamic"] 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Intended Audience :: End Users/Desktop", 13 | "License :: OSI Approved :: MIT License", 14 | "Natural Language :: English", 15 | "Programming Language :: Python", 16 | "Programming Language :: Python :: 3", 17 | ] 18 | include = [ 19 | { path = "CHANGELOG.md", format = "sdist" }, 20 | { path = "tests", format = "sdist" }, 21 | ] 22 | 23 | [tool.poetry.dependencies] 24 | python = ">=3.5" 25 | packaging = ">=20.9" # 20.9 is the last with Python 3.5 compat 26 | importlib-metadata = {version = ">=1.6.0", python = "<3.8"} # 2.1.1 is the last with Python 3.5 compat 27 | 28 | [tool.poetry.dev-dependencies] 29 | pytest = [ 30 | { version = "^7.2", python = "^3.7" }, 31 | { version = "^3.0", python = ">=3.5,<3.7" }, 32 | ] 33 | pre-commit = { version = "^2.20", python = "^3.7" } 34 | pytest-cov = [ 35 | { version = "^4.0", python = "^3.7" }, 36 | { version = "^2.6", python = ">=3.5,<3.7" }, 37 | ] 38 | black = { version = "22.1.0", python = "^3.7" } 39 | mypy = { version = "^0.982", python = "^3.7" } 40 | ruff = {version = "^0.0.272", python = "^3.7"} 41 | argparse-manpage = {version = "^4.6", python = ">=3.7"} 42 | 43 | [tool.poetry.scripts] 44 | dunamai = 'dunamai.__main__:main' 45 | 46 | [tool.black] 47 | line-length = 120 48 | 49 | [tool.ruff] 50 | line-length = 120 51 | extend-select = ["W605", "N"] 52 | ignore = ["E501"] 53 | 54 | [build-system] 55 | requires = ["poetry-core>=1.0.0"] 56 | build-backend = "poetry.core.masonry.api" 57 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import re 3 | import shutil 4 | import sys 5 | from pathlib import Path 6 | 7 | from invoke import task 8 | 9 | ROOT = Path(__file__).parent 10 | 11 | 12 | def get_version() -> str: 13 | for line in (ROOT / "pyproject.toml").read_text("utf-8").splitlines(): 14 | if line.startswith("version ="): 15 | return line.replace("version = ", "").strip('"') 16 | 17 | raise RuntimeError("Could not determine version") 18 | 19 | 20 | def replace_pattern_in_file(file: Path, old: str, new: str, count: int = 1): 21 | content = file.read_text("utf-8") 22 | updated = re.sub(old, new, content, count=count) 23 | file.write_text(updated, "utf-8") 24 | 25 | 26 | def confirm(prompt: str): 27 | response = input(f"Confirm by typing '{prompt}': ") 28 | if response.lower() != prompt.lower(): 29 | sys.exit(1) 30 | 31 | 32 | @task 33 | def build(ctx, clean=True): 34 | with ctx.cd(ROOT): 35 | if clean: 36 | shutil.rmtree("dist", ignore_errors=True) 37 | ctx.run("poetry build") 38 | 39 | 40 | @task 41 | def install(ctx): 42 | ctx.run("pip uninstall -y dunamai") 43 | build(ctx) 44 | wheel = next(ROOT.glob("dist/*.whl")) 45 | ctx.run('pip install "{}"'.format(wheel)) 46 | 47 | 48 | @task 49 | def docs(ctx): 50 | version = get_version() 51 | manpage = "docs/dunamai.1" 52 | 53 | args = [ 54 | "poetry", 55 | "run", 56 | "argparse-manpage", 57 | "--pyfile", 58 | "dunamai/__main__.py", 59 | "--function", 60 | "get_parser", 61 | "--project-name", 62 | "dunamai", 63 | "--prog", 64 | "dunamai", 65 | "--version", 66 | version, 67 | "--author", 68 | "Matthew T. Kennerly (mtkennerly)", 69 | "--url", 70 | "https://github.com/mtkennerly/dunamai", 71 | "--format", 72 | "single-commands-section", 73 | "--output", 74 | manpage, 75 | "--manual-title", 76 | "Dunamai", 77 | ] 78 | 79 | # Join manually to avoid issues with single quotes on Windows using `shlex.join` 80 | joined = " ".join(arg if " " not in arg else f'"{arg}"' for arg in args) 81 | 82 | ctx.run(joined) 83 | 84 | 85 | @task 86 | def prerelease(ctx, new_version): 87 | date = dt.datetime.now().strftime("%Y-%m-%d") 88 | 89 | replace_pattern_in_file( 90 | ROOT / "pyproject.toml", 91 | 'version = ".+"', 92 | f'version = "{new_version}"', 93 | ) 94 | 95 | replace_pattern_in_file( 96 | ROOT / "CHANGELOG.md", 97 | "## Unreleased", 98 | f"## v{new_version} ({date})", 99 | ) 100 | 101 | build(ctx) 102 | docs(ctx) 103 | 104 | 105 | @task 106 | def release(ctx): 107 | version = get_version() 108 | 109 | confirm(f"release {version}") 110 | 111 | ctx.run(f'git commit -m "Release v{version}"') 112 | ctx.run(f'git tag v{version} -m "Release"') 113 | ctx.run("git push") 114 | ctx.run(f"git push origin tag v{version}") 115 | 116 | ctx.run("poetry publish") 117 | -------------------------------------------------------------------------------- /tests/unit/test_main.py: -------------------------------------------------------------------------------- 1 | from argparse import Namespace 2 | 3 | import pytest 4 | 5 | from dunamai import _run_cmd 6 | from dunamai.__main__ import parse_args, VERSION_SOURCE_PATTERN 7 | 8 | 9 | def test__parse_args__from(): 10 | assert parse_args(["from", "any"]) == Namespace( 11 | command="from", 12 | vcs="any", 13 | pattern=VERSION_SOURCE_PATTERN, 14 | dirty=False, 15 | metadata=None, 16 | format=None, 17 | style=None, 18 | latest_tag=False, 19 | tag_dir="tags", 20 | debug=False, 21 | bump=False, 22 | tagged_metadata=False, 23 | tag_branch=None, 24 | full_commit=False, 25 | strict=False, 26 | path=None, 27 | pattern_prefix=None, 28 | ignore_untracked=False, 29 | commit_length=None, 30 | commit_prefix=None, 31 | escape_with=None, 32 | ) 33 | assert parse_args(["from", "git"]).vcs == "git" 34 | assert parse_args(["from", "git", "--tag-branch", "foo"]).tag_branch == "foo" 35 | assert parse_args(["from", "git", "--full-commit"]).full_commit is True 36 | assert parse_args(["from", "mercurial"]).vcs == "mercurial" 37 | assert parse_args(["from", "mercurial", "--full-commit"]).full_commit is True 38 | assert parse_args(["from", "darcs"]).vcs == "darcs" 39 | assert parse_args(["from", "subversion"]).vcs == "subversion" 40 | assert parse_args(["from", "subversion"]).tag_dir == "tags" 41 | assert parse_args(["from", "bazaar"]).vcs == "bazaar" 42 | assert parse_args(["from", "fossil"]).vcs == "fossil" 43 | assert parse_args(["from", "pijul"]).vcs == "pijul" 44 | assert parse_args(["from", "any", "--pattern", r"\d+"]).pattern == r"\d+" 45 | assert parse_args(["from", "any", "--metadata"]).metadata is True 46 | assert parse_args(["from", "any", "--no-metadata"]).metadata is False 47 | assert parse_args(["from", "any", "--dirty"]).dirty is True 48 | assert parse_args(["from", "any", "--format", "v{base}"]).format == "v{base}" 49 | assert parse_args(["from", "any", "--style", "pep440"]).style == "pep440" 50 | assert parse_args(["from", "any", "--style", "semver"]).style == "semver" 51 | assert parse_args(["from", "any", "--latest-tag"]).latest_tag is True 52 | assert parse_args(["from", "any", "--tag-dir", "foo"]).tag_dir == "foo" 53 | assert parse_args(["from", "any", "--tag-branch", "foo"]).tag_branch == "foo" 54 | assert parse_args(["from", "any", "--full-commit"]).full_commit is True 55 | assert parse_args(["from", "any", "--debug"]).debug is True 56 | assert parse_args(["from", "any", "--tagged-metadata"]).tagged_metadata is True 57 | assert parse_args(["from", "any", "--strict"]).strict is True 58 | assert parse_args(["from", "any", "--path", "/tmp"]).path == "/tmp" 59 | assert parse_args(["from", "any", "--pattern-prefix", "foo-"]).pattern_prefix == "foo-" 60 | assert parse_args(["from", "any", "--ignore-untracked"]).ignore_untracked is True 61 | assert parse_args(["from", "any", "--commit-length", "10"]).commit_length == 10 62 | assert parse_args(["from", "any", "--commit-prefix", "x"]).commit_prefix == "x" 63 | assert parse_args(["from", "any", "--escape-with", "x"]).escape_with == "x" 64 | assert parse_args(["from", "subversion", "--tag-dir", "foo"]).tag_dir == "foo" 65 | 66 | with pytest.raises(SystemExit): 67 | parse_args(["from", "unknown"]) 68 | 69 | 70 | def test__parse_args__check(): 71 | assert parse_args(["check", "0.1.0"]) == Namespace(command="check", version="0.1.0", style="pep440") 72 | assert parse_args(["check", "0.1.0", "--style", "semver"]).style == "semver" 73 | assert parse_args(["check", "0.1.0", "--style", "pvp"]).style == "pvp" 74 | 75 | with pytest.raises(SystemExit): 76 | parse_args(["check", "0.1.0", "--style", "unknown"]) 77 | 78 | 79 | def test__cli_check(): 80 | _run_cmd("dunamai check 0.01.0", where=None) 81 | _run_cmd("dunamai check v0.1.0", where=None, codes=[1]) 82 | _run_cmd("dunamai check 0.01.0 --style semver", where=None, codes=[1]) 83 | _run_cmd("dunamai check", where=None, codes=[1]) 84 | _run_cmd("echo 0.01.0 | dunamai check", where=None, shell=True) 85 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | - push 3 | - pull_request 4 | 5 | name: Main 6 | 7 | jobs: 8 | pre-commit: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-python@v5 13 | with: 14 | python-version: '3.10' 15 | - run: | 16 | pip install poetry 17 | poetry install 18 | poetry run pre-commit run --all-files --show-diff-on-failure 19 | 20 | test: 21 | runs-on: ubuntu-22.04 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | python-version: 26 | # - '3.5' 27 | # - '3.6' 28 | - '3.7' 29 | - '3.8' 30 | - '3.9' 31 | - '3.10' 32 | - '3.11' 33 | - '3.12' 34 | - '3.13' 35 | git-version: 36 | - default # 2.28.0 or newer 37 | include: 38 | - python-version: '3.7' 39 | git-version: '2.21.0' # https://lore.kernel.org/git/CAKqNo6RJqp94uLMf8Biuo=ZvMZB9Mq6RRMrUgsLW4u1ks+mnOA@mail.gmail.com/T/#u 40 | - python-version: '3.7' 41 | git-version: '2.7.0' 42 | - python-version: '3.7' 43 | git-version: '2.2.0' 44 | - python-version: '3.7' 45 | git-version: '1.8.2.3' 46 | name: test (python = ${{ matrix.python-version }}, git = ${{ matrix.git-version }}) 47 | env: 48 | DARCS_EMAIL: foo 49 | steps: 50 | - if: ${{ matrix.python-version == '3.5' }} 51 | run: | 52 | echo "PIP_TRUSTED_HOST=pypi.python.org pypi.org files.pythonhosted.org" >> $GITHUB_ENV 53 | - uses: actions/checkout@v4 54 | - uses: actions/setup-python@v5 55 | with: 56 | python-version: ${{ matrix.python-version }} 57 | # - uses: dtolnay/rust-toolchain@1.65.0 58 | # - uses: Swatinem/rust-cache@v2 59 | - if: ${{ matrix.git-version != 'default' }} 60 | env: 61 | NO_OPENSSL: 'yes' 62 | run: | 63 | sudo apt-get update 64 | sudo apt-get install dh-autoreconf libcurl4-gnutls-dev libexpat1-dev gettext libz-dev libssl-dev 65 | curl -o git.tar.gz https://mirrors.edge.kernel.org/pub/software/scm/git/git-${{ matrix.git-version }}.tar.gz 66 | tar -zxf git.tar.gz 67 | cd git-${{ matrix.git-version }} 68 | make configure 69 | ./configure --prefix=/usr 70 | sudo make install 71 | which git 72 | git --version 73 | - run: | 74 | export PATH="$PATH:/opt" 75 | sudo apt-get update 76 | sudo apt-get install -y darcs bzr 77 | # For Pijul: 78 | # sudo apt-get install -y libxxhash-dev libzstd-dev expect 79 | 80 | # rehosted because the official site deletes older artifacts: 81 | curl -L -o ~/fossil.tgz https://github.com/mtkennerly/storage/raw/06e29a4005b24a65bc7d639c0aa1fc152a85d0b7/software/fossil-linux-x64-2.13.tar.gz 82 | tar -xvf ~/fossil.tgz -C /opt 83 | # TODO: Re-enable Pijul tests once this is fixed: https://nest.pijul.com/pijul/pijul/discussions/777 84 | # - name: Prepare Pijul 85 | # run: | 86 | # # Same as default features, but without openssl: 87 | # cargo install pijul --version 1.0.0-beta.2 --no-default-features --features keep-changes 88 | # expect -c 'spawn pijul key generate test ; expect "Password for the new key (press enter to leave it unencrypted):" ; send -- "\r" ; expect eof' 89 | - run: | 90 | export PATH="$PATH:/opt" 91 | 92 | git config --global user.name "foo" 93 | git config --global user.email "foo@example.com" 94 | bzr whoami 'foo ' 95 | - if: ${{ matrix.python-version == '3.5' || matrix.python-version == '3.6' }} 96 | run: | 97 | cp poetry.legacy.lock poetry.lock 98 | - if: ${{ matrix.python-version == '3.12' || matrix.python-version == '3.13' }} 99 | run: | 100 | rm poetry.lock 101 | - run: | 102 | pip install poetry 103 | poetry install 104 | poetry run pytest --verbose --cov --cov-report term-missing 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Dunamai 3 | Dunamai is a Python 3.5+ library and command line tool for producing dynamic, 4 | standards-compliant version strings, derived from tags in your version control system. 5 | This facilitates uniquely identifying nightly or per-commit builds in continuous integration 6 | and releasing new versions of your software simply by creating a tag. 7 | 8 | Dunamai is also available as a [GitHub Action](https://github.com/marketplace/actions/run-dunamai). 9 | 10 | ## Features 11 | * Version control system support: 12 | * [Git](https://git-scm.com) (2.7.0+ is recommended, but versions as old as 1.8.2.3 will work with some reduced functionality) 13 | * [Mercurial](https://www.mercurial-scm.org) 14 | * [Darcs](http://darcs.net) 15 | * [Subversion](https://subversion.apache.org) 16 | * [Bazaar](https://bazaar.canonical.com/en) 17 | * [Fossil](https://www.fossil-scm.org/home/doc/trunk/www/index.wiki) 18 | * [Pijul](https://pijul.org) 19 | * Version styles: 20 | * [PEP 440](https://www.python.org/dev/peps/pep-0440) 21 | * [Semantic Versioning](https://semver.org) 22 | * [Haskell Package Versioning Policy](https://pvp.haskell.org) 23 | * Custom output formats 24 | * Can be used for projects written in any programming language. 25 | For Python, this means you do not need a setup.py. 26 | 27 | ## Usage 28 | ### Installation 29 | ``` 30 | pip install dunamai 31 | ``` 32 | 33 | ### CLI 34 | ```console 35 | # Suppose you are on commit g29045e8, 7 commits after the v0.2.0 tag. 36 | 37 | # Auto-detect the version control system and generate a version: 38 | $ dunamai from any 39 | 0.2.0.post7.dev0+g29045e8 40 | 41 | # Or use an explicit VCS and style: 42 | $ dunamai from git --no-metadata --style semver 43 | 0.2.0-post.7 44 | 45 | # Custom formats: 46 | $ dunamai from any --format "v{base}+{distance}.{commit}" 47 | v0.2.0+7.g29045e8 48 | 49 | # If you'd prefer to frame the version in terms of progress toward the next 50 | # release rather than distance from the latest one, you can bump it: 51 | $ dunamai from any --bump 52 | 0.2.1.dev7+g29045e8 53 | 54 | # Validation of custom formats: 55 | $ dunamai from any --format "v{base}" --style pep440 56 | Version 'v0.2.0' does not conform to the PEP 440 style 57 | 58 | # Validate your own freeform versions: 59 | $ dunamai check 0.01.0 --style semver 60 | Version '0.01.0' does not conform to the Semantic Versioning style 61 | 62 | # More info: 63 | $ dunamai --help 64 | $ dunamai from --help 65 | $ dunamai from git --help 66 | ``` 67 | 68 | ### Library 69 | 70 | ```python 71 | from dunamai import Version, Style 72 | 73 | # Let's say you're on commit g644252b, which is tagged as v0.1.0. 74 | version = Version.from_git() 75 | assert version.serialize() == "0.1.0" 76 | 77 | # Let's say there was a v0.1.0rc5 tag 44 commits ago 78 | # and you have some uncommitted changes. 79 | version = Version.from_any_vcs() 80 | assert version.serialize() == "0.1.0rc5.post44.dev0+g644252b" 81 | assert version.serialize(metadata=False) == "0.1.0rc5.post44.dev0" 82 | assert version.serialize(dirty=True) == "0.1.0rc5.post44.dev0+g644252b.dirty" 83 | assert version.serialize(style=Style.SemVer) == "0.1.0-rc.5.post.44+g644252b" 84 | ``` 85 | 86 | The `serialize()` method gives you an opinionated, PEP 440-compliant default 87 | that ensures that versions for untagged commits are compatible with Pip's `--pre` flag. 88 | The individual parts of the version are also available for you to use and inspect as you please: 89 | 90 | ```python 91 | assert version.base == "0.1.0" 92 | assert version.stage == "rc" 93 | assert version.revision == 5 94 | assert version.distance == 44 95 | assert version.commit == "g644252b" 96 | assert version.dirty is True 97 | 98 | # Available if the latest tag includes metadata, like v0.1.0+linux: 99 | assert version.tagged_metadata == "linux" 100 | ``` 101 | 102 | ### Tips 103 | By default, the "v" prefix on the tag is required, 104 | unless you specify a custom tag pattern. 105 | You can either write a regular expression: 106 | 107 | * Console: 108 | ```console 109 | $ dunamai from any --pattern "(?P\d+\.\d+\.\d+)" 110 | ``` 111 | * Python: 112 | ```python 113 | from dunamai import Version 114 | version = Version.from_any_vcs(pattern=r"(?P\d+\.\d+\.\d+)") 115 | ``` 116 | 117 | ...or use a named preset: 118 | 119 | * Console: 120 | ```console 121 | $ dunamai from any --pattern default-unprefixed 122 | ``` 123 | * Python: 124 | ```python 125 | from dunamai import Version, Pattern 126 | version = Version.from_any_vcs(pattern=Pattern.DefaultUnprefixed) 127 | ``` 128 | 129 | You can also keep the default pattern and just specify a prefix. 130 | For example, this would match tags like `some-package-v1.2.3`: 131 | 132 | * Console: 133 | ```console 134 | $ dunamai from any --pattern-prefix some-package- 135 | ``` 136 | * Python: 137 | ```python 138 | from dunamai import Version 139 | version = Version.from_any_vcs(pattern_prefix="some-package-") 140 | ``` 141 | 142 | ### VCS archives 143 | Sometimes, you may only have access to an archive of a repository (e.g., a zip file) without the full history. 144 | Dunamai can still detect a version in some of these cases: 145 | 146 | * For Git, you can configure `git archive` to produce a file with some metadata for Dunamai. 147 | 148 | Add a `.git_archival.json` file to the root of your repository with this content: 149 | ``` 150 | { 151 | "hash-full": "$Format:%H$", 152 | "hash-short": "$Format:%h$", 153 | "timestamp": "$Format:%cI$", 154 | "refs": "$Format:%D$", 155 | "describe": "$Format:%(describe:tags=true,match=v[0-9]*)$" 156 | } 157 | ``` 158 | 159 | Add this line to your `.gitattributes` file. 160 | If you don't already have this file, add it to the root of your repository: 161 | ``` 162 | .git_archival.json export-subst 163 | ``` 164 | 165 | * For Mercurial, Dunamai will detect and use an `.hg_archival.txt` file created by `hg archive`. 166 | It will also recognize `.hgtags` if present. 167 | 168 | ### Custom formats 169 | Here are the available substitutions for custom formats. 170 | If you have a tag like `v9!0.1.2-beta.3+other`, then: 171 | 172 | * `{base}` = `0.1.2` 173 | * `{stage}` = `beta` 174 | * `{revision}` = `3` 175 | * `{distance}` is the number of commits since the last 176 | * `{commit}` is the commit hash (defaults to short form, unless you use `--full-commit`) 177 | * `{dirty}` expands to either "dirty" or "clean" if you have uncommitted modified files 178 | * `{tagged_metadata}` = `other` 179 | * `{epoch}` = `9` 180 | * `{branch}` = `feature/foo` 181 | * `{branch_escaped}` = `featurefoo` (can be customized using `--escape-with`) 182 | * `{timestamp}` is in the format `YYYYmmddHHMMSS` as UTC 183 | * `{major}` = `0` 184 | * `{minor}` = `1` 185 | * `{patch}` = `2` 186 | 187 | If you specify a substitution, its value will always be included in the output. 188 | For conditional formatting, you can do something like this (Bash): 189 | 190 | ```bash 191 | distance=$(dunamai from any --format "{distance}") 192 | if [ "$distance" = "0" ]; then 193 | dunamai from any --format "v{base}" 194 | else 195 | dunamai from any --format "v{base}+{distance}.{dirty}" 196 | fi 197 | ``` 198 | 199 | ## Comparison to Versioneer 200 | [Versioneer](https://github.com/warner/python-versioneer) 201 | is another great library for dynamic versions, 202 | but there are some design decisions that prompted the creation of Dunamai as an alternative: 203 | 204 | * Versioneer requires a setup.py file to exist, or else `versioneer install` will fail, 205 | rendering it incompatible with non-setuptools-based projects such as those using Poetry or Flit. 206 | Dunamai can be used regardless of the project's build system. 207 | * Versioneer has a CLI that generates Python code which needs to be committed into your repository, 208 | whereas Dunamai is just a normal importable library 209 | with an optional CLI to help statically include your version string. 210 | * Versioneer produces the version as an opaque string, 211 | whereas Dunamai provides a Version class with discrete parts 212 | that can then be inspected and serialized separately. 213 | * Versioneer provides customizability through a config file, 214 | whereas Dunamai aims to offer customizability through its library API and CLI 215 | for both scripting support and use in other libraries. 216 | 217 | ## Integration 218 | * Setting a `__version__` statically: 219 | 220 | ```console 221 | $ echo "__version__ = '$(dunamai from any)'" > your_library/_version.py 222 | ``` 223 | ```python 224 | # your_library/__init__.py 225 | from your_library._version import __version__ 226 | ``` 227 | 228 | Or dynamically (but Dunamai becomes a runtime dependency): 229 | 230 | ```python 231 | # your_library/__init__.py 232 | import dunamai as _dunamai 233 | __version__ = _dunamai.get_version("your-library", third_choice=_dunamai.Version.from_any_vcs).serialize() 234 | ``` 235 | 236 | * setup.py (no install-time dependency on Dunamai as long as you use wheels): 237 | 238 | ```python 239 | from setuptools import setup 240 | from dunamai import Version 241 | 242 | setup( 243 | name="your-library", 244 | version=Version.from_any_vcs().serialize(), 245 | ) 246 | ``` 247 | 248 | Or you could use a static inclusion approach as in the prior example. 249 | 250 | * [Poetry](https://poetry.eustace.io): 251 | 252 | ```console 253 | $ poetry version $(dunamai from any) 254 | ``` 255 | 256 | Or you can use the [poetry-dynamic-versioning](https://github.com/mtkennerly/poetry-dynamic-versioning) plugin. 257 | 258 | ## Other notes 259 | * Dunamai needs access to the full version history to find tags and compute distance. 260 | Be careful if your CI system does a shallow clone by default. 261 | 262 | * For GitHub workflows, invoke `actions/checkout@v3` with `fetch-depth: 0`. 263 | * For GitLab pipelines, set the `GIT_DEPTH` variable to 0. 264 | * For Docker builds, copy the VCS history (e.g., `.git` folder) into the container. 265 | 266 | For Git, you can also avoid doing a full clone by specifying a remote branch for tags 267 | (e.g., `--tag-branch remotes/origin/master`). 268 | * When using Git, remember that lightweight tags do not store their creation time. 269 | Therefore, if a commit has multiple lightweight tags, 270 | we cannot reliably determine which one should be considered the newest. 271 | The solution is to use annotated tags instead. 272 | * When using Git, the initial commit should not be both tagged and empty 273 | (i.e., created with `--allow-empty`). 274 | This is related to a reporting issue in Git. 275 | For more info, [click here](https://github.com/mtkennerly/dunamai/issues/14). 276 | Dunamai tries to work around this, 277 | but multiple tags on an empty initial commit may not be sorted correctly. 278 | -------------------------------------------------------------------------------- /dunamai/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from pathlib import Path 4 | from typing import Mapping, Optional 5 | 6 | from dunamai import check_version, Version, Pattern, Style, Vcs, VERSION_SOURCE_PATTERN 7 | 8 | 9 | common_sub_args = [ 10 | { 11 | "triggers": ["--metadata"], 12 | "action": "store_true", 13 | "dest": "metadata", 14 | "default": None, 15 | "help": "Always include metadata. Ignored when --format is used", 16 | }, 17 | { 18 | "triggers": ["--no-metadata"], 19 | "action": "store_false", 20 | "dest": "metadata", 21 | "default": None, 22 | "help": "Never include metadata. Ignored when --format is used", 23 | }, 24 | { 25 | "triggers": ["--dirty"], 26 | "action": "store_true", 27 | "dest": "dirty", 28 | "help": "Include dirty flag if applicable. Ignored when --format is used", 29 | }, 30 | { 31 | "vcs": [Vcs.Git], 32 | "triggers": ["--ignore-untracked"], 33 | "action": "store_true", 34 | "dest": "ignore_untracked", 35 | "help": "Ignore untracked files when determining whether the repository is dirty", 36 | }, 37 | { 38 | "triggers": ["--tagged-metadata"], 39 | "action": "store_true", 40 | "dest": "tagged_metadata", 41 | "help": "Include tagged metadata if applicable. Ignored when --format is used", 42 | }, 43 | { 44 | "triggers": ["--pattern"], 45 | "default": VERSION_SOURCE_PATTERN, 46 | "help": ( 47 | "Regular expression matched against the version source." 48 | " This must contain one capture group named `base` corresponding to" 49 | " the release segment of the source." 50 | " Optionally, it may contain another two groups named `stage` and `revision`" 51 | " corresponding to a prerelease type (such as 'alpha' or 'rc') and number" 52 | " (such as in 'alpha-2' or 'rc3')." 53 | " It may also contain a group named `tagged_metadata` corresponding to extra" 54 | " metadata after the main part of the version (typically after a plus sign)." 55 | " There may also be a group named `epoch` for the PEP 440 concept." 56 | " If the `base` group is not present, then instead this will be interpreted" 57 | " as a named preset, which may be one of the following: {}" 58 | ).format(", ".join(["`{}`".format(x.value) for x in Pattern])), 59 | }, 60 | { 61 | "triggers": ["--pattern-prefix"], 62 | "help": "Insert this after the pattern's start anchor (`^`).", 63 | }, 64 | { 65 | "triggers": ["--format"], 66 | "help": ( 67 | "Custom output format. Available substitutions:" 68 | " {base}, {stage}, {revision}, {distance}, {commit}, {dirty}," 69 | " {tagged_metadata}, {epoch}, {branch}, {branch_escaped}, {timestamp}," 70 | " {major}, {minor}, {patch}" 71 | ), 72 | }, 73 | { 74 | "triggers": ["--style"], 75 | "choices": [x.value for x in Style], 76 | "help": ( 77 | "Preconfigured output format." 78 | " Will default to PEP 440 if not set and no custom format given." 79 | " If you specify both a style and a custom format, then the format" 80 | " will be validated against the style's rules" 81 | ), 82 | }, 83 | { 84 | "triggers": ["--latest-tag"], 85 | "action": "store_true", 86 | "dest": "latest_tag", 87 | "default": False, 88 | "help": "Only inspect the latest tag on the latest tagged commit for a pattern match", 89 | }, 90 | { 91 | "triggers": ["--strict"], 92 | "action": "store_true", 93 | "dest": "strict", 94 | "default": False, 95 | "help": ("Elevate warnings to errors. When there are no tags, fail instead of falling back to 0.0.0"), 96 | }, 97 | { 98 | "triggers": ["--path"], 99 | "help": "Directory to inspect, if not the current working directory", 100 | }, 101 | { 102 | "triggers": ["--debug"], 103 | "action": "store_true", 104 | "dest": "debug", 105 | "default": False, 106 | "help": "Display additional information on stderr for troubleshooting", 107 | }, 108 | { 109 | "triggers": ["--bump"], 110 | "action": "store_true", 111 | "dest": "bump", 112 | "default": False, 113 | "help": ( 114 | "Increment the last part of the version `base` by 1," 115 | " unless the `stage` is set, in which case increment the `revision`" 116 | " by 1 or set it to a default of 2 if there was no `revision`" 117 | " Does nothing when on a commit with a version tag." 118 | ), 119 | }, 120 | { 121 | "vcs": [Vcs.Git, Vcs.Mercurial], 122 | "triggers": ["--full-commit"], 123 | "action": "store_true", 124 | "dest": "full_commit", 125 | "default": False, 126 | "help": "Get the full commit hash instead of the short form", 127 | }, 128 | { 129 | "triggers": ["--commit-length"], 130 | "dest": "commit_length", 131 | "type": int, 132 | "help": "Use this many characters from the start of the full commit hash", 133 | }, 134 | { 135 | "triggers": ["--commit-prefix"], 136 | "dest": "commit_prefix", 137 | "type": str, 138 | "help": "Add this prefix when serializing commit IDs", 139 | }, 140 | { 141 | "triggers": ["--escape-with"], 142 | "dest": "escape_with", 143 | "type": str, 144 | "help": "When escaping, replace with this substitution. The default is simply to remove invalid characters.", 145 | }, 146 | { 147 | "vcs": [Vcs.Git], 148 | "triggers": ["--tag-branch"], 149 | "help": "Branch on which to find tags, if different than the current branch", 150 | }, 151 | { 152 | "vcs": [Vcs.Subversion], 153 | "triggers": ["--tag-dir"], 154 | "default": "tags", 155 | "help": "Location of tags relative to the root", 156 | }, 157 | ] 158 | cli_spec = { 159 | "description": "Generate dynamic versions", 160 | "sub_dest": "command", 161 | "sub": { 162 | "from": { 163 | "description": "Generate version from a particular VCS", 164 | "sub_dest": "vcs", 165 | "sub": { 166 | Vcs.Any.value: { 167 | "description": "Generate version from any detected VCS", 168 | "args": common_sub_args, 169 | }, 170 | Vcs.Git.value: { 171 | "description": "Generate version from Git", 172 | "args": common_sub_args, 173 | }, 174 | Vcs.Mercurial.value: { 175 | "description": "Generate version from Mercurial", 176 | "args": common_sub_args, 177 | }, 178 | Vcs.Darcs.value: { 179 | "description": "Generate version from Darcs", 180 | "args": common_sub_args, 181 | }, 182 | Vcs.Subversion.value: { 183 | "description": "Generate version from Subversion", 184 | "args": common_sub_args, 185 | }, 186 | Vcs.Bazaar.value: { 187 | "description": "Generate version from Bazaar", 188 | "args": common_sub_args, 189 | }, 190 | Vcs.Fossil.value: { 191 | "description": "Generate version from Fossil", 192 | "args": common_sub_args, 193 | }, 194 | Vcs.Pijul.value: { 195 | "description": "Generate version from Pijul", 196 | "args": common_sub_args, 197 | }, 198 | }, 199 | }, 200 | "check": { 201 | "description": "Check if a version is valid for a style", 202 | "args": [ 203 | { 204 | "triggers": [], 205 | "dest": "version", 206 | "help": "Version to check; may be piped in", 207 | "nargs": "?", 208 | }, 209 | { 210 | "triggers": ["--style"], 211 | "choices": [x.value for x in Style], 212 | "default": Style.Pep440.value, 213 | "help": "Style against which to check", 214 | }, 215 | ], 216 | }, 217 | }, 218 | } 219 | 220 | 221 | def build_parser( 222 | spec: Mapping, parser: Optional[argparse.ArgumentParser] = None, vcs: Optional[Vcs] = None 223 | ) -> argparse.ArgumentParser: 224 | if parser is None: 225 | parser = argparse.ArgumentParser( 226 | description=spec["description"], formatter_class=argparse.ArgumentDefaultsHelpFormatter 227 | ) 228 | if "args" in spec: 229 | for arg in spec["args"]: 230 | help = arg["help"] 231 | if "vcs" in arg: 232 | if vcs not in [*arg["vcs"], Vcs.Any]: 233 | continue 234 | help += " (only: {})".format(", ".join([x.name for x in arg["vcs"]])) 235 | triggers = arg["triggers"] 236 | parser.add_argument( 237 | *triggers, 238 | help=help, 239 | **{k: v for k, v in arg.items() if k not in ["triggers", "help", "vcs"]}, 240 | ) 241 | if "sub" in spec: 242 | subparsers = parser.add_subparsers(dest=spec["sub_dest"]) 243 | subparsers.required = True 244 | for name, sub_spec in spec["sub"].items(): 245 | subparser = subparsers.add_parser( 246 | name, 247 | description=sub_spec.get("description"), 248 | help=sub_spec.get("description"), 249 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 250 | ) 251 | build_parser(sub_spec, subparser, Vcs(name) if spec["sub_dest"] == "vcs" else None) 252 | 253 | return parser 254 | 255 | 256 | def get_parser() -> argparse.ArgumentParser: 257 | return build_parser(cli_spec) 258 | 259 | 260 | def parse_args(argv=None) -> argparse.Namespace: 261 | return get_parser().parse_args(argv) 262 | 263 | 264 | def from_stdin(value: Optional[str]) -> Optional[str]: 265 | if value is not None: 266 | return value 267 | 268 | if not sys.stdin.isatty(): 269 | return sys.stdin.readline().strip() 270 | 271 | return None 272 | 273 | 274 | def from_vcs( 275 | vcs: Vcs, 276 | pattern: str, 277 | metadata: Optional[bool], 278 | dirty: bool, 279 | format: Optional[str], 280 | style: Optional[Style], 281 | latest_tag: bool, 282 | tag_dir: str, 283 | debug: bool, 284 | bump: bool, 285 | tagged_metadata: bool, 286 | tag_branch: Optional[str], 287 | full_commit: bool, 288 | strict: bool, 289 | path: Optional[Path], 290 | pattern_prefix: Optional[str], 291 | ignore_untracked: bool, 292 | commit_length: Optional[int], 293 | commit_prefix: Optional[str], 294 | escape_with: Optional[str], 295 | ) -> None: 296 | version = Version.from_vcs( 297 | vcs, 298 | pattern, 299 | latest_tag, 300 | tag_dir, 301 | tag_branch, 302 | full_commit, 303 | strict, 304 | path, 305 | pattern_prefix, 306 | ignore_untracked, 307 | commit_length, 308 | ) 309 | 310 | for concern in version.concerns: 311 | print("Warning: {}".format(concern.message()), file=sys.stderr) 312 | 313 | print(version.serialize(metadata, dirty, format, style, bump, tagged_metadata, commit_prefix, escape_with)) 314 | 315 | if debug: 316 | print("# Matched tag: {}".format(version._matched_tag), file=sys.stderr) 317 | print("# Newer unmatched tags: {}".format(version._newer_unmatched_tags), file=sys.stderr) 318 | 319 | 320 | def main() -> None: 321 | args = parse_args() 322 | try: 323 | if args.command == "from": 324 | tag_dir = getattr(args, "tag_dir", "tags") 325 | tag_branch = getattr(args, "tag_branch", None) 326 | full_commit = getattr(args, "full_commit", False) 327 | ignore_untracked = getattr(args, "ignore_untracked", False) 328 | commit_length = getattr(args, "commit_length", None) 329 | commit_prefix = getattr(args, "commit_prefix", None) 330 | escape_with = getattr(args, "escape_with", None) 331 | from_vcs( 332 | Vcs(args.vcs), 333 | args.pattern, 334 | args.metadata, 335 | args.dirty, 336 | args.format, 337 | Style(args.style) if args.style else None, 338 | args.latest_tag, 339 | tag_dir, 340 | args.debug, 341 | args.bump, 342 | args.tagged_metadata, 343 | tag_branch, 344 | full_commit, 345 | args.strict, 346 | Path(args.path) if args.path is not None else None, 347 | args.pattern_prefix, 348 | ignore_untracked, 349 | commit_length, 350 | commit_prefix, 351 | escape_with, 352 | ) 353 | elif args.command == "check": 354 | version = from_stdin(args.version) 355 | if version is None: 356 | raise ValueError("A version must be specified") 357 | check_version(version, Style(args.style)) 358 | except Exception as e: 359 | print(e, file=sys.stderr) 360 | sys.exit(1) 361 | 362 | 363 | if __name__ == "__main__": 364 | main() 365 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.25.0 (2025-07-04) 2 | 3 | * Added `commit_prefix` option to `Version.serialize()` 4 | to insert a prefix when serializing commit IDs. 5 | * Added `escape_with` option to `Version.serialize()` 6 | to customize how branch names are escaped. 7 | 8 | ## v1.24.1 (2025-05-09) 9 | 10 | * Fixed: Deprecation warning from `re.sub()` using Python 3.13+. 11 | ([Contributed by emmanuel-ferdman](https://github.com/mtkennerly/dunamai/pull/103)) 12 | 13 | ## v1.24.0 (2025-05-07) 14 | 15 | * Changed: Previously, for Git 2.7+, 16 | if the initial commit were both tagged and empty, 17 | Dunamai would raise an exception due to Git not reporting that tag in 18 | `git log --simplify-by-decoration`. 19 | Now, if a tag is missing from that list, Dunamai treats it as the oldest tag. 20 | 21 | ## v1.23.2 (2025-05-06) 22 | 23 | * Fixed: `Version.from_git` would fail if the `GIT_TRACE` environment variable were set, 24 | due to unexpected output from Git. 25 | 26 | ## v1.23.1 (2025-03-20) 27 | 28 | * Fixed: `Version.__lt__` checked if *all* fields were less than the other instance, 29 | rather than using the proper field precedence and version ordering. 30 | 31 | ## v1.23.0 (2024-11-17) 32 | 33 | * Added: `{major}`, `{minor}`, and `{patch}` format placeholders. 34 | 35 | ## v1.22.0 (2024-08-07) 36 | 37 | * Fixed: The `--ignore-untracked` CLI flag was ignored. 38 | * Added: `--commit-length` option. 39 | 40 | ## v1.21.2 (2024-06-26) 41 | 42 | * Fixed: Some timestamps could fail to parse on Python 3.5 and 3.6. 43 | 44 | ## v1.21.1 (2024-05-03) 45 | 46 | * Fixed: Distance was calculated inconsistently for Git 47 | when there were some tags and none matched the version pattern. 48 | 49 | ## v1.21.0 (2024-04-29) 50 | 51 | * Generally, when Dunamai can detect the VCS in use, but there's no version set yet, 52 | then Dunamai uses 0.0.0 as a fallback, unless strict mode is enabled. 53 | This is useful for new projects that do not yet have a release. 54 | 55 | However, if there were some tags and none matched the version pattern, 56 | then Dunamai would yield an error. 57 | That wouldn't be helpful for a new project with some non-version tag, 58 | and it could be incorrect for a monorepo with different tags for different packages. 59 | 60 | Now, Dunamai will use 0.0.0 in this case as well, unless strict mode is enabled. 61 | * You can now specify a pattern prefix. 62 | For example, `--pattern default --pattern-prefix some-package-` 63 | would match tags like `some-package-v1.2.3`. 64 | This is useful if you just want a custom prefix without writing a whole pattern. 65 | * Added `--ignore-untracked` option to control checking whether the repository is dirty. 66 | 67 | ## v1.20.0 (2024-04-12) 68 | 69 | * Updated `Version.bump()` to add a `smart` argument, 70 | which only bumps when `distance != 0`. 71 | This will also make `Version.serialize()` use pre-release formatting automatically, 72 | like calling `Version.serialize(bump=True)`. 73 | 74 | ## v1.19.2 (2024-02-16) 75 | 76 | * Fixed an exception when a Git repository had a broken ref. 77 | Git would print a warning that Dunamai failed to parse. 78 | 79 | ## v1.19.1 (2024-02-07) 80 | 81 | * Relaxed Python bounds from `^3.5` to `>=3.5` since Python does not follow Semantic Versioning. 82 | * Fixed some `git log` commands that did not include `-c log.showsignature=false`. 83 | ([Contributed by pdecat](https://github.com/mtkennerly/dunamai/pull/75)) 84 | 85 | ## v1.19.0 (2023-10-04) 86 | 87 | * Added a `--path` option to inspect a directory other than the current one. 88 | The `Version.from_*` methods now also take a `path` argument. 89 | 90 | ## v1.18.1 (2023-09-22) 91 | 92 | * For Git 2.16+, `--decorate-refs=refs/tags/` is now specified for `git log` 93 | in case you've configured `log.excludeDecoration=refs/tags/`. 94 | 95 | ## v1.18.0 (2023-07-10) 96 | 97 | * Added a `vcs` attribute to `Version` to indicate which VCS was detected. 98 | 99 | ## v1.17.0 (2023-05-19) 100 | 101 | * The `from` command will print a warning for shallow Git repositories. 102 | This becomes an error with `--strict`. 103 | * The `Version` class has a new `concerns` field to indicate warnings with the version. 104 | Right now, the only possibility is `Concern.ShallowRepository`. 105 | 106 | ## v1.16.1 (2023-05-13) 107 | 108 | * Fixed outdated reference to `pkg_resources` in the docstring for `get_version`. 109 | * `CHANGELOG.md` and `tests` are now included in sdists. 110 | 111 | ## v1.16.0 (2023-02-21) 112 | 113 | * Updated `Version.parse` to better handle PEP 440 versions produced by Dunamai itself. 114 | Specifically, in `1.2.3.post4.dev5`, the post number becomes the distance and the dev number is ignored. 115 | In `1.2.3.dev5`, the dev number becomes the distance. 116 | * Added `increment` argument to `bump_version` and `Version.bump`. 117 | ([Contributed by legendof-selda](https://github.com/mtkennerly/dunamai/pull/54)) 118 | * Fixed Git detection when there is a "dubious ownership" error. 119 | Previously, `from git` would report that it was not a Git project, 120 | and `from any` would report that it could not detect a VCS. 121 | Now, both commands report that there is dubious ownership. 122 | * Improved error reporting for `from any` VCS detection. 123 | The error now specifies which VCSes were checked and which were not found to be installed. 124 | 125 | ## v1.15.0 (2022-12-02) 126 | 127 | * Added compatibility with Git versions as old as 1.8.2.3. 128 | 129 | ## v1.14.1 (2022-11-15) 130 | 131 | * Fixed Git 2.7.0 compatibility by changing `git log --no-show-signature` to `git -c log.showsignature=false log`. 132 | 133 | ## v1.14.0 (2022-11-07) 134 | 135 | * Added a `strict` option to prevent falling back to `0.0.0` when there are no tags. 136 | * Added support for `.git_archival.json` files created by `git archive`. 137 | * Added support for `.hg_archival.txt` files created by `hg archive`. 138 | 139 | ## v1.13.2 (2022-10-14) 140 | 141 | * Fixed an error when parsing Git output with `showSignature = true` configured. 142 | ([Contributed by riton](https://github.com/mtkennerly/dunamai/pull/51)) 143 | 144 | ## v1.13.1 (2022-09-25) 145 | 146 | * Made pattern-related error messages more readable by moving the pattern after 147 | the primary message instead of mixing them. 148 | 149 | ## v1.13.0 (2022-08-21) 150 | 151 | * Added support for [Pijul](https://pijul.org). 152 | 153 | ## v1.12.0 (2022-05-07) 154 | 155 | * Added `Pattern` type for named pattern presets. Currently, this includes: 156 | * `Pattern.Default` (CLI: `--pattern default`) for the existing default. 157 | * `Pattern.DefaultUnprefixed` (CLI: `--pattern default-unprefixed`) 158 | for the existing default, but without requiring the `v` prefix. 159 | * Added `tag_branch` option (CLI: `--tag-branch`) for Git repositories. 160 | This is particularly useful for Gitflow without fast forward, where 161 | `develop` does not contain the tag history, so you can specify 162 | `--tag-branch master`. 163 | * Added `full_commit` option (CLI: `--full-commit`) for Git and Mercurial repositories 164 | to obtain the full commit hash instead of the short form. 165 | * Fixed `Version.parse` so that it better handles versions without the `v` 166 | prefix when the pattern does not (or may not) require it. 167 | * Fixed error reporting when a custom pattern is an invalid regular expression, 168 | as well as when a custom format is malformed. 169 | It was fine when Dunamai was used as a library, but the error message lacked 170 | context on the CLI. 171 | * Fixed `from any` not passing the `--tag-dir` option along for Subversion 172 | repositories. 173 | 174 | ## v1.11.1 (2022-04-05) 175 | 176 | * Fixed the `--bump` CLI option and the `bump` argument of `Version.serialize` 177 | bumping even on a commit with a version tag. Now, no bumping occurs on such 178 | a commit. 179 | 180 | ## v1.11.0 (2022-03-15) 181 | 182 | * Explicitly specified `Optional[...]` typing on arguments with a default of `None`. 183 | ([Contributed by jonathangreen](https://github.com/mtkennerly/dunamai/pull/44)) 184 | * Made `VERSION_SOURCE_PATTERN` public for consumption by other tools. 185 | 186 | ## v1.10.0 (2022-03-08) 187 | 188 | * Added `branch` and `timestamp` to the `Version` class, 189 | along with associated format placeholders (`branch`, `branch_escaped`, `timestamp`). 190 | Branch info is not populated for Darcs and Subversion repositories. 191 | * Fixed validation for PEP 440, where the local segment was allowed to contain any characters. 192 | * Fixed validation for Semantic Versioning, where some segments were allowed to contain 193 | these additional characters: 194 | 195 | ``` 196 | [ \ ] ^ _ ` 197 | ``` 198 | 199 | ## v1.9.0 (2022-02-20) 200 | 201 | * Changed `Version.serialize`'s `format` argument to support passing a callback. 202 | ([Contributed by marnikow](https://github.com/mtkennerly/dunamai/pull/40)) 203 | * Added `ignore` option to `get_version()`. 204 | ([Contributed by marnikow](https://github.com/mtkennerly/dunamai/pull/39)) 205 | * Added `parser` option to `get_version()`. 206 | * Added `Version.parse()`. 207 | ([Contributed by marnikow](https://github.com/mtkennerly/dunamai/pull/41)) 208 | * Added `Version.bump()`. 209 | ([Contributed by marnikow](https://github.com/mtkennerly/dunamai/pull/38)) 210 | 211 | ## v1.8.0 (2022-01-27) 212 | 213 | * Changed the build backend to poetry-core. 214 | ([Contributed by fabaff](https://github.com/mtkennerly/dunamai/pull/35)) 215 | * Clarified serialization options that are ignored when using a custom format. 216 | * Relaxed dependency range of `importlib-metadata` for compatibility with Poetry. 217 | * Added `epoch` to `Version` class, default tag pattern, and format placeholders. 218 | * Fixed PEP 440 validation to allow multiple digits in the epoch. 219 | * Improved parsing of optional pattern groups so that we don't stop checking at 220 | the first one that's omitted. 221 | * Fixed handling of tags with `post`/`dev` stages so that they are serialized 222 | and bumped correctly when using PEP 440. 223 | 224 | ## v1.7.0 (2021-10-31) 225 | 226 | * Broadened the default version tag pattern to allow more separator styles 227 | recognized in PEP 440 pre-normalized forms (`-`, `.`, and `_`). 228 | * Enhanced `serialize_pep440()` to normalize the alternative prerelease names 229 | (`alpha` -> `a`, `beta` -> `b`, `c`/`pre`/`preview` -> `rc`) and 230 | capitalizations (`RC` -> `rc`, etc). 231 | * Added a `py.typed` file for PEP-561. 232 | ([Contributed by wwuck](https://github.com/mtkennerly/dunamai/pull/25)) 233 | * Replaced `pkg_resources` dependency with `packaging` and `importlib_metadata`. 234 | ([Contributed by flying-sheep](https://github.com/mtkennerly/dunamai/pull/29)) 235 | * Added some missing public items to `__all__`. 236 | 237 | ## v1.6.0 (2021-08-09) 238 | 239 | * Fixed an oversight where the default version tag pattern would only find 240 | tags with exactly three parts in the base (e.g., `v1.0.0` and `v1.2.3`). 241 | This is now relaxed so that `v1`, `v1.2.3.4`, and so on are also recognized. 242 | * Added support for execution via `python -m dunamai`. 243 | ([Contributed by jstriebel](https://github.com/mtkennerly/dunamai/pull/19)) 244 | 245 | ## v1.5.5 (2021-04-26) 246 | 247 | * Fixed handling of Git tags that contain slashes. 248 | ([Contributed by ioben](https://github.com/mtkennerly/dunamai/pull/17)) 249 | 250 | ## v1.5.4 (2021-01-20) 251 | 252 | * Fixed handling of Git tags that contain commas. 253 | 254 | ## v1.5.3 (2021-01-13) 255 | 256 | * Fixed Semantic Versioning enforcement to allow metadata segments with 257 | more than two dot-separated identifiers. 258 | 259 | ## v1.5.2 (2020-12-17) 260 | 261 | * For Git, avoided use of `--decorate-refs` to maintain compatibility with 262 | older Git versions. 263 | 264 | ## v1.5.1 (2020-12-16) 265 | 266 | * Improved ordering of Git tags, particularly when commit dates were not chronological. 267 | ([Contributed by mariusvniekerk](https://github.com/mtkennerly/dunamai/pull/9)) 268 | * Improved Subversion handling when in a subdirectory of the repository. 269 | ([Contributed by Spirotot](https://github.com/mtkennerly/dunamai/pull/10)) 270 | 271 | ## v1.5.0 (2020-12-02) 272 | 273 | * Added the `--tagged-metadata` option and corresponding attribute on the 274 | `Version` class. 275 | ([Contributed by mariusvniekerk](https://github.com/mtkennerly/dunamai/pull/8)) 276 | * Added explicit dependency on setuptools (because of using `pkg_resources`) 277 | for environments where it is not installed by default. 278 | 279 | ## v1.4.1 (2020-11-17) 280 | 281 | * For Git, replaced `--porcelain=v1` with `--porcelain` to maintain compatibility 282 | with older Git versions. 283 | 284 | ## v1.4.0 (2020-11-17) 285 | 286 | * Added the `--bump` command line option and the `bump` argument to 287 | `Version.serialize()`. 288 | * Fixed an issue with Git annotated tag sorting. When there was a newer 289 | annotated tag A on an older commit and an older annotated tag B on a 290 | newer commit, Dunamai would choose tag A, but will now correctly choose 291 | tag B because the commit is newer. 292 | * With Git, trigger the dirty flag when there are untracked files. 293 | ([Contributed by jpc4242](https://github.com/mtkennerly/dunamai/pull/6)) 294 | 295 | ## v1.3.1 (2020-09-27) 296 | 297 | * Fixed ambiguous reference error when using Git if a tag and branch name 298 | were identical. 299 | 300 | ## v1.3.0 (2020-07-04) 301 | 302 | * Previously, when there were not yet any version-like tags, the distance would 303 | be set to 0, so the only differentiator was the commit ID. Now, the distance 304 | will be set to the number of commits so far. For example: 305 | 306 | * No commits: base = 0.0.0, distance = 0 307 | * 1 commit, no tags: base = 0.0.0, distance = 1 308 | * 10 commits, no tags: base = 0.0.0, distance = 10 309 | 310 | ## v1.2.0 (2020-06-12) 311 | 312 | * Added `--debug` flag. 313 | 314 | ## v1.1.0 (2020-03-22) 315 | 316 | * Added these functions to the public API: 317 | * `serialize_pep440` 318 | * `serialize_semver` 319 | * `serialize_pvp` 320 | * `bump_version` 321 | 322 | ## v1.0.0 (2019-10-26) 323 | 324 | * Changed the `Version` class to align with Dunamai's own semantics instead of 325 | PEP 440's semantics. 326 | 327 | Previously, `Version` implemented all of PEP 440's features, like epochs and 328 | dev releases, even though Dunamai itself did not use epochs (unless you 329 | created your own `Version` instance with one and serialized it) and always 330 | set dev to 0 in the `from_git`/etc methods. The `serialize` method then 331 | tried to generalize those PEP 440 concepts to other versioning schemes, 332 | as in `0.1.0-epoch.1` for Semantic Versioning, even though that doesn't 333 | have an equivalent meaning in that scheme. 334 | 335 | Now, the `Version` class implements the semantics used by Dunamai, giving 336 | it more power in the serialization to map those concepts in an appropriate 337 | way for each scheme. For example, `dev0` is now only added for PEP 440 (in 338 | order to be compatible with Pip's `--pre` flag), but `dev.0` is no longer 339 | added for Semantic Versioning because it served no purpose there. 340 | 341 | API changes: 342 | 343 | * `post` has been renamed to `distance`, and its type is simply `int` 344 | rather than `Optional[int]` 345 | * `epoch` and `dev` have been removed 346 | * `pre_type` has been renamed to `stage` 347 | * `pre_number` has been renamed to `revision`, and it is no longer required 348 | when specifying a stage 349 | * Improved error reporting when the version control system cannot be detected 350 | and when a specified VCS is unavailable. 351 | * Improved the default regular expression for tags: 352 | * It now requires a full match of the tag. 353 | * It now recognizes when the `base` and `stage` are separated by a hyphen. 354 | * It now recognizes when the `stage` and `revision` are separated by a dot. 355 | * It now allows a `stage` without a `revision`. 356 | 357 | ## v0.9.0 (2019-10-22) 358 | 359 | * Added Fossil support. 360 | * Fixed case with Git/Mercurial/Subversion/Bazaar where, if you checked out an 361 | older commit, then Dunamai would consider tags for commits both before and 362 | after the commit that was checked out. It now only considers tags for the 363 | checked out commit or one of its ancestors, making the results more 364 | deterministic. 365 | * Changed VCS detection to be based on the result of VCS commands rather than 366 | looking for VCS-specific directories/files. This avoids the risk of false 367 | positives and simplifies cases with inconsistent VCS files (e.g., 368 | Fossil uses `.fslckout` on Linux and `_FOSSIL_` on Windows) 369 | 370 | ## v0.8.1 (2019-08-30) 371 | 372 | * Fixed handling of annotated Git tags, which were previously ignored. 373 | 374 | ## v0.8.0 (2019-06-05) 375 | 376 | * Changed `Version.from_any_vcs` to accept the `tag_dir` argument, 377 | which will only be used if Subversion is the detected VCS. 378 | Likewise, `dunamai from any` now accepts `--tag-dir`. 379 | * Added `Version.from_vcs` to make it easier for other tools to map from a 380 | user's VCS configuration to the appropriate function. 381 | 382 | ## v0.7.1 (2019-05-16) 383 | 384 | * Fixed issue on Linux where shell commands were not interpreted correctly. 385 | 386 | ## v0.7.0 (2019-04-16) 387 | 388 | * Added Bazaar support. 389 | * Added the `dunamai check` command and the corresponding `check_version` 390 | function. 391 | * Added the option to check just the latest tag or to keep checking tags 392 | until a match is found. The default behavior is now to keep checking. 393 | * Added enforcement of Semantic Versioning rule against numeric segments 394 | with a leading zero. 395 | * Renamed the `with_metadata` and `with_dirty` arguments of `Version.serialize` 396 | to `metadata` and `dirty` respectively. 397 | * Fixed the equality and ordering of `Version` to consider all attributes. 398 | `dirty` and `commit` were ignored previously if neither `post` nor `dev` 399 | were set, and `dirty=None` and `dirty=False` were not distinguished. 400 | 401 | ## v0.6.0 (2019-04-14) 402 | 403 | * Added Subversion support. 404 | * Added support for the PVP style. 405 | * Changed the type of the `style` argument in `Version.serialize` 406 | from `str` to `Style`. 407 | 408 | ## v0.5.0 (2019-03-31) 409 | 410 | * Added built-in Semantic Versioning output style in addition to PEP 440. 411 | * Added style validation for custom output formats. 412 | * Added Darcs support. 413 | 414 | ## v0.4.0 (2019-03-29) 415 | 416 | * Added support for custom serialization formats. 417 | 418 | ## v0.3.0 (2019-03-29) 419 | 420 | * Added Mercurial support. 421 | * Added a CLI. 422 | * Renamed `Version.from_git_describe` to `Version.from_git`. 423 | * Changed behavior of `Version.serialize` argument `with_metadata` so that, 424 | by default, metadata is excluded when post and dev are not set. 425 | * Added `with_dirty` argument to `Version.serialize` and removed `flag_dirty` 426 | argument from `Version.from_git`. The information should always be collected, 427 | and it is up to the serialization step to decide what to do with it. 428 | * Added `Version.from_any_vcs`. 429 | * Removed `source` attribute of `Version` since some VCSes may require multiple 430 | commands in conjunction and therefore not have a single source string. 431 | 432 | ## v0.2.0 (2019-03-26) 433 | 434 | * Fixed a wrong Git command being used. 435 | * Made metadata serialization opt-in. 436 | 437 | ## v0.1.0 (2019-03-26) 438 | 439 | * Initial release. 440 | -------------------------------------------------------------------------------- /docs/dunamai.1: -------------------------------------------------------------------------------- 1 | .TH DUNAMAI "1" "2025\-07\-04" "dunamai 1.25.0" "Dunamai" 2 | .SH NAME 3 | dunamai 4 | .SH SYNOPSIS 5 | .B dunamai 6 | [-h] {from,check} ... 7 | .SH DESCRIPTION 8 | Generate dynamic versions 9 | 10 | .SH 11 | POSITIONAL ARGUMENTS 12 | .SS \fBdunamai from\fR 13 | Generate version from a particular VCS 14 | 15 | usage: dunamai from [\-h] 16 | {any,git,mercurial,darcs,subversion,bazaar,fossil,pijul} 17 | ... 18 | 19 | Generate version from a particular VCS 20 | 21 | .SS \fBdunamai from any\fR 22 | Generate version from any detected VCS 23 | 24 | usage: dunamai from any [\-h] [\-\-metadata] [\-\-no\-metadata] [\-\-dirty] 25 | [\-\-ignore\-untracked] [\-\-tagged\-metadata] 26 | [\-\-pattern PATTERN] [\-\-pattern\-prefix PATTERN_PREFIX] 27 | [\-\-format FORMAT] [\-\-style {pep440,semver,pvp}] 28 | [\-\-latest\-tag] [\-\-strict] [\-\-path PATH] [\-\-debug] 29 | [\-\-bump] [\-\-full\-commit] 30 | [\-\-commit\-length COMMIT_LENGTH] 31 | [\-\-commit\-prefix COMMIT_PREFIX] 32 | [\-\-escape\-with ESCAPE_WITH] [\-\-tag\-branch TAG_BRANCH] 33 | [\-\-tag\-dir TAG_DIR] 34 | 35 | Generate version from any detected VCS 36 | 37 | options: 38 | .RS 7 39 | .TP 40 | \fB\-\-metadata\fR 41 | Always include metadata. Ignored when \-\-format is used 42 | 43 | .TP 44 | \fB\-\-no\-metadata\fR 45 | Never include metadata. Ignored when \-\-format is used 46 | 47 | .TP 48 | \fB\-\-dirty\fR 49 | Include dirty flag if applicable. Ignored when \-\-format is used 50 | 51 | .TP 52 | \fB\-\-ignore\-untracked\fR 53 | Ignore untracked files when determining whether the repository is dirty (only: 54 | Git) 55 | 56 | .TP 57 | \fB\-\-tagged\-metadata\fR 58 | Include tagged metadata if applicable. Ignored when \-\-format is used 59 | 60 | .TP 61 | \fB\-\-pattern\fR \fI\,PATTERN\/\fR 62 | Regular expression matched against the version source. This must contain one 63 | capture group named `base` corresponding to the release segment of the source. 64 | Optionally, it may contain another two groups named `stage` and `revision` 65 | corresponding to a prerelease type (such as 'alpha' or 'rc') and number (such 66 | as in 'alpha\-2' or 'rc3'). It may also contain a group named `tagged_metadata` 67 | corresponding to extra metadata after the main part of the version (typically 68 | after a plus sign). There may also be a group named `epoch` for the PEP 440 69 | concept. If the `base` group is not present, then instead this will be 70 | interpreted as a named preset, which may be one of the following: `default`, 71 | `default\-unprefixed` 72 | 73 | .TP 74 | \fB\-\-pattern\-prefix\fR \fI\,PATTERN_PREFIX\/\fR 75 | Insert this after the pattern's start anchor (`^`). 76 | 77 | .TP 78 | \fB\-\-format\fR \fI\,FORMAT\/\fR 79 | Custom output format. Available substitutions: {base}, {stage}, {revision}, 80 | {distance}, {commit}, {dirty}, {tagged_metadata}, {epoch}, {branch}, 81 | {branch_escaped}, {timestamp}, {major}, {minor}, {patch} 82 | 83 | .TP 84 | \fB\-\-style\fR \fI\,{pep440,semver,pvp}\/\fR 85 | Preconfigured output format. Will default to PEP 440 if not set and no custom 86 | format given. If you specify both a style and a custom format, then the format 87 | will be validated against the style's rules 88 | 89 | .TP 90 | \fB\-\-latest\-tag\fR 91 | Only inspect the latest tag on the latest tagged commit for a pattern match 92 | 93 | .TP 94 | \fB\-\-strict\fR 95 | Elevate warnings to errors. When there are no tags, fail instead of falling 96 | back to 0.0.0 97 | 98 | .TP 99 | \fB\-\-path\fR \fI\,PATH\/\fR 100 | Directory to inspect, if not the current working directory 101 | 102 | .TP 103 | \fB\-\-debug\fR 104 | Display additional information on stderr for troubleshooting 105 | 106 | .TP 107 | \fB\-\-bump\fR 108 | Increment the last part of the version `base` by 1, unless the `stage` is set, 109 | in which case increment the `revision` by 1 or set it to a default of 2 if 110 | there was no `revision` Does nothing when on a commit with a version tag. 111 | 112 | .TP 113 | \fB\-\-full\-commit\fR 114 | Get the full commit hash instead of the short form (only: Git, Mercurial) 115 | 116 | .TP 117 | \fB\-\-commit\-length\fR \fI\,COMMIT_LENGTH\/\fR 118 | Use this many characters from the start of the full commit hash 119 | 120 | .TP 121 | \fB\-\-commit\-prefix\fR \fI\,COMMIT_PREFIX\/\fR 122 | Add this prefix when serializing commit IDs 123 | 124 | .TP 125 | \fB\-\-escape\-with\fR \fI\,ESCAPE_WITH\/\fR 126 | When escaping, replace with this substitution. The default is simply to remove 127 | invalid characters. 128 | 129 | .TP 130 | \fB\-\-tag\-branch\fR \fI\,TAG_BRANCH\/\fR 131 | Branch on which to find tags, if different than the current branch (only: Git) 132 | 133 | .TP 134 | \fB\-\-tag\-dir\fR \fI\,TAG_DIR\/\fR 135 | Location of tags relative to the root (only: Subversion) 136 | .RE 137 | 138 | .SS \fBdunamai from git\fR 139 | Generate version from Git 140 | 141 | usage: dunamai from git [\-h] [\-\-metadata] [\-\-no\-metadata] [\-\-dirty] 142 | [\-\-ignore\-untracked] [\-\-tagged\-metadata] 143 | [\-\-pattern PATTERN] [\-\-pattern\-prefix PATTERN_PREFIX] 144 | [\-\-format FORMAT] [\-\-style {pep440,semver,pvp}] 145 | [\-\-latest\-tag] [\-\-strict] [\-\-path PATH] [\-\-debug] 146 | [\-\-bump] [\-\-full\-commit] 147 | [\-\-commit\-length COMMIT_LENGTH] 148 | [\-\-commit\-prefix COMMIT_PREFIX] 149 | [\-\-escape\-with ESCAPE_WITH] [\-\-tag\-branch TAG_BRANCH] 150 | 151 | Generate version from Git 152 | 153 | options: 154 | .RS 7 155 | .TP 156 | \fB\-\-metadata\fR 157 | Always include metadata. Ignored when \-\-format is used 158 | 159 | .TP 160 | \fB\-\-no\-metadata\fR 161 | Never include metadata. Ignored when \-\-format is used 162 | 163 | .TP 164 | \fB\-\-dirty\fR 165 | Include dirty flag if applicable. Ignored when \-\-format is used 166 | 167 | .TP 168 | \fB\-\-ignore\-untracked\fR 169 | Ignore untracked files when determining whether the repository is dirty (only: 170 | Git) 171 | 172 | .TP 173 | \fB\-\-tagged\-metadata\fR 174 | Include tagged metadata if applicable. Ignored when \-\-format is used 175 | 176 | .TP 177 | \fB\-\-pattern\fR \fI\,PATTERN\/\fR 178 | Regular expression matched against the version source. This must contain one 179 | capture group named `base` corresponding to the release segment of the source. 180 | Optionally, it may contain another two groups named `stage` and `revision` 181 | corresponding to a prerelease type (such as 'alpha' or 'rc') and number (such 182 | as in 'alpha\-2' or 'rc3'). It may also contain a group named `tagged_metadata` 183 | corresponding to extra metadata after the main part of the version (typically 184 | after a plus sign). There may also be a group named `epoch` for the PEP 440 185 | concept. If the `base` group is not present, then instead this will be 186 | interpreted as a named preset, which may be one of the following: `default`, 187 | `default\-unprefixed` 188 | 189 | .TP 190 | \fB\-\-pattern\-prefix\fR \fI\,PATTERN_PREFIX\/\fR 191 | Insert this after the pattern's start anchor (`^`). 192 | 193 | .TP 194 | \fB\-\-format\fR \fI\,FORMAT\/\fR 195 | Custom output format. Available substitutions: {base}, {stage}, {revision}, 196 | {distance}, {commit}, {dirty}, {tagged_metadata}, {epoch}, {branch}, 197 | {branch_escaped}, {timestamp}, {major}, {minor}, {patch} 198 | 199 | .TP 200 | \fB\-\-style\fR \fI\,{pep440,semver,pvp}\/\fR 201 | Preconfigured output format. Will default to PEP 440 if not set and no custom 202 | format given. If you specify both a style and a custom format, then the format 203 | will be validated against the style's rules 204 | 205 | .TP 206 | \fB\-\-latest\-tag\fR 207 | Only inspect the latest tag on the latest tagged commit for a pattern match 208 | 209 | .TP 210 | \fB\-\-strict\fR 211 | Elevate warnings to errors. When there are no tags, fail instead of falling 212 | back to 0.0.0 213 | 214 | .TP 215 | \fB\-\-path\fR \fI\,PATH\/\fR 216 | Directory to inspect, if not the current working directory 217 | 218 | .TP 219 | \fB\-\-debug\fR 220 | Display additional information on stderr for troubleshooting 221 | 222 | .TP 223 | \fB\-\-bump\fR 224 | Increment the last part of the version `base` by 1, unless the `stage` is set, 225 | in which case increment the `revision` by 1 or set it to a default of 2 if 226 | there was no `revision` Does nothing when on a commit with a version tag. 227 | 228 | .TP 229 | \fB\-\-full\-commit\fR 230 | Get the full commit hash instead of the short form (only: Git, Mercurial) 231 | 232 | .TP 233 | \fB\-\-commit\-length\fR \fI\,COMMIT_LENGTH\/\fR 234 | Use this many characters from the start of the full commit hash 235 | 236 | .TP 237 | \fB\-\-commit\-prefix\fR \fI\,COMMIT_PREFIX\/\fR 238 | Add this prefix when serializing commit IDs 239 | 240 | .TP 241 | \fB\-\-escape\-with\fR \fI\,ESCAPE_WITH\/\fR 242 | When escaping, replace with this substitution. The default is simply to remove 243 | invalid characters. 244 | 245 | .TP 246 | \fB\-\-tag\-branch\fR \fI\,TAG_BRANCH\/\fR 247 | Branch on which to find tags, if different than the current branch (only: Git) 248 | .RE 249 | 250 | .SS \fBdunamai from mercurial\fR 251 | Generate version from Mercurial 252 | 253 | usage: dunamai from mercurial [\-h] [\-\-metadata] [\-\-no\-metadata] [\-\-dirty] 254 | [\-\-tagged\-metadata] [\-\-pattern PATTERN] 255 | [\-\-pattern\-prefix PATTERN_PREFIX] 256 | [\-\-format FORMAT] [\-\-style {pep440,semver,pvp}] 257 | [\-\-latest\-tag] [\-\-strict] [\-\-path PATH] 258 | [\-\-debug] [\-\-bump] [\-\-full\-commit] 259 | [\-\-commit\-length COMMIT_LENGTH] 260 | [\-\-commit\-prefix COMMIT_PREFIX] 261 | [\-\-escape\-with ESCAPE_WITH] 262 | 263 | Generate version from Mercurial 264 | 265 | options: 266 | .RS 7 267 | .TP 268 | \fB\-\-metadata\fR 269 | Always include metadata. Ignored when \-\-format is used 270 | 271 | .TP 272 | \fB\-\-no\-metadata\fR 273 | Never include metadata. Ignored when \-\-format is used 274 | 275 | .TP 276 | \fB\-\-dirty\fR 277 | Include dirty flag if applicable. Ignored when \-\-format is used 278 | 279 | .TP 280 | \fB\-\-tagged\-metadata\fR 281 | Include tagged metadata if applicable. Ignored when \-\-format is used 282 | 283 | .TP 284 | \fB\-\-pattern\fR \fI\,PATTERN\/\fR 285 | Regular expression matched against the version source. This must contain one 286 | capture group named `base` corresponding to the release segment of the source. 287 | Optionally, it may contain another two groups named `stage` and `revision` 288 | corresponding to a prerelease type (such as 'alpha' or 'rc') and number (such 289 | as in 'alpha\-2' or 'rc3'). It may also contain a group named `tagged_metadata` 290 | corresponding to extra metadata after the main part of the version (typically 291 | after a plus sign). There may also be a group named `epoch` for the PEP 440 292 | concept. If the `base` group is not present, then instead this will be 293 | interpreted as a named preset, which may be one of the following: `default`, 294 | `default\-unprefixed` 295 | 296 | .TP 297 | \fB\-\-pattern\-prefix\fR \fI\,PATTERN_PREFIX\/\fR 298 | Insert this after the pattern's start anchor (`^`). 299 | 300 | .TP 301 | \fB\-\-format\fR \fI\,FORMAT\/\fR 302 | Custom output format. Available substitutions: {base}, {stage}, {revision}, 303 | {distance}, {commit}, {dirty}, {tagged_metadata}, {epoch}, {branch}, 304 | {branch_escaped}, {timestamp}, {major}, {minor}, {patch} 305 | 306 | .TP 307 | \fB\-\-style\fR \fI\,{pep440,semver,pvp}\/\fR 308 | Preconfigured output format. Will default to PEP 440 if not set and no custom 309 | format given. If you specify both a style and a custom format, then the format 310 | will be validated against the style's rules 311 | 312 | .TP 313 | \fB\-\-latest\-tag\fR 314 | Only inspect the latest tag on the latest tagged commit for a pattern match 315 | 316 | .TP 317 | \fB\-\-strict\fR 318 | Elevate warnings to errors. When there are no tags, fail instead of falling 319 | back to 0.0.0 320 | 321 | .TP 322 | \fB\-\-path\fR \fI\,PATH\/\fR 323 | Directory to inspect, if not the current working directory 324 | 325 | .TP 326 | \fB\-\-debug\fR 327 | Display additional information on stderr for troubleshooting 328 | 329 | .TP 330 | \fB\-\-bump\fR 331 | Increment the last part of the version `base` by 1, unless the `stage` is set, 332 | in which case increment the `revision` by 1 or set it to a default of 2 if 333 | there was no `revision` Does nothing when on a commit with a version tag. 334 | 335 | .TP 336 | \fB\-\-full\-commit\fR 337 | Get the full commit hash instead of the short form (only: Git, Mercurial) 338 | 339 | .TP 340 | \fB\-\-commit\-length\fR \fI\,COMMIT_LENGTH\/\fR 341 | Use this many characters from the start of the full commit hash 342 | 343 | .TP 344 | \fB\-\-commit\-prefix\fR \fI\,COMMIT_PREFIX\/\fR 345 | Add this prefix when serializing commit IDs 346 | 347 | .TP 348 | \fB\-\-escape\-with\fR \fI\,ESCAPE_WITH\/\fR 349 | When escaping, replace with this substitution. The default is simply to remove 350 | invalid characters. 351 | .RE 352 | 353 | .SS \fBdunamai from darcs\fR 354 | Generate version from Darcs 355 | 356 | usage: dunamai from darcs [\-h] [\-\-metadata] [\-\-no\-metadata] [\-\-dirty] 357 | [\-\-tagged\-metadata] [\-\-pattern PATTERN] 358 | [\-\-pattern\-prefix PATTERN_PREFIX] [\-\-format FORMAT] 359 | [\-\-style {pep440,semver,pvp}] [\-\-latest\-tag] 360 | [\-\-strict] [\-\-path PATH] [\-\-debug] [\-\-bump] 361 | [\-\-commit\-length COMMIT_LENGTH] 362 | [\-\-commit\-prefix COMMIT_PREFIX] 363 | [\-\-escape\-with ESCAPE_WITH] 364 | 365 | Generate version from Darcs 366 | 367 | options: 368 | .RS 7 369 | .TP 370 | \fB\-\-metadata\fR 371 | Always include metadata. Ignored when \-\-format is used 372 | 373 | .TP 374 | \fB\-\-no\-metadata\fR 375 | Never include metadata. Ignored when \-\-format is used 376 | 377 | .TP 378 | \fB\-\-dirty\fR 379 | Include dirty flag if applicable. Ignored when \-\-format is used 380 | 381 | .TP 382 | \fB\-\-tagged\-metadata\fR 383 | Include tagged metadata if applicable. Ignored when \-\-format is used 384 | 385 | .TP 386 | \fB\-\-pattern\fR \fI\,PATTERN\/\fR 387 | Regular expression matched against the version source. This must contain one 388 | capture group named `base` corresponding to the release segment of the source. 389 | Optionally, it may contain another two groups named `stage` and `revision` 390 | corresponding to a prerelease type (such as 'alpha' or 'rc') and number (such 391 | as in 'alpha\-2' or 'rc3'). It may also contain a group named `tagged_metadata` 392 | corresponding to extra metadata after the main part of the version (typically 393 | after a plus sign). There may also be a group named `epoch` for the PEP 440 394 | concept. If the `base` group is not present, then instead this will be 395 | interpreted as a named preset, which may be one of the following: `default`, 396 | `default\-unprefixed` 397 | 398 | .TP 399 | \fB\-\-pattern\-prefix\fR \fI\,PATTERN_PREFIX\/\fR 400 | Insert this after the pattern's start anchor (`^`). 401 | 402 | .TP 403 | \fB\-\-format\fR \fI\,FORMAT\/\fR 404 | Custom output format. Available substitutions: {base}, {stage}, {revision}, 405 | {distance}, {commit}, {dirty}, {tagged_metadata}, {epoch}, {branch}, 406 | {branch_escaped}, {timestamp}, {major}, {minor}, {patch} 407 | 408 | .TP 409 | \fB\-\-style\fR \fI\,{pep440,semver,pvp}\/\fR 410 | Preconfigured output format. Will default to PEP 440 if not set and no custom 411 | format given. If you specify both a style and a custom format, then the format 412 | will be validated against the style's rules 413 | 414 | .TP 415 | \fB\-\-latest\-tag\fR 416 | Only inspect the latest tag on the latest tagged commit for a pattern match 417 | 418 | .TP 419 | \fB\-\-strict\fR 420 | Elevate warnings to errors. When there are no tags, fail instead of falling 421 | back to 0.0.0 422 | 423 | .TP 424 | \fB\-\-path\fR \fI\,PATH\/\fR 425 | Directory to inspect, if not the current working directory 426 | 427 | .TP 428 | \fB\-\-debug\fR 429 | Display additional information on stderr for troubleshooting 430 | 431 | .TP 432 | \fB\-\-bump\fR 433 | Increment the last part of the version `base` by 1, unless the `stage` is set, 434 | in which case increment the `revision` by 1 or set it to a default of 2 if 435 | there was no `revision` Does nothing when on a commit with a version tag. 436 | 437 | .TP 438 | \fB\-\-commit\-length\fR \fI\,COMMIT_LENGTH\/\fR 439 | Use this many characters from the start of the full commit hash 440 | 441 | .TP 442 | \fB\-\-commit\-prefix\fR \fI\,COMMIT_PREFIX\/\fR 443 | Add this prefix when serializing commit IDs 444 | 445 | .TP 446 | \fB\-\-escape\-with\fR \fI\,ESCAPE_WITH\/\fR 447 | When escaping, replace with this substitution. The default is simply to remove 448 | invalid characters. 449 | .RE 450 | 451 | .SS \fBdunamai from subversion\fR 452 | Generate version from Subversion 453 | 454 | usage: dunamai from subversion [\-h] [\-\-metadata] [\-\-no\-metadata] [\-\-dirty] 455 | [\-\-tagged\-metadata] [\-\-pattern PATTERN] 456 | [\-\-pattern\-prefix PATTERN_PREFIX] 457 | [\-\-format FORMAT] [\-\-style {pep440,semver,pvp}] 458 | [\-\-latest\-tag] [\-\-strict] [\-\-path PATH] 459 | [\-\-debug] [\-\-bump] 460 | [\-\-commit\-length COMMIT_LENGTH] 461 | [\-\-commit\-prefix COMMIT_PREFIX] 462 | [\-\-escape\-with ESCAPE_WITH] [\-\-tag\-dir TAG_DIR] 463 | 464 | Generate version from Subversion 465 | 466 | options: 467 | .RS 7 468 | .TP 469 | \fB\-\-metadata\fR 470 | Always include metadata. Ignored when \-\-format is used 471 | 472 | .TP 473 | \fB\-\-no\-metadata\fR 474 | Never include metadata. Ignored when \-\-format is used 475 | 476 | .TP 477 | \fB\-\-dirty\fR 478 | Include dirty flag if applicable. Ignored when \-\-format is used 479 | 480 | .TP 481 | \fB\-\-tagged\-metadata\fR 482 | Include tagged metadata if applicable. Ignored when \-\-format is used 483 | 484 | .TP 485 | \fB\-\-pattern\fR \fI\,PATTERN\/\fR 486 | Regular expression matched against the version source. This must contain one 487 | capture group named `base` corresponding to the release segment of the source. 488 | Optionally, it may contain another two groups named `stage` and `revision` 489 | corresponding to a prerelease type (such as 'alpha' or 'rc') and number (such 490 | as in 'alpha\-2' or 'rc3'). It may also contain a group named `tagged_metadata` 491 | corresponding to extra metadata after the main part of the version (typically 492 | after a plus sign). There may also be a group named `epoch` for the PEP 440 493 | concept. If the `base` group is not present, then instead this will be 494 | interpreted as a named preset, which may be one of the following: `default`, 495 | `default\-unprefixed` 496 | 497 | .TP 498 | \fB\-\-pattern\-prefix\fR \fI\,PATTERN_PREFIX\/\fR 499 | Insert this after the pattern's start anchor (`^`). 500 | 501 | .TP 502 | \fB\-\-format\fR \fI\,FORMAT\/\fR 503 | Custom output format. Available substitutions: {base}, {stage}, {revision}, 504 | {distance}, {commit}, {dirty}, {tagged_metadata}, {epoch}, {branch}, 505 | {branch_escaped}, {timestamp}, {major}, {minor}, {patch} 506 | 507 | .TP 508 | \fB\-\-style\fR \fI\,{pep440,semver,pvp}\/\fR 509 | Preconfigured output format. Will default to PEP 440 if not set and no custom 510 | format given. If you specify both a style and a custom format, then the format 511 | will be validated against the style's rules 512 | 513 | .TP 514 | \fB\-\-latest\-tag\fR 515 | Only inspect the latest tag on the latest tagged commit for a pattern match 516 | 517 | .TP 518 | \fB\-\-strict\fR 519 | Elevate warnings to errors. When there are no tags, fail instead of falling 520 | back to 0.0.0 521 | 522 | .TP 523 | \fB\-\-path\fR \fI\,PATH\/\fR 524 | Directory to inspect, if not the current working directory 525 | 526 | .TP 527 | \fB\-\-debug\fR 528 | Display additional information on stderr for troubleshooting 529 | 530 | .TP 531 | \fB\-\-bump\fR 532 | Increment the last part of the version `base` by 1, unless the `stage` is set, 533 | in which case increment the `revision` by 1 or set it to a default of 2 if 534 | there was no `revision` Does nothing when on a commit with a version tag. 535 | 536 | .TP 537 | \fB\-\-commit\-length\fR \fI\,COMMIT_LENGTH\/\fR 538 | Use this many characters from the start of the full commit hash 539 | 540 | .TP 541 | \fB\-\-commit\-prefix\fR \fI\,COMMIT_PREFIX\/\fR 542 | Add this prefix when serializing commit IDs 543 | 544 | .TP 545 | \fB\-\-escape\-with\fR \fI\,ESCAPE_WITH\/\fR 546 | When escaping, replace with this substitution. The default is simply to remove 547 | invalid characters. 548 | 549 | .TP 550 | \fB\-\-tag\-dir\fR \fI\,TAG_DIR\/\fR 551 | Location of tags relative to the root (only: Subversion) 552 | .RE 553 | 554 | .SS \fBdunamai from bazaar\fR 555 | Generate version from Bazaar 556 | 557 | usage: dunamai from bazaar [\-h] [\-\-metadata] [\-\-no\-metadata] [\-\-dirty] 558 | [\-\-tagged\-metadata] [\-\-pattern PATTERN] 559 | [\-\-pattern\-prefix PATTERN_PREFIX] [\-\-format FORMAT] 560 | [\-\-style {pep440,semver,pvp}] [\-\-latest\-tag] 561 | [\-\-strict] [\-\-path PATH] [\-\-debug] [\-\-bump] 562 | [\-\-commit\-length COMMIT_LENGTH] 563 | [\-\-commit\-prefix COMMIT_PREFIX] 564 | [\-\-escape\-with ESCAPE_WITH] 565 | 566 | Generate version from Bazaar 567 | 568 | options: 569 | .RS 7 570 | .TP 571 | \fB\-\-metadata\fR 572 | Always include metadata. Ignored when \-\-format is used 573 | 574 | .TP 575 | \fB\-\-no\-metadata\fR 576 | Never include metadata. Ignored when \-\-format is used 577 | 578 | .TP 579 | \fB\-\-dirty\fR 580 | Include dirty flag if applicable. Ignored when \-\-format is used 581 | 582 | .TP 583 | \fB\-\-tagged\-metadata\fR 584 | Include tagged metadata if applicable. Ignored when \-\-format is used 585 | 586 | .TP 587 | \fB\-\-pattern\fR \fI\,PATTERN\/\fR 588 | Regular expression matched against the version source. This must contain one 589 | capture group named `base` corresponding to the release segment of the source. 590 | Optionally, it may contain another two groups named `stage` and `revision` 591 | corresponding to a prerelease type (such as 'alpha' or 'rc') and number (such 592 | as in 'alpha\-2' or 'rc3'). It may also contain a group named `tagged_metadata` 593 | corresponding to extra metadata after the main part of the version (typically 594 | after a plus sign). There may also be a group named `epoch` for the PEP 440 595 | concept. If the `base` group is not present, then instead this will be 596 | interpreted as a named preset, which may be one of the following: `default`, 597 | `default\-unprefixed` 598 | 599 | .TP 600 | \fB\-\-pattern\-prefix\fR \fI\,PATTERN_PREFIX\/\fR 601 | Insert this after the pattern's start anchor (`^`). 602 | 603 | .TP 604 | \fB\-\-format\fR \fI\,FORMAT\/\fR 605 | Custom output format. Available substitutions: {base}, {stage}, {revision}, 606 | {distance}, {commit}, {dirty}, {tagged_metadata}, {epoch}, {branch}, 607 | {branch_escaped}, {timestamp}, {major}, {minor}, {patch} 608 | 609 | .TP 610 | \fB\-\-style\fR \fI\,{pep440,semver,pvp}\/\fR 611 | Preconfigured output format. Will default to PEP 440 if not set and no custom 612 | format given. If you specify both a style and a custom format, then the format 613 | will be validated against the style's rules 614 | 615 | .TP 616 | \fB\-\-latest\-tag\fR 617 | Only inspect the latest tag on the latest tagged commit for a pattern match 618 | 619 | .TP 620 | \fB\-\-strict\fR 621 | Elevate warnings to errors. When there are no tags, fail instead of falling 622 | back to 0.0.0 623 | 624 | .TP 625 | \fB\-\-path\fR \fI\,PATH\/\fR 626 | Directory to inspect, if not the current working directory 627 | 628 | .TP 629 | \fB\-\-debug\fR 630 | Display additional information on stderr for troubleshooting 631 | 632 | .TP 633 | \fB\-\-bump\fR 634 | Increment the last part of the version `base` by 1, unless the `stage` is set, 635 | in which case increment the `revision` by 1 or set it to a default of 2 if 636 | there was no `revision` Does nothing when on a commit with a version tag. 637 | 638 | .TP 639 | \fB\-\-commit\-length\fR \fI\,COMMIT_LENGTH\/\fR 640 | Use this many characters from the start of the full commit hash 641 | 642 | .TP 643 | \fB\-\-commit\-prefix\fR \fI\,COMMIT_PREFIX\/\fR 644 | Add this prefix when serializing commit IDs 645 | 646 | .TP 647 | \fB\-\-escape\-with\fR \fI\,ESCAPE_WITH\/\fR 648 | When escaping, replace with this substitution. The default is simply to remove 649 | invalid characters. 650 | .RE 651 | 652 | .SS \fBdunamai from fossil\fR 653 | Generate version from Fossil 654 | 655 | usage: dunamai from fossil [\-h] [\-\-metadata] [\-\-no\-metadata] [\-\-dirty] 656 | [\-\-tagged\-metadata] [\-\-pattern PATTERN] 657 | [\-\-pattern\-prefix PATTERN_PREFIX] [\-\-format FORMAT] 658 | [\-\-style {pep440,semver,pvp}] [\-\-latest\-tag] 659 | [\-\-strict] [\-\-path PATH] [\-\-debug] [\-\-bump] 660 | [\-\-commit\-length COMMIT_LENGTH] 661 | [\-\-commit\-prefix COMMIT_PREFIX] 662 | [\-\-escape\-with ESCAPE_WITH] 663 | 664 | Generate version from Fossil 665 | 666 | options: 667 | .RS 7 668 | .TP 669 | \fB\-\-metadata\fR 670 | Always include metadata. Ignored when \-\-format is used 671 | 672 | .TP 673 | \fB\-\-no\-metadata\fR 674 | Never include metadata. Ignored when \-\-format is used 675 | 676 | .TP 677 | \fB\-\-dirty\fR 678 | Include dirty flag if applicable. Ignored when \-\-format is used 679 | 680 | .TP 681 | \fB\-\-tagged\-metadata\fR 682 | Include tagged metadata if applicable. Ignored when \-\-format is used 683 | 684 | .TP 685 | \fB\-\-pattern\fR \fI\,PATTERN\/\fR 686 | Regular expression matched against the version source. This must contain one 687 | capture group named `base` corresponding to the release segment of the source. 688 | Optionally, it may contain another two groups named `stage` and `revision` 689 | corresponding to a prerelease type (such as 'alpha' or 'rc') and number (such 690 | as in 'alpha\-2' or 'rc3'). It may also contain a group named `tagged_metadata` 691 | corresponding to extra metadata after the main part of the version (typically 692 | after a plus sign). There may also be a group named `epoch` for the PEP 440 693 | concept. If the `base` group is not present, then instead this will be 694 | interpreted as a named preset, which may be one of the following: `default`, 695 | `default\-unprefixed` 696 | 697 | .TP 698 | \fB\-\-pattern\-prefix\fR \fI\,PATTERN_PREFIX\/\fR 699 | Insert this after the pattern's start anchor (`^`). 700 | 701 | .TP 702 | \fB\-\-format\fR \fI\,FORMAT\/\fR 703 | Custom output format. Available substitutions: {base}, {stage}, {revision}, 704 | {distance}, {commit}, {dirty}, {tagged_metadata}, {epoch}, {branch}, 705 | {branch_escaped}, {timestamp}, {major}, {minor}, {patch} 706 | 707 | .TP 708 | \fB\-\-style\fR \fI\,{pep440,semver,pvp}\/\fR 709 | Preconfigured output format. Will default to PEP 440 if not set and no custom 710 | format given. If you specify both a style and a custom format, then the format 711 | will be validated against the style's rules 712 | 713 | .TP 714 | \fB\-\-latest\-tag\fR 715 | Only inspect the latest tag on the latest tagged commit for a pattern match 716 | 717 | .TP 718 | \fB\-\-strict\fR 719 | Elevate warnings to errors. When there are no tags, fail instead of falling 720 | back to 0.0.0 721 | 722 | .TP 723 | \fB\-\-path\fR \fI\,PATH\/\fR 724 | Directory to inspect, if not the current working directory 725 | 726 | .TP 727 | \fB\-\-debug\fR 728 | Display additional information on stderr for troubleshooting 729 | 730 | .TP 731 | \fB\-\-bump\fR 732 | Increment the last part of the version `base` by 1, unless the `stage` is set, 733 | in which case increment the `revision` by 1 or set it to a default of 2 if 734 | there was no `revision` Does nothing when on a commit with a version tag. 735 | 736 | .TP 737 | \fB\-\-commit\-length\fR \fI\,COMMIT_LENGTH\/\fR 738 | Use this many characters from the start of the full commit hash 739 | 740 | .TP 741 | \fB\-\-commit\-prefix\fR \fI\,COMMIT_PREFIX\/\fR 742 | Add this prefix when serializing commit IDs 743 | 744 | .TP 745 | \fB\-\-escape\-with\fR \fI\,ESCAPE_WITH\/\fR 746 | When escaping, replace with this substitution. The default is simply to remove 747 | invalid characters. 748 | .RE 749 | 750 | .SS \fBdunamai from pijul\fR 751 | Generate version from Pijul 752 | 753 | usage: dunamai from pijul [\-h] [\-\-metadata] [\-\-no\-metadata] [\-\-dirty] 754 | [\-\-tagged\-metadata] [\-\-pattern PATTERN] 755 | [\-\-pattern\-prefix PATTERN_PREFIX] [\-\-format FORMAT] 756 | [\-\-style {pep440,semver,pvp}] [\-\-latest\-tag] 757 | [\-\-strict] [\-\-path PATH] [\-\-debug] [\-\-bump] 758 | [\-\-commit\-length COMMIT_LENGTH] 759 | [\-\-commit\-prefix COMMIT_PREFIX] 760 | [\-\-escape\-with ESCAPE_WITH] 761 | 762 | Generate version from Pijul 763 | 764 | options: 765 | .RS 7 766 | .TP 767 | \fB\-\-metadata\fR 768 | Always include metadata. Ignored when \-\-format is used 769 | 770 | .TP 771 | \fB\-\-no\-metadata\fR 772 | Never include metadata. Ignored when \-\-format is used 773 | 774 | .TP 775 | \fB\-\-dirty\fR 776 | Include dirty flag if applicable. Ignored when \-\-format is used 777 | 778 | .TP 779 | \fB\-\-tagged\-metadata\fR 780 | Include tagged metadata if applicable. Ignored when \-\-format is used 781 | 782 | .TP 783 | \fB\-\-pattern\fR \fI\,PATTERN\/\fR 784 | Regular expression matched against the version source. This must contain one 785 | capture group named `base` corresponding to the release segment of the source. 786 | Optionally, it may contain another two groups named `stage` and `revision` 787 | corresponding to a prerelease type (such as 'alpha' or 'rc') and number (such 788 | as in 'alpha\-2' or 'rc3'). It may also contain a group named `tagged_metadata` 789 | corresponding to extra metadata after the main part of the version (typically 790 | after a plus sign). There may also be a group named `epoch` for the PEP 440 791 | concept. If the `base` group is not present, then instead this will be 792 | interpreted as a named preset, which may be one of the following: `default`, 793 | `default\-unprefixed` 794 | 795 | .TP 796 | \fB\-\-pattern\-prefix\fR \fI\,PATTERN_PREFIX\/\fR 797 | Insert this after the pattern's start anchor (`^`). 798 | 799 | .TP 800 | \fB\-\-format\fR \fI\,FORMAT\/\fR 801 | Custom output format. Available substitutions: {base}, {stage}, {revision}, 802 | {distance}, {commit}, {dirty}, {tagged_metadata}, {epoch}, {branch}, 803 | {branch_escaped}, {timestamp}, {major}, {minor}, {patch} 804 | 805 | .TP 806 | \fB\-\-style\fR \fI\,{pep440,semver,pvp}\/\fR 807 | Preconfigured output format. Will default to PEP 440 if not set and no custom 808 | format given. If you specify both a style and a custom format, then the format 809 | will be validated against the style's rules 810 | 811 | .TP 812 | \fB\-\-latest\-tag\fR 813 | Only inspect the latest tag on the latest tagged commit for a pattern match 814 | 815 | .TP 816 | \fB\-\-strict\fR 817 | Elevate warnings to errors. When there are no tags, fail instead of falling 818 | back to 0.0.0 819 | 820 | .TP 821 | \fB\-\-path\fR \fI\,PATH\/\fR 822 | Directory to inspect, if not the current working directory 823 | 824 | .TP 825 | \fB\-\-debug\fR 826 | Display additional information on stderr for troubleshooting 827 | 828 | .TP 829 | \fB\-\-bump\fR 830 | Increment the last part of the version `base` by 1, unless the `stage` is set, 831 | in which case increment the `revision` by 1 or set it to a default of 2 if 832 | there was no `revision` Does nothing when on a commit with a version tag. 833 | 834 | .TP 835 | \fB\-\-commit\-length\fR \fI\,COMMIT_LENGTH\/\fR 836 | Use this many characters from the start of the full commit hash 837 | 838 | .TP 839 | \fB\-\-commit\-prefix\fR \fI\,COMMIT_PREFIX\/\fR 840 | Add this prefix when serializing commit IDs 841 | 842 | .TP 843 | \fB\-\-escape\-with\fR \fI\,ESCAPE_WITH\/\fR 844 | When escaping, replace with this substitution. The default is simply to remove 845 | invalid characters. 846 | .RE 847 | 848 | .SS \fBdunamai check\fR 849 | Check if a version is valid for a style 850 | 851 | usage: dunamai check [\-h] [\-\-style {pep440,semver,pvp}] [version] 852 | 853 | Check if a version is valid for a style 854 | 855 | arguments: 856 | .RS 7 857 | .TP 858 | \fBversion\fR 859 | Version to check; may be piped in 860 | .RE 861 | 862 | 863 | options: 864 | .RS 7 865 | .TP 866 | \fB\-\-style\fR \fI\,{pep440,semver,pvp}\/\fR 867 | Style against which to check 868 | .RE 869 | 870 | 871 | .SH AUTHOR 872 | .nf 873 | Matthew T. Kennerly (mtkennerly) 874 | .fi 875 | 876 | .SH DISTRIBUTION 877 | The latest version of dunamai may be downloaded from 878 | .UR https://github.com/mtkennerly/dunamai 879 | .UE 880 | -------------------------------------------------------------------------------- /tests/unit/test_dunamai.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import os 3 | import re 4 | from contextlib import contextmanager 5 | from pathlib import Path 6 | from typing import Callable, Iterator, Optional 7 | 8 | try: 9 | import importlib.metadata as ilm 10 | except ImportError: 11 | import importlib_metadata as ilm # type: ignore 12 | 13 | import pytest 14 | 15 | from dunamai import ( 16 | bump_version, 17 | check_version, 18 | get_version, 19 | Version, 20 | serialize_pep440, 21 | serialize_pvp, 22 | serialize_semver, 23 | Pattern, 24 | Style, 25 | Vcs, 26 | _run_cmd, 27 | VERSION_SOURCE_PATTERN, 28 | ) 29 | 30 | 31 | @contextmanager 32 | def chdir(where: Path) -> Iterator[None]: 33 | start = Path.cwd() 34 | os.chdir(str(where)) 35 | try: 36 | yield 37 | finally: 38 | os.chdir(str(start)) 39 | 40 | 41 | def make_run_callback(where: Path) -> Callable: 42 | def inner(command, expected_code: int = 0): 43 | _, out = _run_cmd(command, where=where, codes=[expected_code]) 44 | return out 45 | 46 | return inner 47 | 48 | 49 | def make_from_callback(function: Callable, mock_commit: Optional[str] = "abc") -> Callable: 50 | def inner(*args, **kwargs): 51 | version = function(*args, **kwargs) 52 | if version.commit and mock_commit: 53 | version.commit = mock_commit 54 | return version 55 | 56 | return inner 57 | 58 | 59 | from_any_vcs = make_from_callback(Version.from_any_vcs) 60 | from_any_vcs_unmocked = make_from_callback(Version.from_any_vcs, mock_commit=None) 61 | from_explicit_vcs = make_from_callback(Version.from_vcs) 62 | 63 | 64 | def test__pattern__regex() -> None: 65 | assert Pattern.Default.regex() == VERSION_SOURCE_PATTERN 66 | assert Pattern.DefaultUnprefixed.regex() == VERSION_SOURCE_PATTERN.replace("^v", "^v?", 1) 67 | assert Pattern.Default.regex("foo-") == VERSION_SOURCE_PATTERN.replace("^", "^foo-", 1) 68 | 69 | 70 | def test__pattern__parse() -> None: 71 | assert Pattern.parse(r"(?P\d+)") == r"(?P\d+)" 72 | assert Pattern.parse(r"(?P\d+)", "foo-") == r"(?P\d+)" 73 | assert Pattern.parse(r"^(?P\d+)", "foo-") == r"^foo-(?P\d+)" 74 | 75 | assert Pattern.parse("default") == Pattern.Default.regex() 76 | assert Pattern.parse("default-unprefixed") == Pattern.DefaultUnprefixed.regex() 77 | assert Pattern.parse("default", "foo-") == Pattern.Default.regex("foo-") 78 | 79 | with pytest.raises(ValueError): 80 | Pattern.parse(r"foo") 81 | 82 | 83 | def test__version__init() -> None: 84 | v = Version("1", stage=("a", 2), distance=3, commit="abc", dirty=True, tagged_metadata="def", epoch=4) 85 | assert v.base == "1" 86 | assert v.stage == "a" 87 | assert v.revision == 2 88 | assert v.distance == 3 89 | assert v.commit == "abc" 90 | assert v.dirty 91 | assert v.tagged_metadata == "def" 92 | assert v.epoch == 4 93 | 94 | 95 | def test__version__str() -> None: 96 | v = Version("1", stage=("a", 2), distance=3, commit="abc", dirty=True) 97 | assert str(v) == v.serialize() 98 | 99 | 100 | def test__version__repr() -> None: 101 | v = Version( 102 | "1", 103 | stage=("a", 2), 104 | distance=3, 105 | commit="abc", 106 | dirty=True, 107 | tagged_metadata="tagged", 108 | epoch=4, 109 | branch="master", 110 | timestamp=dt.datetime(2000, 1, 2, 3, 4, 5).replace(tzinfo=dt.timezone.utc), 111 | ) 112 | assert repr(v) == ( 113 | "Version(base='1', stage='a', revision=2, distance=3, commit='abc'," 114 | " dirty=True, tagged_metadata='tagged', epoch=4, branch='master'," 115 | " timestamp=datetime.datetime(2000, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc))" 116 | ) 117 | 118 | 119 | def test__version__ordering() -> None: 120 | assert Version("0.1.0", distance=2) == Version("0.1.0", distance=2) 121 | assert Version("0.2.0") > Version("0.1.0") 122 | assert Version("0.1.0", distance=2) > Version("0.1.0", distance=1) 123 | with pytest.raises(TypeError): 124 | Version("0.1.0") == "0.1.0" 125 | with pytest.raises(TypeError): 126 | Version("0.1.0") < "0.2.0" 127 | assert Version("0.1.0", commit="a") != Version("0.1.0", commit="b") 128 | assert Version("0.1.0", dirty=True) == Version("0.1.0", dirty=True) 129 | assert Version("0.1.0", dirty=False) != Version("0.1.0", dirty=True) 130 | assert Version("0.1.0") != Version("0.1.0", dirty=True) 131 | assert Version("0.1.0") != Version("0.1.0", dirty=False) 132 | 133 | assert Version("0.2.0") < Version("0.2.1") 134 | assert Version("1.1.0") < Version("1.2.0") 135 | assert Version("1.0.0", stage=("rc", 1)) < Version("1.0.0") 136 | assert Version("1.0.0", stage=("a", 1)) < Version("1.0.0", stage=("b", 1)) 137 | assert Version("1.0.0") < Version("1.0.0", stage=("post", 1)) 138 | assert Version("1.0.0", epoch=1) < Version("1.0.0", epoch=2) 139 | assert Version("1.0.0", stage=("bar", 1)) < Version("1.0.0", stage=("foo", 1)) 140 | 141 | 142 | def test__version__serialize__pep440() -> None: 143 | assert Version("0.1.0").serialize() == "0.1.0" 144 | 145 | assert Version("1", stage=("a", 2), distance=3, commit="abc", dirty=True).serialize() == "1a2.post3.dev0+abc" 146 | assert ( 147 | Version("1", stage=("a", 2), distance=3, commit="abc", dirty=True).serialize(dirty=True) 148 | == "1a2.post3.dev0+abc.dirty" 149 | ) 150 | assert ( 151 | Version("1", stage=("a", 2), distance=3, commit="abc", dirty=True).serialize(metadata=False) == "1a2.post3.dev0" 152 | ) 153 | assert ( 154 | Version("1", stage=("a", 2), distance=3, commit="abc", dirty=True).serialize(metadata=False, dirty=True) 155 | == "1a2.post3.dev0" 156 | ) 157 | 158 | assert Version("1", stage=("a", 0), distance=3, commit="abc", dirty=False).serialize() == "1a0.post3.dev0+abc" 159 | assert Version("1", stage=("a", 2), distance=0, commit="abc", dirty=False).serialize() == "1a2" 160 | assert Version("1", stage=("a", 2), distance=3, commit="000", dirty=False).serialize() == "1a2.post3.dev0+000" 161 | assert ( 162 | Version("1", stage=("a", 0), distance=3, commit="abc", dirty=False).serialize(commit_prefix="x") 163 | == "1a0.post3.dev0+xabc" 164 | ) 165 | 166 | assert Version("1", stage=("a", None)).serialize() == "1a0" 167 | assert Version("1", stage=("b", 2)).serialize() == "1b2" 168 | assert Version("1", stage=("rc", 2)).serialize() == "1rc2" 169 | 170 | assert Version("0.1.0").serialize(bump=True) == "0.1.0" 171 | assert Version("0.1.0", distance=3).serialize(bump=True) == "0.1.1.dev3" 172 | assert Version("1", distance=3).serialize(bump=True) == "2.dev3" 173 | assert Version("0.1.0", stage=("a", None), distance=3).serialize(bump=True) == "0.1.0a2.dev3" 174 | assert Version("0.1.0", stage=("b", 2), distance=3).serialize(bump=True) == "0.1.0b3.dev3" 175 | 176 | assert Version("0.1.0", epoch=2).serialize() == "2!0.1.0" 177 | 178 | assert Version("0.1.0", stage=("post", 1)).serialize() == "0.1.0.post1" 179 | assert Version("0.1.0", stage=("post", 1), distance=3).serialize() == "0.1.0.post1.dev3" 180 | assert Version("0.1.0", stage=("post", 1), distance=3).serialize(bump=True) == "0.1.0.post2.dev3" 181 | assert Version("0.1.0", stage=("dev", 1)).serialize() == "0.1.0.dev1" 182 | assert Version("0.1.0", stage=("dev", 1), distance=3).serialize() == "0.1.0.dev4" 183 | assert Version("0.1.0", stage=("dev", 1), distance=3).serialize(bump=True) == "0.1.0.dev5" 184 | 185 | 186 | def test__version__serialize__semver() -> None: 187 | style = Style.SemVer 188 | assert Version("0.1.0").serialize(style=style) == "0.1.0" 189 | 190 | assert ( 191 | Version("0.1.0", stage=("alpha", 2), distance=3, commit="abc", dirty=True).serialize(style=style) 192 | == "0.1.0-alpha.2.post.3+abc" 193 | ) 194 | assert ( 195 | Version("0.1.0", stage=("alpha", 2), distance=3, commit="abc", dirty=True).serialize(dirty=True, style=style) 196 | == "0.1.0-alpha.2.post.3+abc.dirty" 197 | ) 198 | assert ( 199 | Version("0.1.0", stage=("alpha", 2), distance=3, commit="abc", dirty=True).serialize( 200 | metadata=False, style=style 201 | ) 202 | == "0.1.0-alpha.2.post.3" 203 | ) 204 | assert ( 205 | Version("0.1.0", stage=("alpha", 2), distance=3, commit="abc", dirty=True).serialize( 206 | metadata=False, dirty=True, style=style 207 | ) 208 | == "0.1.0-alpha.2.post.3" 209 | ) 210 | 211 | assert ( 212 | Version("0.1.0", stage=("alpha", 0), distance=3, commit="abc", dirty=False).serialize(style=style) 213 | == "0.1.0-alpha.0.post.3+abc" 214 | ) 215 | assert ( 216 | Version("0.1.0", stage=("alpha", 2), distance=0, commit="abc", dirty=False).serialize(style=style) 217 | == "0.1.0-alpha.2" 218 | ) 219 | assert ( 220 | Version("0.1.0", stage=("alpha", 2), distance=3, commit="000", dirty=False).serialize(style=style) 221 | == "0.1.0-alpha.2.post.3+000" 222 | ) 223 | 224 | assert Version("0.1.0", stage=("alpha", None)).serialize(style=style) == "0.1.0-alpha" 225 | assert Version("0.1.0", stage=("beta", 2)).serialize(style=style) == "0.1.0-beta.2" 226 | assert Version("0.1.0", stage=("rc", 2)).serialize(style=style) == "0.1.0-rc.2" 227 | 228 | assert Version("0.1.0").serialize(style=style, bump=True) == "0.1.0" 229 | assert Version("0.1.0", distance=3).serialize(style=style, bump=True) == "0.1.1-pre.3" 230 | assert ( 231 | Version("0.1.0", stage=("alpha", None), distance=3).serialize(style=style, bump=True) == "0.1.0-alpha.2.pre.3" 232 | ) 233 | assert Version("0.1.0", stage=("beta", 2), distance=4).serialize(style=style, bump=True) == "0.1.0-beta.3.pre.4" 234 | 235 | assert Version("0.1.0", epoch=2).serialize(style=style) == "0.1.0" 236 | 237 | 238 | def test__version__serialize__pvp() -> None: 239 | style = Style.Pvp 240 | assert Version("0.1.0").serialize(style=style) == "0.1.0" 241 | 242 | assert ( 243 | Version("0.1.0", stage=("alpha", 2), distance=3, commit="abc", dirty=True).serialize(style=style) 244 | == "0.1.0-alpha-2-post-3-abc" 245 | ) 246 | assert ( 247 | Version("0.1.0", stage=("alpha", 2), distance=3, commit="abc", dirty=True).serialize(dirty=True, style=style) 248 | == "0.1.0-alpha-2-post-3-abc-dirty" 249 | ) 250 | assert ( 251 | Version("0.1.0", stage=("alpha", 2), distance=3, commit="abc", dirty=True).serialize( 252 | metadata=False, style=style 253 | ) 254 | == "0.1.0-alpha-2-post-3" 255 | ) 256 | assert ( 257 | Version("0.1.0", stage=("alpha", 2), distance=3, commit="abc", dirty=True).serialize( 258 | metadata=False, dirty=True, style=style 259 | ) 260 | == "0.1.0-alpha-2-post-3" 261 | ) 262 | 263 | assert ( 264 | Version("0.1.0", stage=("alpha", 0), distance=3, commit="abc", dirty=False).serialize(style=style) 265 | == "0.1.0-alpha-0-post-3-abc" 266 | ) 267 | assert ( 268 | Version("0.1.0", stage=("alpha", 2), distance=0, commit="abc", dirty=False).serialize(style=style) 269 | == "0.1.0-alpha-2" 270 | ) 271 | assert ( 272 | Version("0.1.0", stage=("alpha", 2), distance=3, commit="000", dirty=False).serialize(style=style) 273 | == "0.1.0-alpha-2-post-3-000" 274 | ) 275 | 276 | assert Version("0.1.0", stage=("alpha", None)).serialize(style=style) == "0.1.0-alpha" 277 | assert Version("0.1.0", stage=("beta", 2)).serialize(style=style) == "0.1.0-beta-2" 278 | assert Version("0.1.0", stage=("rc", 2)).serialize(style=style) == "0.1.0-rc-2" 279 | 280 | assert Version("0.1.0").serialize(style=style, bump=True) == "0.1.0" 281 | assert Version("0.1.0", distance=3).serialize(style=style, bump=True) == "0.1.1-pre-3" 282 | assert ( 283 | Version("0.1.0", stage=("alpha", None), distance=3).serialize(style=style, bump=True) == "0.1.0-alpha-2-pre-3" 284 | ) 285 | assert Version("0.1.0", stage=("beta", 2), distance=4).serialize(style=style, bump=True) == "0.1.0-beta-3-pre-4" 286 | 287 | assert Version("0.1.0", epoch=2).serialize(style=style) == "0.1.0" 288 | 289 | 290 | def test__version__serialize__pep440_metadata() -> None: 291 | assert Version("0.1.0").serialize() == "0.1.0" 292 | assert Version("0.1.0").serialize(metadata=True) == "0.1.0" 293 | assert Version("0.1.0").serialize(metadata=False) == "0.1.0" 294 | 295 | assert Version("0.1.0", stage=("a", 1), commit="abc").serialize() == "0.1.0a1" 296 | assert Version("0.1.0", stage=("a", 1), commit="abc").serialize(metadata=True) == "0.1.0a1+abc" 297 | assert Version("0.1.0", stage=("a", 1), commit="abc").serialize(metadata=False) == "0.1.0a1" 298 | 299 | assert Version("0.1.0", distance=1, commit="abc").serialize() == "0.1.0.post1.dev0+abc" 300 | assert Version("0.1.0", distance=1, commit="abc").serialize(metadata=True) == "0.1.0.post1.dev0+abc" 301 | assert Version("0.1.0", distance=1, commit="abc").serialize(metadata=False) == "0.1.0.post1.dev0" 302 | 303 | assert ( 304 | Version("0.1.0", distance=1, commit="abc", tagged_metadata="def").serialize(tagged_metadata=True) 305 | == "0.1.0.post1.dev0+def.abc" 306 | ) 307 | 308 | 309 | def test__version__serialize__semver_with_metadata() -> None: 310 | style = Style.SemVer 311 | assert Version("0.1.0").serialize(style=style) == "0.1.0" 312 | assert Version("0.1.0").serialize(metadata=True, style=style) == "0.1.0" 313 | assert Version("0.1.0").serialize(metadata=False, style=style) == "0.1.0" 314 | 315 | assert Version("0.1.0", stage=("a", 1), commit="abc").serialize(style=style) == "0.1.0-a.1" 316 | assert Version("0.1.0", stage=("a", 1), commit="abc").serialize(metadata=True, style=style) == "0.1.0-a.1+abc" 317 | assert Version("0.1.0", stage=("a", 1), commit="abc").serialize(metadata=False, style=style) == "0.1.0-a.1" 318 | 319 | assert Version("0.1.0", distance=1, commit="abc").serialize(style=style) == "0.1.0-post.1+abc" 320 | assert Version("0.1.0", distance=1, commit="abc").serialize(metadata=True, style=style) == "0.1.0-post.1+abc" 321 | assert Version("0.1.0", distance=1, commit="abc").serialize(metadata=False, style=style) == "0.1.0-post.1" 322 | 323 | assert ( 324 | Version("0.1.0", distance=1, commit="abc", tagged_metadata="def").serialize(style=style, tagged_metadata=True) 325 | == "0.1.0-post.1+def.abc" 326 | ) 327 | 328 | 329 | def test__version__serialize__pvp_with_metadata() -> None: 330 | style = Style.Pvp 331 | assert Version("0.1.0").serialize(style=style) == "0.1.0" 332 | assert Version("0.1.0").serialize(metadata=True, style=style) == "0.1.0" 333 | assert Version("0.1.0").serialize(metadata=False, style=style) == "0.1.0" 334 | 335 | assert Version("0.1.0", stage=("a", 1), commit="abc").serialize(style=style) == "0.1.0-a-1" 336 | assert Version("0.1.0", stage=("a", 1), commit="abc").serialize(metadata=True, style=style) == "0.1.0-a-1-abc" 337 | assert Version("0.1.0", stage=("a", 1), commit="abc").serialize(metadata=False, style=style) == "0.1.0-a-1" 338 | 339 | assert Version("0.1.0", distance=1, commit="abc").serialize(style=style) == "0.1.0-post-1-abc" 340 | assert Version("0.1.0", distance=1, commit="abc").serialize(metadata=True, style=style) == "0.1.0-post-1-abc" 341 | assert Version("0.1.0", distance=1, commit="abc").serialize(metadata=False, style=style) == "0.1.0-post-1" 342 | 343 | assert ( 344 | Version("0.1.0", distance=1, commit="abc", tagged_metadata="def").serialize(style=style, tagged_metadata=True) 345 | == "0.1.0-post-1-def-abc" 346 | ) 347 | 348 | 349 | def test__version__serialize__pep440_with_dirty() -> None: 350 | assert Version("0.1.0", dirty=True).serialize() == "0.1.0" 351 | assert Version("0.1.0", dirty=True).serialize(dirty=True) == "0.1.0+dirty" 352 | 353 | assert Version("0.1.0", dirty=False).serialize() == "0.1.0" 354 | assert Version("0.1.0", dirty=False).serialize(dirty=True) == "0.1.0" 355 | 356 | assert Version("0.1.0", dirty=True).serialize(metadata=True) == "0.1.0" 357 | assert Version("0.1.0", dirty=True).serialize(metadata=True, dirty=True) == "0.1.0+dirty" 358 | 359 | assert Version("0.1.0", dirty=True).serialize(metadata=False) == "0.1.0" 360 | assert Version("0.1.0", dirty=True).serialize(metadata=False, dirty=True) == "0.1.0" 361 | 362 | 363 | def test__version__serialize__semver_with_dirty() -> None: 364 | style = Style.SemVer 365 | assert Version("0.1.0", dirty=True).serialize(style=style) == "0.1.0" 366 | assert Version("0.1.0", dirty=True).serialize(dirty=True, style=style) == "0.1.0+dirty" 367 | 368 | assert Version("0.1.0", dirty=False).serialize(style=style) == "0.1.0" 369 | assert Version("0.1.0", dirty=False).serialize(dirty=True, style=style) == "0.1.0" 370 | 371 | assert Version("0.1.0", dirty=True).serialize(metadata=True, style=style) == "0.1.0" 372 | assert Version("0.1.0", dirty=True).serialize(metadata=True, dirty=True, style=style) == "0.1.0+dirty" 373 | 374 | assert Version("0.1.0", dirty=True).serialize(metadata=False, style=style) == "0.1.0" 375 | assert Version("0.1.0", dirty=True).serialize(metadata=False, dirty=True, style=style) == "0.1.0" 376 | 377 | 378 | def test__version__serialize__pvp_with_dirty() -> None: 379 | style = Style.Pvp 380 | assert Version("0.1.0", dirty=True).serialize(style=style) == "0.1.0" 381 | assert Version("0.1.0", dirty=True).serialize(dirty=True, style=style) == "0.1.0-dirty" 382 | 383 | assert Version("0.1.0", dirty=False).serialize(style=style) == "0.1.0" 384 | assert Version("0.1.0", dirty=False).serialize(dirty=True, style=style) == "0.1.0" 385 | 386 | assert Version("0.1.0", dirty=True).serialize(metadata=True, style=style) == "0.1.0" 387 | assert Version("0.1.0", dirty=True).serialize(metadata=True, dirty=True, style=style) == "0.1.0-dirty" 388 | 389 | assert Version("0.1.0", dirty=True).serialize(metadata=False, style=style) == "0.1.0" 390 | assert Version("0.1.0", dirty=True).serialize(metadata=False, dirty=True, style=style) == "0.1.0" 391 | 392 | 393 | def test__version__serialize__format_as_str() -> None: 394 | format = "{base},{stage},{revision},{distance},{commit},{dirty},{branch},{branch_escaped},{timestamp},{major},{minor},{patch}" 395 | assert Version("0.1.0").serialize(format=format) == "0.1.0,,,0,,clean,,,,0,1,0" 396 | assert ( 397 | Version( 398 | "1", 399 | stage=("a", 2), 400 | distance=3, 401 | commit="abc", 402 | dirty=True, 403 | branch="a/b", 404 | timestamp=dt.datetime(2001, 2, 3, 4, 5, 6, tzinfo=dt.timezone.utc), 405 | ).serialize(format=format) 406 | == "1,a,2,3,abc,dirty,a/b,ab,20010203040506,1,0,0" 407 | ) 408 | with pytest.raises(ValueError): 409 | Version("0.1.0").serialize(format="v{base}", style=Style.Pep440) 410 | 411 | assert Version("1", commit="abc").serialize(format="{commit}", commit_prefix="x") == "xabc" 412 | assert Version("1", branch="a/b").serialize(format="{branch_escaped}", escape_with=".") == "a.b" 413 | 414 | 415 | def test__version__serialize__format_as_callable() -> None: 416 | def format(v: Version) -> str: 417 | return "{},{},{}".format(v.base, v.stage, v.revision) 418 | 419 | assert Version("0.1.0").serialize(format=format) == "0.1.0,None,None" 420 | assert Version("1", stage=("a", 2)).serialize(format=format) == "1,a,2" 421 | with pytest.raises(ValueError): 422 | Version("0.1.0").serialize(format=lambda v: "v{}".format(v.base), style=Style.Pep440) 423 | 424 | def immutable(v: Version) -> str: 425 | v.distance += 100 426 | return v.serialize() 427 | 428 | version = Version("0.1.0") 429 | version.serialize(format=immutable) 430 | assert version.distance == 0 431 | 432 | 433 | def test__version__bump() -> None: 434 | assert Version("1.2.3").bump().serialize() == "1.2.4" 435 | assert Version("1.2.3").bump(-2).serialize() == "1.3.0" 436 | assert Version("1.2.3").bump(0).serialize() == "2.0.0" 437 | assert Version("1.2.3", stage=("a", None)).bump().serialize() == "1.2.3a2" 438 | assert Version("1.2.3", stage=("a", 4)).bump().serialize() == "1.2.3a5" 439 | 440 | assert Version("1.2.3", distance=0).bump(smart=False).serialize() == "1.2.4" 441 | assert Version("1.2.3", distance=0).bump(smart=True).serialize() == "1.2.3" 442 | assert Version("1.2.3", distance=0).serialize(bump=True) == "1.2.3" 443 | 444 | assert Version("1.2.3", distance=5).bump(smart=False).serialize() == "1.2.4.post5.dev0" 445 | assert Version("1.2.3", distance=5).bump(smart=True).serialize() == "1.2.4.dev5" 446 | assert Version("1.2.3", distance=5).serialize(bump=True) == "1.2.4.dev5" 447 | 448 | 449 | def test__version__parse(): 450 | assert Version.parse("1.2.3") == Version("1.2.3") 451 | assert Version.parse("1.2.3a") == Version("1.2.3", stage=("a", None)) 452 | assert Version.parse("1.2.3a3") == Version("1.2.3", stage=("a", 3)) 453 | assert Version.parse("1.2.3+7") == Version("1.2.3", distance=7) 454 | assert Version.parse("1.2.3+d7") == Version("1.2.3", distance=7) 455 | assert Version.parse("1.2.3+b6a9020") == Version("1.2.3", commit="b6a9020") 456 | assert Version.parse("1.2.3+gb6a9020") == Version("1.2.3", commit="b6a9020") 457 | assert Version.parse("1.2.3+dirty") == Version("1.2.3", dirty=True) 458 | assert Version.parse("1.2.3+clean") == Version("1.2.3", dirty=False) 459 | assert Version.parse("1.2.3a3+7.b6a9020.dirty") == Version( 460 | "1.2.3", stage=("a", 3), distance=7, commit="b6a9020", dirty=True 461 | ) 462 | assert Version.parse("1.2.3a3+7.b6a9020.dirty.linux") == Version( 463 | "1.2.3", stage=("a", 3), distance=7, commit="b6a9020", dirty=True, tagged_metadata="linux" 464 | ) 465 | assert Version.parse("2!1.2.3") == Version("1.2.3", epoch=2) 466 | assert Version.parse("2!1.2.3a3+d7.gb6a9020.dirty.linux") == Version( 467 | "1.2.3", 468 | stage=("a", 3), 469 | distance=7, 470 | commit="b6a9020", 471 | dirty=True, 472 | tagged_metadata="linux", 473 | epoch=2, 474 | ) 475 | 476 | assert Version.parse("foo") == Version("foo") 477 | 478 | assert Version.parse("1.2.3.dev5") == Version("1.2.3", distance=5) 479 | assert Version.parse("1.2.3.post4") == Version("1.2.3", stage=("post", 4)) 480 | assert Version.parse("1.2.3.post4+d6") == Version("1.2.3", stage=("post", 4), distance=6) 481 | assert Version.parse("1.2.3.post4.dev5") == Version("1.2.3", distance=4) 482 | assert Version.parse("1.2.3.post4.dev5+d6") == Version("1.2.3", distance=10) 483 | assert Version.parse("1.2.3.post4.dev5.blah6") == Version("1.2.3.post4.dev5.blah6") 484 | 485 | 486 | def test__get_version__from_name() -> None: 487 | assert get_version("dunamai") == Version(ilm.version("dunamai")) 488 | 489 | 490 | def test__get_version__first_choice() -> None: 491 | assert get_version("dunamai", first_choice=lambda: Version("1")) == Version("1") 492 | 493 | 494 | def test__get_version__third_choice() -> None: 495 | assert get_version("dunamai_nonexistent_test", third_choice=lambda: Version("3")) == Version("3") 496 | 497 | 498 | def test__get_version__fallback() -> None: 499 | assert get_version("dunamai_nonexistent_test") == Version("0.0.0") 500 | 501 | 502 | def test__get_version__from_name__ignore() -> None: 503 | assert get_version( 504 | "dunamai", 505 | ignore=[Version(ilm.version("dunamai"))], 506 | fallback=Version("2"), 507 | ) == Version("2") 508 | 509 | 510 | def test__get_version__first_choice__ignore() -> None: 511 | assert get_version( 512 | "dunamai_nonexistent_test", 513 | first_choice=lambda: Version("1"), 514 | ignore=[Version("1")], 515 | fallback=Version("2"), 516 | ) == Version("2") 517 | 518 | 519 | def test__get_version__first_choice__ignore_with_distance() -> None: 520 | assert get_version( 521 | "dunamai_nonexistent_test", 522 | first_choice=lambda: Version("1", distance=2), 523 | ignore=[Version("1")], 524 | fallback=Version("2"), 525 | ) == Version("2") 526 | assert get_version( 527 | "dunamai_nonexistent_test", 528 | first_choice=lambda: Version("1"), 529 | ignore=[Version("1", distance=2)], 530 | fallback=Version("2"), 531 | ) != Version("2") 532 | 533 | 534 | def test__get_version__first_choice__ignore__with_commit() -> None: 535 | assert get_version( 536 | "dunamai_nonexistent_test", 537 | first_choice=lambda: Version("1", commit="aaaa"), 538 | ignore=[Version("1")], 539 | fallback=Version("2"), 540 | ) == Version("2") 541 | 542 | 543 | def test__get_version__first_choice__ignore__without_commit() -> None: 544 | assert get_version( 545 | "dunamai_nonexistent_test", 546 | first_choice=lambda: Version("1"), 547 | ignore=[Version("1", commit="aaaa")], 548 | fallback=Version("2"), 549 | ) == Version("1") 550 | 551 | 552 | def test__get_version__third_choice__ignore() -> None: 553 | assert get_version( 554 | "dunamai_nonexistent_test", 555 | third_choice=lambda: Version("3"), 556 | ignore=[Version("3")], 557 | fallback=Version("2"), 558 | ) == Version("2") 559 | 560 | 561 | def test__version__from_any_vcs(tmp_path) -> None: 562 | with chdir(tmp_path): 563 | with pytest.raises(RuntimeError): 564 | Version.from_any_vcs() 565 | with pytest.raises(RuntimeError): 566 | Version.from_vcs(Vcs.Any) 567 | 568 | 569 | def test__check_version__pep440() -> None: 570 | check_version("0.1.0") 571 | check_version("0.01.0") 572 | 573 | check_version("2!0.1.0") 574 | check_version("23!0.1.0") 575 | check_version("0.1.0a1") 576 | check_version("0.1.0b1") 577 | check_version("0.1.0rc1") 578 | with pytest.raises(ValueError): 579 | check_version("0.1.0x1") 580 | 581 | check_version("0.1.0.post0") 582 | check_version("0.1.0.dev0") 583 | check_version("0.1.0.post0.dev0") 584 | with pytest.raises(ValueError): 585 | check_version("0.1.0.other0") 586 | 587 | check_version("0.1.0+abc.dirty") 588 | check_version("0.1.0+abc..dirty") 589 | with pytest.raises(ValueError): 590 | check_version("0.1.0+abc_dirty") 591 | with pytest.raises(ValueError): 592 | check_version("0.1.0+.abc") 593 | with pytest.raises(ValueError): 594 | check_version("0.1.0+abc.") 595 | 596 | check_version("2!0.1.0a1.post0.dev0+abc.dirty") 597 | 598 | 599 | def test__check_version__semver() -> None: 600 | style = Style.SemVer 601 | 602 | check_version("0.1.0", style=style) 603 | check_version("0.1.0-alpha.1", style=style) 604 | check_version("0.1.0+abc", style=style) 605 | check_version("0.1.0+a.b.c", style=style) 606 | check_version("0.1.0-alpha.1.beta.2+abc.dirty", style=style) 607 | 608 | with pytest.raises(ValueError): 609 | check_version("1", style=style) 610 | with pytest.raises(ValueError): 611 | check_version("0.1", style=style) 612 | with pytest.raises(ValueError): 613 | check_version("0.0.0.1", style=style) 614 | 615 | # "-" is a valid identifier. 616 | Version("0.1.0--").serialize(style=style) 617 | Version("0.1.0--.-").serialize(style=style) 618 | 619 | with pytest.raises(ValueError): 620 | check_version("0.1.0+abc_dirty", style=style) 621 | 622 | # No leading zeroes in numeric segments: 623 | with pytest.raises(ValueError): 624 | Version("00.0.0").serialize(style=style) 625 | with pytest.raises(ValueError): 626 | Version("0.01.0").serialize(style=style) 627 | with pytest.raises(ValueError): 628 | Version("0.1.0-alpha.02").serialize(style=style) 629 | # But leading zeroes are fine for non-numeric parts: 630 | Version("0.1.0-alpha.02a").serialize(style=style) 631 | 632 | # Identifiers can't be empty: 633 | with pytest.raises(ValueError): 634 | Version("0.1.0-.").serialize(style=style) 635 | with pytest.raises(ValueError): 636 | Version("0.1.0-a.").serialize(style=style) 637 | with pytest.raises(ValueError): 638 | Version("0.1.0-.a").serialize(style=style) 639 | 640 | 641 | def test__check_version__pvp() -> None: 642 | style = Style.Pvp 643 | 644 | check_version("1", style=style) 645 | check_version("0.1", style=style) 646 | check_version("0.0.1", style=style) 647 | check_version("0.0.0.1", style=style) 648 | check_version("0.1.0-alpha-1", style=style) 649 | 650 | with pytest.raises(ValueError): 651 | check_version("0.1.0-a.1", style=style) 652 | with pytest.raises(ValueError): 653 | check_version("0.1.0-abc_dirty", style=style) 654 | 655 | 656 | def test__default_version_pattern() -> None: 657 | def check_re( 658 | tag: str, 659 | base: Optional[str] = None, 660 | stage: Optional[str] = None, 661 | revision: Optional[str] = None, 662 | tagged_metadata: Optional[str] = None, 663 | epoch: Optional[str] = None, 664 | ) -> None: 665 | result = re.search(VERSION_SOURCE_PATTERN, tag) 666 | if result is None: 667 | if any(x is not None for x in [base, stage, revision]): 668 | raise ValueError("Pattern did not match, {tag}".format(tag=tag)) 669 | else: 670 | assert result.group("base") == base 671 | assert result.group("stage") == stage 672 | assert result.group("revision") == revision 673 | assert result.group("tagged_metadata") == tagged_metadata 674 | assert result.group("epoch") == epoch 675 | 676 | check_re("v0.1.0", "0.1.0") 677 | check_re("av0.1.0") 678 | 679 | check_re("v0.1.0a", "0.1.0", "a") 680 | check_re("v0.1.0a1", "0.1.0", "a", "1") 681 | check_re("v0.1.0a1b", None) 682 | check_re("v0.1.0-1.a", None) 683 | 684 | check_re("v0.1.0-alpha.123", "0.1.0", "alpha", "123") 685 | check_re("v0.1.0-1.alpha", None) 686 | check_re("v0.1.0-alpha.1.post.4", None) 687 | 688 | check_re("v0.1.0rc.4", "0.1.0", "rc", "4") 689 | check_re("v0.1.0-beta", "0.1.0", "beta") 690 | 691 | check_re("v0.1.0a2", "0.1.0", "a", "2") 692 | check_re("v0.1.0-a-2", "0.1.0", "a", "2") 693 | check_re("v0.1.0.a.2", "0.1.0", "a", "2") 694 | check_re("v0.1.0_a_2", "0.1.0", "a", "2") 695 | 696 | check_re("v0.1.0rc.4+specifier", "0.1.0", "rc", "4", tagged_metadata="specifier") 697 | 698 | check_re("v1", "1") 699 | check_re("v1b2", "1", "b", "2") 700 | 701 | check_re("v1!2", "2", epoch="1") 702 | 703 | 704 | def test__serialize_pep440(): 705 | assert serialize_pep440("1.2.3") == "1.2.3" 706 | assert serialize_pep440("1.2.3", epoch=0) == "0!1.2.3" 707 | assert serialize_pep440("1.2.3", stage="a") == "1.2.3a0" 708 | assert serialize_pep440("1.2.3", stage="a", revision=4) == "1.2.3a4" 709 | assert serialize_pep440("1.2.3", post=4) == "1.2.3.post4" 710 | assert serialize_pep440("1.2.3", dev=4) == "1.2.3.dev4" 711 | assert serialize_pep440("1.2.3", metadata=[]) == "1.2.3" 712 | assert serialize_pep440("1.2.3", metadata=["foo"]) == "1.2.3+foo" 713 | assert serialize_pep440("1.2.3", metadata=["foo", "bar"]) == "1.2.3+foo.bar" 714 | assert serialize_pep440("1.2.3", metadata=[4]) == "1.2.3+4" 715 | 716 | assert ( 717 | serialize_pep440("1.2.3", epoch=0, stage="a", revision=4, post=5, dev=6, metadata=["foo", "bar"]) 718 | == "0!1.2.3a4.post5.dev6+foo.bar" 719 | ) 720 | 721 | assert serialize_pep440("1.2.3", stage="alpha", revision=4) == "1.2.3a4" 722 | assert serialize_pep440("1.2.3", stage="ALphA", revision=4) == "1.2.3a4" 723 | assert serialize_pep440("1.2.3", stage="beta", revision=4) == "1.2.3b4" 724 | assert serialize_pep440("1.2.3", stage="c", revision=4) == "1.2.3rc4" 725 | assert serialize_pep440("1.2.3", stage="pre", revision=4) == "1.2.3rc4" 726 | assert serialize_pep440("1.2.3", stage="preview", revision=4) == "1.2.3rc4" 727 | 728 | with pytest.raises(ValueError): 729 | serialize_pep440("foo") 730 | 731 | 732 | def test__serialize_semver(): 733 | assert serialize_semver("1.2.3") == "1.2.3" 734 | assert serialize_semver("1.2.3", pre=["alpha"]) == "1.2.3-alpha" 735 | assert serialize_semver("1.2.3", pre=["alpha", 4]) == "1.2.3-alpha.4" 736 | assert serialize_semver("1.2.3", metadata=["foo"]) == "1.2.3+foo" 737 | assert serialize_semver("1.2.3", metadata=["foo", "bar"]) == "1.2.3+foo.bar" 738 | assert serialize_semver("1.2.3", metadata=[4]) == "1.2.3+4" 739 | 740 | assert serialize_semver("1.2.3", pre=["alpha", 4], metadata=["foo", "bar"]) == "1.2.3-alpha.4+foo.bar" 741 | 742 | with pytest.raises(ValueError): 743 | serialize_semver("foo") 744 | 745 | 746 | def test__serialize_pvp(): 747 | assert serialize_pvp("1") == "1" 748 | assert serialize_pvp("1.2") == "1.2" 749 | assert serialize_pvp("1.2.3") == "1.2.3" 750 | assert serialize_pvp("1.2.3.4") == "1.2.3.4" 751 | assert serialize_pvp("1.2.3", metadata=["foo"]) == "1.2.3-foo" 752 | assert serialize_pvp("1.2.3", metadata=["foo", "bar"]) == "1.2.3-foo-bar" 753 | assert serialize_pvp("1.2.3", metadata=[4]) == "1.2.3-4" 754 | 755 | with pytest.raises(ValueError): 756 | serialize_pvp("foo") 757 | 758 | 759 | def test__bump_version(): 760 | # default bump=1 761 | assert bump_version("1.2.3") == "1.2.4" 762 | 763 | assert bump_version("1.2.3", 0) == "2.0.0" 764 | assert bump_version("1.2.3", 1) == "1.3.0" 765 | assert bump_version("1.2.3", 2) == "1.2.4" 766 | 767 | assert bump_version("1.2.3", -1) == "1.2.4" 768 | assert bump_version("1.2.3", -2) == "1.3.0" 769 | assert bump_version("1.2.3", -3) == "2.0.0" 770 | 771 | # expicit bump increment 772 | assert bump_version("1.2.3", increment=3) == "1.2.6" 773 | 774 | assert bump_version("1.2.3", 0, increment=3) == "4.0.0" 775 | assert bump_version("1.2.3", 1, increment=3) == "1.5.0" 776 | assert bump_version("1.2.3", 2, increment=3) == "1.2.6" 777 | 778 | assert bump_version("1.2.3", -1, increment=3) == "1.2.6" 779 | assert bump_version("1.2.3", -2, increment=3) == "1.5.0" 780 | assert bump_version("1.2.3", -3, increment=3) == "4.0.0" 781 | 782 | # check if incorrect index raises issues 783 | with pytest.raises(IndexError): 784 | bump_version("1.2.3", 3) 785 | 786 | with pytest.raises(IndexError): 787 | bump_version("1.2.3", -4) 788 | 789 | with pytest.raises(ValueError): 790 | bump_version("foo", 0) 791 | -------------------------------------------------------------------------------- /tests/integration/test_dunamai.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import os 3 | import shutil 4 | import sys 5 | import time 6 | from contextlib import contextmanager 7 | from pathlib import Path 8 | from typing import Callable, Iterator, List, Optional 9 | 10 | import pytest 11 | 12 | from dunamai import Version, Vcs, Concern, _get_git_version, _run_cmd 13 | 14 | 15 | def avoid_identical_ref_timestamps() -> None: 16 | time.sleep(1.2) 17 | 18 | 19 | def lacks_git_version(version: List[int]) -> bool: 20 | if shutil.which("git") is None: 21 | return True 22 | return _get_git_version() < version 23 | 24 | 25 | REPO = Path(__file__).parent.parent.parent 26 | 27 | 28 | @contextmanager 29 | def chdir(where: Path) -> Iterator[None]: 30 | start = Path.cwd() 31 | os.chdir(str(where)) 32 | try: 33 | yield 34 | finally: 35 | os.chdir(str(start)) 36 | 37 | 38 | def is_git_legacy() -> bool: 39 | version = _get_git_version() 40 | return version < [2, 7] 41 | 42 | 43 | def set_missing_env(key: str, value: str, alts: Optional[List[str]] = None) -> None: 44 | if alts is None: 45 | alts = [] 46 | 47 | for k in [key, *alts]: 48 | if os.environ.get(k) is not None: 49 | return 50 | 51 | os.environ[key] = value 52 | 53 | 54 | def make_run_callback(where: Path) -> Callable: 55 | def inner(command, expected_code: int = 0, env: Optional[dict] = None): 56 | _, out = _run_cmd(command, where=where, codes=[expected_code], env=env) 57 | return out 58 | 59 | return inner 60 | 61 | 62 | def make_from_callback(function: Callable, clear: bool = True, chronological: bool = True) -> Callable: 63 | def inner(*args, fresh: bool = False, **kwargs): 64 | version = function(*args, **kwargs) 65 | if fresh: 66 | assert version.commit is None 67 | assert version.timestamp is None 68 | else: 69 | assert isinstance(version.commit, str) 70 | assert len(version.commit) > 0 71 | 72 | if chronological: 73 | assert isinstance(version.timestamp, dt.datetime) 74 | now = dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc) 75 | delta = dt.timedelta(minutes=1) 76 | assert now - delta <= version.timestamp <= now + delta 77 | if clear: 78 | version.commit = None 79 | version.timestamp = None 80 | return version 81 | 82 | return inner 83 | 84 | 85 | from_any_vcs = make_from_callback(Version.from_any_vcs) 86 | from_any_vcs_unmocked = make_from_callback(Version.from_any_vcs, clear=False) 87 | from_explicit_vcs = make_from_callback(Version.from_vcs) 88 | 89 | 90 | @pytest.mark.skipif(shutil.which("git") is None, reason="Requires Git") 91 | def test__version__from_git__with_annotated_tags(tmp_path) -> None: 92 | run = make_run_callback(tmp_path) 93 | from_vcs = make_from_callback(Version.from_git) 94 | b = "master" 95 | legacy = is_git_legacy() 96 | 97 | with chdir(tmp_path): 98 | run("git init") 99 | try: 100 | # Compatibility for newer Git versions: 101 | run("git branch -m master") 102 | except Exception: 103 | pass 104 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=True, branch=b) 105 | assert from_vcs(fresh=True).vcs == Vcs.Git 106 | 107 | # Additional one-off check not in other VCS integration tests: 108 | # strict mode requires there to be a tag 109 | with pytest.raises(RuntimeError): 110 | from_vcs(strict=True) 111 | 112 | (tmp_path / "foo.txt").write_text("hi") 113 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=True, branch=b) 114 | assert from_vcs(fresh=True).concerns == set() 115 | 116 | run("git add .") 117 | run('git commit --no-gpg-sign -m "Initial commit"') 118 | assert from_vcs() == Version("0.0.0", distance=1, dirty=False, branch=b) 119 | 120 | # Detect dirty if untracked files 121 | (tmp_path / "bar.txt").write_text("bye") 122 | assert from_vcs() == Version("0.0.0", distance=1, dirty=True, branch=b) 123 | assert from_vcs(ignore_untracked=True) == Version("0.0.0", distance=1, dirty=False, branch=b) 124 | 125 | # Once the untracked file is removed we are no longer dirty 126 | (tmp_path / "bar.txt").unlink() 127 | assert from_vcs() == Version("0.0.0", distance=1, dirty=False, branch=b) 128 | 129 | # Additional one-off check not in other VCS integration tests: 130 | # when the only tag in the repository does not match the pattern. 131 | run("git tag other -m Annotated") 132 | assert from_vcs() == Version("0.0.0", distance=1, dirty=False, branch=b) 133 | with pytest.raises(ValueError): 134 | from_vcs(strict=True) 135 | 136 | avoid_identical_ref_timestamps() 137 | run("git tag v0.1.0 -m Annotated") 138 | assert from_vcs() == Version("0.1.0", dirty=False, branch=b) 139 | assert from_vcs(latest_tag=True) == Version("0.1.0", dirty=False, branch=b) 140 | assert run("dunamai from git") == "0.1.0" 141 | assert run("dunamai from any") == "0.1.0" 142 | 143 | # Additional one-off checks not in other VCS integration tests: 144 | assert run(r'dunamai from any --pattern "(?P\d\.\d\.\d)"') == "0.1.0" 145 | run(r'dunamai from any --pattern "(\d\.\d\.\d)"', 1) 146 | assert run('dunamai from any --format "v{base}"') == "v0.1.0" 147 | assert run('dunamai from any --style "semver"') == "0.1.0" 148 | assert ( 149 | run('dunamai from any --format "v{base}" --style "semver"', 1) 150 | == "Version 'v0.1.0' does not conform to the Semantic Versioning style" 151 | ) 152 | assert run("dunamai from any --latest-tag") == "0.1.0" 153 | assert from_explicit_vcs(Vcs.Any) == Version("0.1.0", dirty=False, branch=b) 154 | assert from_explicit_vcs(Vcs.Git) == Version("0.1.0", dirty=False, branch=b) 155 | assert run("dunamai from any --bump") == "0.1.0" 156 | assert run('dunamai from git --format "{commit}"') != run('dunamai from git --format "{commit}" --full-commit') 157 | assert run('dunamai from any --format "{commit}"') != run('dunamai from any --format "{commit}" --full-commit') 158 | 159 | if not legacy: 160 | # Verify tags with '/' work 161 | run("git tag test/v0.1.0") 162 | assert run(r'dunamai from any --pattern "^test/v(?P\d\.\d\.\d)"') == "0.1.0" 163 | assert run('dunamai from any --pattern-prefix "test/"') == "0.1.0" 164 | 165 | (tmp_path / "foo.txt").write_text("bye") 166 | assert from_vcs() == Version("0.1.0", dirty=True, branch=b) 167 | 168 | run("git add .") 169 | run('git commit --no-gpg-sign -m "Second"') 170 | assert from_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 171 | assert from_any_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 172 | 173 | run("git tag unmatched -m Annotated") 174 | assert from_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 175 | with pytest.raises(ValueError): 176 | from_vcs(latest_tag=True) 177 | 178 | # check that we find the expected tag that has the most recent tag creation time 179 | if legacy: 180 | run("git tag -d unmatched") 181 | run("git tag v0.2.0 -m Annotated") 182 | if not legacy: 183 | avoid_identical_ref_timestamps() 184 | run("git tag v0.2.0b1 -m Annotated") 185 | avoid_identical_ref_timestamps() 186 | run("git tag v0.2.0 -m Annotated") 187 | avoid_identical_ref_timestamps() 188 | run("git tag v0.1.1 HEAD~1 -m Annotated") 189 | assert from_vcs() == Version("0.2.0", dirty=False, branch=b) 190 | assert from_vcs(latest_tag=True) == Version("0.2.0", dirty=False, branch=b) 191 | 192 | # Check handling with identical tag and branch names: 193 | run("git checkout -b v0.2.0") 194 | assert from_vcs() == Version("0.2.0", dirty=False, branch="heads/v0.2.0") 195 | assert from_vcs(latest_tag=True) == Version("0.2.0", dirty=False, branch="heads/v0.2.0") 196 | 197 | if not legacy: 198 | run("git checkout v0.1.0") 199 | assert from_vcs() == Version("0.1.1", dirty=False) 200 | assert from_vcs(latest_tag=True) == Version("0.1.1", dirty=False) 201 | 202 | # Additional one-off check not in other VCS integration tests: 203 | run("git checkout master") 204 | (tmp_path / "foo.txt").write_text("third") 205 | run("git add .") 206 | run('git commit --no-gpg-sign -m "Third"') 207 | # bumping: 208 | commit = run('dunamai from any --format "{commit}"') 209 | assert run("dunamai from any --bump") == "0.2.1.dev1+{}".format(commit) 210 | if not legacy: 211 | # tag with pre-release segment. 212 | run("git tag v0.2.1b3 -m Annotated") 213 | assert from_vcs() == Version("0.2.1", stage=("b", 3), dirty=False, branch=b) 214 | 215 | if not legacy: 216 | # Additional one-off check: tag containing comma. 217 | (tmp_path / "foo.txt").write_text("fourth") 218 | run("git add .") 219 | run('git commit --no-gpg-sign -m "Fourth"') 220 | run("git tag v0.3.0+a,b -m Annotated") 221 | assert from_vcs() == Version("0.3.0", dirty=False, tagged_metadata="a,b", branch=b) 222 | 223 | if not legacy: 224 | assert from_vcs(path=tmp_path) == Version("0.3.0", dirty=False, tagged_metadata="a,b", branch=b) 225 | 226 | 227 | @pytest.mark.skipif(shutil.which("git") is None, reason="Requires Git") 228 | def test__version__from_git__with_lightweight_tags(tmp_path) -> None: 229 | run = make_run_callback(tmp_path) 230 | from_vcs = make_from_callback(Version.from_git) 231 | b = "master" 232 | legacy = is_git_legacy() 233 | 234 | with chdir(tmp_path): 235 | run("git init") 236 | (tmp_path / "foo.txt").write_text("hi") 237 | run("git add .") 238 | run('git commit --no-gpg-sign -m "Initial commit"') 239 | 240 | run("git tag v0.1.0") 241 | assert from_vcs() == Version("0.1.0", dirty=False, branch=b) 242 | assert from_vcs(latest_tag=True) == Version("0.1.0", dirty=False, branch=b) 243 | assert run("dunamai from git") == "0.1.0" 244 | assert run("dunamai from any") == "0.1.0" 245 | 246 | (tmp_path / "foo.txt").write_text("bye") 247 | run("git add .") 248 | avoid_identical_ref_timestamps() 249 | run('git commit --no-gpg-sign -m "Second"') 250 | assert from_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 251 | assert from_any_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 252 | 253 | (tmp_path / "foo.txt").write_text("again") 254 | run("git add .") 255 | avoid_identical_ref_timestamps() 256 | run('git commit --no-gpg-sign -m "Third"') 257 | assert from_vcs() == Version("0.1.0", distance=2, dirty=False, branch=b) 258 | assert from_any_vcs() == Version("0.1.0", distance=2, dirty=False, branch=b) 259 | 260 | run("git tag v0.2.0") 261 | run("git tag v0.1.1 HEAD~1") 262 | assert from_vcs() == Version("0.2.0", dirty=False, branch=b) 263 | assert from_vcs(latest_tag=True) == Version("0.2.0", dirty=False, branch=b) 264 | 265 | if not legacy: 266 | run("git checkout v0.1.1") 267 | assert from_vcs() == Version("0.1.1", dirty=False) 268 | assert from_vcs(latest_tag=True) == Version("0.1.1", dirty=False) 269 | 270 | 271 | @pytest.mark.skipif(shutil.which("git") is None, reason="Requires Git") 272 | def test__version__from_git__with_mixed_tags(tmp_path) -> None: 273 | run = make_run_callback(tmp_path) 274 | from_vcs = make_from_callback(Version.from_git) 275 | b = "master" 276 | 277 | with chdir(tmp_path): 278 | run("git init") 279 | (tmp_path / "foo.txt").write_text("hi") 280 | run("git add .") 281 | run('git commit --no-gpg-sign -m "Initial commit"') 282 | 283 | run("git tag v0.1.0") 284 | (tmp_path / "foo.txt").write_text("hi 2") 285 | run("git add .") 286 | avoid_identical_ref_timestamps() 287 | run('git commit --no-gpg-sign -m "Second"') 288 | 289 | run('git tag v0.2.0 -m "Annotated"') 290 | assert from_vcs() == Version("0.2.0", dirty=False, branch=b) 291 | assert from_vcs(latest_tag=True) == Version("0.2.0", dirty=False, branch=b) 292 | 293 | (tmp_path / "foo.txt").write_text("hi 3") 294 | run("git add .") 295 | avoid_identical_ref_timestamps() 296 | run('git commit --no-gpg-sign -m "Third"') 297 | 298 | run("git tag v0.3.0") 299 | assert from_vcs() == Version("0.3.0", dirty=False, branch=b) 300 | assert from_vcs(latest_tag=True) == Version("0.3.0", dirty=False, branch=b) 301 | 302 | 303 | @pytest.mark.skipif(shutil.which("git") is None, reason="Requires Git") 304 | def test__version__from_git__with_nonchronological_commits(tmp_path) -> None: 305 | run = make_run_callback(tmp_path) 306 | from_vcs = make_from_callback(Version.from_git, chronological=False) 307 | b = "master" 308 | legacy = is_git_legacy() 309 | 310 | with chdir(tmp_path): 311 | run("git init") 312 | (tmp_path / "foo.txt").write_text("hi") 313 | run("git add .") 314 | run( 315 | 'git commit --no-gpg-sign -m "Initial commit"', 316 | env={ 317 | "GIT_COMMITTER_DATE": "2000-01-02T01:00:00", 318 | "GIT_AUTHOR_DATE": "2000-01-02T01:00:00", 319 | **os.environ, 320 | }, 321 | ) 322 | 323 | run("git tag v0.1.0") 324 | (tmp_path / "foo.txt").write_text("hi 2") 325 | run("git add .") 326 | avoid_identical_ref_timestamps() 327 | run( 328 | 'git commit --no-gpg-sign -m "Second"', 329 | env={ 330 | "GIT_COMMITTER_DATE": "2000-01-01T01:00:00", 331 | "GIT_AUTHOR_DATE": "2000-01-01T01:00:00", 332 | **os.environ, 333 | }, 334 | ) 335 | 336 | run("git tag v0.2.0") 337 | if legacy: 338 | assert from_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 339 | else: 340 | assert from_vcs() == Version("0.2.0", dirty=False, branch=b) 341 | 342 | 343 | @pytest.mark.skipif(shutil.which("git") is None, reason="Requires Git") 344 | @pytest.mark.skipif(is_git_legacy(), reason="Requires non-legacy Git") 345 | def test__version__from_git__gitflow(tmp_path) -> None: 346 | run = make_run_callback(tmp_path) 347 | from_vcs = make_from_callback(Version.from_git) 348 | b = "master" 349 | b2 = "develop" 350 | 351 | with chdir(tmp_path): 352 | run("git init") 353 | (tmp_path / "foo.txt").write_text("hi") 354 | run("git add .") 355 | run("git commit --no-gpg-sign -m Initial") 356 | run("git tag v0.1.0 -m Release") 357 | 358 | run("git checkout -b develop") 359 | (tmp_path / "foo.txt").write_text("second") 360 | run("git add .") 361 | run("git commit --no-gpg-sign -m Second") 362 | 363 | run("git checkout -b release/v0.2.0") 364 | (tmp_path / "foo.txt").write_text("bugfix") 365 | run("git add .") 366 | run("git commit --no-gpg-sign -m Bugfix") 367 | 368 | run("git checkout develop") 369 | run("git merge --no-gpg-sign --no-ff release/v0.2.0") 370 | 371 | run("git checkout master") 372 | run("git merge --no-gpg-sign --no-ff release/v0.2.0") 373 | 374 | run("git tag v0.2.0 -m Release") 375 | assert from_vcs() == Version("0.2.0", dirty=False, branch=b) 376 | 377 | run("git checkout develop") 378 | assert from_vcs() == Version("0.1.0", distance=3, dirty=False, branch=b2) 379 | assert run('dunamai from any --format "{base}+{distance}"') == "0.1.0+3" 380 | assert from_vcs(tag_branch="master") == Version("0.2.0", distance=1, dirty=False, branch=b2) 381 | assert run('dunamai from any --tag-branch master --format "{base}+{distance}"') == "0.2.0+1" 382 | assert run('dunamai from git --tag-branch master --format "{base}+{distance}"') == "0.2.0+1" 383 | 384 | (tmp_path / "foo.txt").write_text("feature") 385 | run("git add .") 386 | run("git commit --no-gpg-sign -m Feature") 387 | assert from_vcs() == Version("0.1.0", distance=4, dirty=False, branch=b2) 388 | assert from_vcs(tag_branch="master") == Version("0.2.0", distance=2, dirty=False, branch=b2) 389 | 390 | run("git checkout master") 391 | assert from_vcs() == Version("0.2.0", dirty=False, branch=b) 392 | assert from_vcs(tag_branch="master") == Version("0.2.0", dirty=False, branch=b) 393 | assert from_vcs(tag_branch="develop") == Version("0.1.0", distance=3, dirty=False, branch=b) 394 | 395 | 396 | def test__version__from_git__archival_untagged() -> None: 397 | with chdir(REPO / "tests" / "archival" / "git-untagged"): 398 | detected = Version.from_git() 399 | assert detected == Version( 400 | "0.0.0", 401 | branch="master", 402 | commit="8fe614d", 403 | timestamp=dt.datetime(2022, 11, 6, 23, 7, 50, tzinfo=dt.timezone.utc), 404 | ) 405 | assert detected._matched_tag is None 406 | assert detected._newer_unmatched_tags is None 407 | 408 | assert Version.from_any_vcs() == detected 409 | 410 | assert Version.from_git(full_commit=True).commit == "8fe614dbf9e767e70442ab8f56e99bd08d7e782d" 411 | 412 | with pytest.raises(RuntimeError): 413 | Version.from_git(strict=True) 414 | 415 | 416 | def test__version__from_git__archival_tagged() -> None: 417 | with chdir(REPO / "tests" / "archival" / "git-tagged"): 418 | detected = Version.from_git() 419 | assert detected == Version( 420 | "0.1.0", 421 | branch="master", 422 | dirty=False, 423 | distance=0, 424 | commit="8fe614d", 425 | timestamp=dt.datetime(2022, 11, 6, 23, 7, 50, tzinfo=dt.timezone.utc), 426 | ) 427 | assert detected._matched_tag == "v0.1.0" 428 | assert detected._newer_unmatched_tags == [] 429 | 430 | 431 | def test__version__from_git__archival_tagged_post() -> None: 432 | with chdir(REPO / "tests" / "archival" / "git-tagged-post"): 433 | detected = Version.from_git() 434 | assert detected == Version( 435 | "0.1.0", 436 | branch="master", 437 | dirty=False, 438 | distance=1, 439 | commit="1b57ff7", 440 | timestamp=dt.datetime(2022, 11, 6, 23, 16, 59, tzinfo=dt.timezone.utc), 441 | ) 442 | assert detected._matched_tag == "v0.1.0" 443 | assert detected._newer_unmatched_tags == [] 444 | 445 | 446 | @pytest.mark.skipif(shutil.which("git") is None, reason="Requires Git") 447 | def test__version__from_git__shallow(tmp_path) -> None: 448 | run = make_run_callback(tmp_path) 449 | 450 | with chdir(tmp_path): 451 | run("git clone --depth 1 https://github.com/mtkennerly/dunamai.git .") 452 | assert Version.from_git().concerns == {Concern.ShallowRepository} 453 | 454 | with pytest.raises(RuntimeError): 455 | Version.from_git(strict=True) 456 | 457 | 458 | @pytest.mark.skipif(shutil.which("git") is None, reason="Requires Git") 459 | @pytest.mark.skipif(is_git_legacy(), reason="Requires non-legacy Git") 460 | def test__version__from_git__trace_env_var(tmp_path) -> None: 461 | run = make_run_callback(tmp_path) 462 | env = {**os.environ, "GIT_TRACE": "1"} 463 | 464 | with chdir(tmp_path): 465 | run("git init") 466 | (tmp_path / "foo.txt").write_text("hi") 467 | run("git add .") 468 | run("git commit --no-gpg-sign -m Initial") 469 | run("git tag v0.1.0 -m Release") 470 | assert run("dunamai from git", env=env) == "0.1.0" 471 | 472 | 473 | @pytest.mark.skipif(lacks_git_version([2, 27]), reason="Requires Git 2.27+") 474 | def test__version__from_git__exclude_decoration(tmp_path) -> None: 475 | run = make_run_callback(tmp_path) 476 | from_vcs = make_from_callback(Version.from_git) 477 | b = "master" 478 | 479 | with chdir(tmp_path): 480 | run("git init") 481 | (tmp_path / "foo.txt").write_text("hi") 482 | run("git add .") 483 | run("git commit --no-gpg-sign -m Initial") 484 | run("git tag v0.1.0 -m Release") 485 | run("git config log.excludeDecoration refs/tags/") 486 | 487 | assert from_vcs() == Version("0.1.0", dirty=False, branch=b) 488 | 489 | 490 | # Older versions of Git fail with code 128: 491 | # "fatal: missing object 0000000000000000000000000000000000000000 for refs/tags/bad.txt" 492 | @pytest.mark.skipif(lacks_git_version([2, 7]), reason="Requires Git 2.7+") 493 | def test__version__from_git__broken_ref(tmp_path) -> None: 494 | run = make_run_callback(tmp_path) 495 | from_vcs = make_from_callback(Version.from_git) 496 | b = "master" 497 | 498 | with chdir(tmp_path): 499 | run("git init") 500 | (tmp_path / "foo.txt").write_text("hi") 501 | run("git add .") 502 | run("git commit --no-gpg-sign -m Initial") 503 | run("git tag v0.1.0 -m Release") 504 | (tmp_path / ".git/refs/tags/bad.txt").touch() 505 | 506 | assert from_vcs() == Version("0.1.0", dirty=False, branch=b) 507 | 508 | 509 | @pytest.mark.skipif(shutil.which("git") is None, reason="Requires Git") 510 | @pytest.mark.skipif(is_git_legacy(), reason="Requires non-legacy Git") 511 | def test__version__from_git__initial_commit_empty_and_tagged(tmp_path) -> None: 512 | run = make_run_callback(tmp_path) 513 | 514 | with chdir(tmp_path): 515 | run("git init") 516 | run("git commit --no-gpg-sign --allow-empty -m Initial") 517 | run("git tag v0.1.0 -m Release") 518 | assert run("dunamai from git") == "0.1.0" 519 | 520 | run("git commit --no-gpg-sign --allow-empty -m Second") 521 | assert run("dunamai from git").startswith("0.1.0.post1.dev0+") 522 | 523 | run("git tag v0.2.0 -m Release") 524 | assert run("dunamai from git") == "0.2.0" 525 | 526 | (tmp_path / "foo.txt").write_text("hi") 527 | run("git add .") 528 | run("git commit --no-gpg-sign -m Third") 529 | run("git tag v0.3.0 -m Release") 530 | assert run("dunamai from git") == "0.3.0" 531 | 532 | 533 | @pytest.mark.skipif(shutil.which("git") is None, reason="Requires Git") 534 | def test__version__not_a_repository(tmp_path) -> None: 535 | run = make_run_callback(tmp_path) 536 | 537 | with chdir(tmp_path): 538 | assert run("dunamai from git", 1) == "This does not appear to be a Git project" 539 | 540 | 541 | @pytest.mark.skipif(shutil.which("hg") is None, reason="Requires Mercurial") 542 | def test__version__from_mercurial(tmp_path) -> None: 543 | run = make_run_callback(tmp_path) 544 | from_vcs = make_from_callback(Version.from_mercurial) 545 | b = "default" 546 | 547 | with chdir(tmp_path): 548 | run("hg init") 549 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=False, branch=b) 550 | assert from_vcs(fresh=True).vcs == Vcs.Mercurial 551 | 552 | (tmp_path / "foo.txt").write_text("hi") 553 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=True, branch=b) 554 | 555 | run("hg add .") 556 | run('hg commit -m "Initial commit"') 557 | assert from_vcs() == Version("0.0.0", distance=1, dirty=False, branch=b) 558 | assert run('dunamai from mercurial --format "{commit}"') != run( 559 | 'dunamai from mercurial --format "{commit}" --full-commit' 560 | ) 561 | assert run('dunamai from any --format "{commit}"') != run('dunamai from any --format "{commit}" --full-commit') 562 | 563 | run("hg tag v0.1.0") 564 | assert from_vcs() == Version("0.1.0", dirty=False, branch=b) 565 | assert from_vcs(latest_tag=True) == Version("0.1.0", dirty=False, branch=b) 566 | assert run("dunamai from mercurial") == "0.1.0" 567 | assert run("dunamai from any") == "0.1.0" 568 | 569 | (tmp_path / "foo.txt").write_text("bye") 570 | assert from_vcs() == Version("0.1.0", dirty=True, branch=b) 571 | 572 | run("hg add .") 573 | run('hg commit -m "Second"') 574 | assert from_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 575 | assert from_any_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 576 | 577 | run("hg tag unmatched") 578 | assert from_vcs() == Version("0.1.0", distance=2, dirty=False, branch=b) 579 | with pytest.raises(ValueError): 580 | from_vcs(latest_tag=True) 581 | 582 | run("hg tag v0.2.0") 583 | assert from_vcs() == Version("0.2.0", dirty=False, branch=b) 584 | assert from_vcs(latest_tag=True) == Version("0.2.0", dirty=False, branch=b) 585 | 586 | run('hg tag v0.1.1 -r "tag(v0.1.0)"') 587 | assert from_vcs() == Version("0.2.0", distance=1, dirty=False, branch=b) 588 | assert from_vcs(latest_tag=True) == Version("0.2.0", distance=1, dirty=False, branch=b) 589 | 590 | run("hg checkout v0.1.0") 591 | assert from_vcs() == Version("0.1.0", dirty=False, branch=b) 592 | assert from_vcs(latest_tag=True) == Version("0.1.0", dirty=False, branch=b) 593 | 594 | assert from_vcs(path=tmp_path) == Version("0.1.0", dirty=False, branch=b) 595 | 596 | 597 | def test__version__from_mercurial__archival_untagged() -> None: 598 | with chdir(REPO / "tests" / "archival" / "hg-untagged"): 599 | detected = Version.from_mercurial() 600 | assert detected == Version( 601 | "0.0.0", 602 | branch="default", 603 | commit="25e474af1332ed4fff9351c70ef8f36352c013f2", 604 | ) 605 | assert detected._matched_tag is None 606 | assert detected._newer_unmatched_tags is None 607 | 608 | assert Version.from_any_vcs() == detected 609 | 610 | with pytest.raises(RuntimeError): 611 | Version.from_mercurial(strict=True) 612 | 613 | 614 | def test__version__from_mercurial__archival_tagged() -> None: 615 | with chdir(REPO / "tests" / "archival" / "hg-tagged"): 616 | detected = Version.from_mercurial() 617 | assert detected == Version( 618 | "0.1.1", 619 | branch="default", 620 | commit="cf36273384e558411364a3a973aaa0cc08e48aea", 621 | ) 622 | assert detected._matched_tag == "v0.1.1" 623 | assert detected._newer_unmatched_tags == ["foo bar"] 624 | 625 | 626 | @pytest.mark.skipif(shutil.which("darcs") is None, reason="Requires Darcs") 627 | def test__version__from_darcs(tmp_path) -> None: 628 | run = make_run_callback(tmp_path) 629 | from_vcs = make_from_callback(Version.from_darcs) 630 | 631 | with chdir(tmp_path): 632 | run("darcs init") 633 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=False) 634 | assert from_vcs(fresh=True).vcs == Vcs.Darcs 635 | 636 | (tmp_path / "foo.txt").write_text("hi") 637 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=True) 638 | 639 | run("darcs add foo.txt") 640 | run('darcs record -am "Initial commit"') 641 | assert from_vcs() == Version("0.0.0", distance=1, dirty=False) 642 | 643 | run("darcs tag v0.1.0") 644 | assert from_vcs() == Version("0.1.0", dirty=False) 645 | assert from_vcs(latest_tag=True) == Version("0.1.0", dirty=False) 646 | assert run("dunamai from darcs") == "0.1.0" 647 | assert run("dunamai from any") == "0.1.0" 648 | 649 | (tmp_path / "foo.txt").write_text("bye") 650 | assert from_vcs() == Version("0.1.0", dirty=True) 651 | 652 | run('darcs record -am "Second"') 653 | assert from_vcs() == Version("0.1.0", distance=1, dirty=False) 654 | assert from_any_vcs() == Version("0.1.0", distance=1, dirty=False) 655 | 656 | run("darcs tag unmatched") 657 | assert from_vcs() == Version("0.1.0", distance=2, dirty=False) 658 | with pytest.raises(ValueError): 659 | from_vcs(latest_tag=True) 660 | 661 | run("darcs tag v0.2.0") 662 | assert from_vcs() == Version("0.2.0", dirty=False) 663 | 664 | run("darcs obliterate --all --last 3") 665 | assert from_vcs() == Version("0.1.0", dirty=False) 666 | 667 | assert from_vcs(path=tmp_path) == Version("0.1.0", dirty=False) 668 | 669 | 670 | @pytest.mark.skipif(None in [shutil.which("svn"), shutil.which("svnadmin")], reason="Requires Subversion") 671 | def test__version__from_subversion(tmp_path) -> None: 672 | vcs = tmp_path / "dunamai-svn" 673 | vcs.mkdir() 674 | run = make_run_callback(vcs) 675 | from_vcs = make_from_callback(Version.from_subversion, clear=False) 676 | 677 | vcs_srv = tmp_path / "dunamai-svn-srv" 678 | vcs_srv.mkdir() 679 | run_srv = make_run_callback(vcs_srv) 680 | vcs_srv_uri = vcs_srv.as_uri() 681 | 682 | with chdir(vcs_srv): 683 | run_srv("svnadmin create .") 684 | 685 | with chdir(vcs): 686 | run('svn checkout "{}" .'.format(vcs_srv_uri)) 687 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=False) 688 | assert from_vcs(fresh=True).vcs == Vcs.Subversion 689 | 690 | run("svn mkdir trunk tags") 691 | 692 | # No tags yet, so version should be 0.0.0. 693 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=True) 694 | 695 | run("svn add --force .") 696 | run('svn commit -m "Initial commit"') # commit 1 697 | run("svn update") 698 | 699 | # A single commit, but still no tags. Version should be 0.0.0. 700 | assert from_vcs() == Version("0.0.0", distance=1, commit="1", dirty=False) 701 | 702 | with chdir(vcs / "trunk"): 703 | # ^-- Make sure things work when we're in trunk, too. 704 | Path("foo.txt").write_text("hi") 705 | run("svn add --force .") 706 | run('svn commit -m "Initial foo.txt commit"') # commit 2 707 | run("svn update") 708 | 709 | # Two commits, but still no tag. Version should still be 0.0.0. 710 | assert from_vcs() == Version("0.0.0", distance=2, commit="2", dirty=False) 711 | 712 | run('svn copy {0}/trunk {0}/tags/v0.1.0 -m "Tag 1"'.format(vcs_srv_uri)) # commit 3 and first tag! 713 | run("svn update") 714 | 715 | # 3 commits, one tag (v.0.1.0), version should be 0.1.0. 716 | assert from_vcs() == Version("0.1.0", commit="3", dirty=False) 717 | assert run("dunamai from subversion") == "0.1.0" 718 | assert run("dunamai from any") == "0.1.0" 719 | 720 | # Dirty the working directory. Make sure we identify it as such. 721 | (vcs / "trunk" / "foo.txt").write_text("bye") 722 | assert from_vcs() == Version("0.1.0", commit="3", dirty=True) 723 | 724 | # Fourth commit, still just one tag. Version should be 0.1.0, and dirty flag 725 | # should be reset. 726 | run('svn commit -m "Fourth"') # commit 4 727 | run("svn update") 728 | assert from_vcs() == Version("0.1.0", distance=1, commit="4", dirty=False) 729 | assert from_any_vcs_unmocked() == Version("0.1.0", distance=1, commit="4", dirty=False) 730 | 731 | # Ensure we get the tag based on the highest commit, not necessarily 732 | # just the newest tag. 733 | run('svn copy {0}/trunk {0}/tags/v0.2.0 -m "Tag 2"'.format(vcs_srv_uri)) # commit 5 734 | run('svn copy {0}/trunk {0}/tags/v0.1.1 -r 1 -m "Tag 3"'.format(vcs_srv_uri)) # commit 6 735 | run("svn update") 736 | assert from_vcs() == Version("0.2.0", distance=1, commit="6", dirty=False) 737 | assert from_vcs(latest_tag=True) == Version("0.2.0", distance=1, commit="6", dirty=False) 738 | 739 | run('svn copy {0}/trunk {0}/tags/unmatched -m "Tag 4"'.format(vcs_srv_uri)) # commit 7 740 | run("svn update") 741 | assert from_vcs() == Version("0.2.0", distance=2, commit="7", dirty=False) 742 | with pytest.raises(ValueError): 743 | from_vcs(latest_tag=True) 744 | 745 | # Checkout an earlier commit. Commit 2 occurred before the first tag 746 | # (v0.1.0, commit #3), so version should be 0.0.0. 747 | run("svn update -r 2") 748 | assert from_vcs() == Version("0.0.0", distance=2, commit="2", dirty=False) 749 | assert from_vcs(latest_tag=True) == Version("0.0.0", distance=2, commit="2", dirty=False) 750 | 751 | with chdir(vcs / "trunk"): 752 | # Do this in trunk, to make sure things still work there. 753 | # Checkout an earlier commit. Commit 3 was first tag (v0.1.0, commit 754 | # #3), so version should be 0.1.0. 755 | run("svn update -r 3") 756 | assert from_vcs() == Version("0.1.0", distance=0, commit="3", dirty=False) 757 | assert from_vcs(latest_tag=True) == Version("0.1.0", distance=0, commit="3", dirty=False) 758 | 759 | assert from_vcs(path=vcs) == Version("0.1.0", distance=0, commit="3", dirty=False) 760 | 761 | 762 | @pytest.mark.skipif(shutil.which("bzr") is None, reason="Requires Bazaar") 763 | def test__version__from_bazaar(tmp_path) -> None: 764 | vcs = tmp_path / "dunamai-bzr" 765 | vcs.mkdir() 766 | run = make_run_callback(vcs) 767 | from_vcs = make_from_callback(Version.from_bazaar, clear=False) 768 | b = "dunamai-bzr" 769 | 770 | with chdir(vcs): 771 | run("bzr init") 772 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=False) 773 | assert from_vcs(fresh=True).vcs == Vcs.Bazaar 774 | 775 | (vcs / "foo.txt").write_text("hi") 776 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=True) 777 | 778 | run("bzr add .") 779 | run('bzr commit -m "Initial commit"') 780 | assert from_vcs() == Version("0.0.0", distance=1, commit="1", dirty=False, branch=b) 781 | 782 | run("bzr tag v0.1.0") 783 | assert from_vcs() == Version("0.1.0", commit="1", dirty=False, branch=b) 784 | assert from_vcs(latest_tag=True) == Version("0.1.0", commit="1", dirty=False, branch=b) 785 | assert run("dunamai from bazaar") == "0.1.0" 786 | assert run("dunamai from any") == "0.1.0" 787 | 788 | (vcs / "foo.txt").write_text("bye") 789 | assert from_vcs() == Version("0.1.0", commit="1", dirty=True, branch=b) 790 | 791 | run("bzr add .") 792 | run('bzr commit -m "Second"') 793 | assert from_vcs() == Version("0.1.0", distance=1, commit="2", dirty=False, branch=b) 794 | assert from_any_vcs_unmocked() == Version("0.1.0", distance=1, commit="2", dirty=False, branch=b) 795 | 796 | run("bzr tag unmatched") 797 | assert from_vcs() == Version("0.1.0", distance=1, commit="2", dirty=False, branch=b) 798 | with pytest.raises(ValueError): 799 | from_vcs(latest_tag=True) 800 | 801 | run("bzr tag v0.2.0") 802 | run("bzr tag v0.1.1 -r v0.1.0") 803 | assert from_vcs() == Version("0.2.0", commit="2", dirty=False, branch=b) 804 | assert from_vcs(latest_tag=True) == Version("0.2.0", commit="2", dirty=False, branch=b) 805 | 806 | run("bzr checkout . old -r v0.1.0") 807 | 808 | with chdir(vcs / "old"): 809 | assert from_vcs() == Version("0.1.1", commit="1", dirty=False, branch=b) 810 | assert from_vcs(latest_tag=True) == Version("0.1.1", commit="1", dirty=False, branch=b) 811 | 812 | with chdir(vcs): 813 | shutil.rmtree("old") 814 | run("bzr nick renamed") 815 | assert from_vcs() == Version("0.2.0", commit="2", dirty=False, branch=b) 816 | (vcs / "foo.txt").write_text("branched") 817 | run("bzr add .") 818 | run('bzr commit -m "branched"') 819 | assert from_vcs() == Version("0.2.0", distance=1, commit="3", dirty=False, branch="renamed") 820 | run("bzr tag v0.2.1") 821 | assert from_vcs() == Version("0.2.1", commit="3", dirty=False, branch="renamed") 822 | 823 | assert from_vcs(path=vcs) == Version("0.2.1", commit="3", dirty=False, branch="renamed") 824 | 825 | 826 | @pytest.mark.skipif(shutil.which("fossil") is None, reason="Requires Fossil") 827 | def test__version__from_fossil(tmp_path) -> None: 828 | run = make_run_callback(tmp_path) 829 | from_vcs = make_from_callback(Version.from_fossil) 830 | b = "trunk" 831 | 832 | if sys.platform != "win32": 833 | set_missing_env("FOSSIL_HOME", str(REPO / "tests"), ["HOME", "XDG_CONFIG_HOME"]) 834 | set_missing_env("USER", "dunamai") 835 | 836 | with chdir(tmp_path): 837 | run("fossil init repo") 838 | run("fossil open repo --force") 839 | assert from_vcs() == Version("0.0.0", distance=0, dirty=False, branch=b) 840 | assert from_vcs().vcs == Vcs.Fossil 841 | 842 | (tmp_path / "foo.txt").write_text("hi") 843 | assert from_vcs() == Version("0.0.0", distance=0, dirty=True, branch=b) 844 | 845 | run("fossil add .") 846 | run('fossil commit -m "Initial commit"') 847 | assert from_vcs() == Version("0.0.0", distance=1, dirty=False, branch=b) 848 | 849 | run("fossil tag add v0.1.0 trunk") 850 | assert from_vcs() == Version("0.1.0", dirty=False, branch=b) 851 | assert from_vcs(latest_tag=True) == Version("0.1.0", dirty=False, branch=b) 852 | assert run("dunamai from fossil") == "0.1.0" 853 | assert run("dunamai from any") == "0.1.0" 854 | 855 | (tmp_path / "foo.txt").write_text("bye") 856 | assert from_vcs() == Version("0.1.0", dirty=True, branch=b) 857 | 858 | run("fossil add .") 859 | run('fossil commit -m "Second"') 860 | assert from_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 861 | assert from_any_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 862 | 863 | run("fossil tag add unmatched trunk") 864 | assert from_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 865 | with pytest.raises(ValueError): 866 | from_vcs(latest_tag=True) 867 | 868 | (tmp_path / "foo.txt").write_text("third") 869 | run("fossil add .") 870 | run("fossil commit --tag v0.2.0 -m 'Third'") 871 | assert from_vcs() == Version("0.2.0", dirty=False, branch=b) 872 | assert from_vcs(latest_tag=True) == Version("0.2.0", dirty=False, branch=b) 873 | 874 | run("fossil tag add v0.1.1 v0.1.0") 875 | assert from_vcs() == Version("0.2.0", dirty=False, branch=b) 876 | assert from_vcs(latest_tag=True) == Version("0.2.0", dirty=False, branch=b) 877 | 878 | run("fossil checkout v0.1.0") 879 | assert from_vcs() == Version("0.1.1", dirty=False, branch=b) 880 | assert from_vcs(latest_tag=True) == Version("0.1.1", dirty=False, branch=b) 881 | 882 | assert from_vcs(path=tmp_path) == Version("0.1.1", dirty=False, branch=b) 883 | 884 | 885 | @pytest.mark.skipif(shutil.which("pijul") is None, reason="Requires Pijul") 886 | def test__version__from_pijul(tmp_path) -> None: 887 | run = make_run_callback(tmp_path) 888 | from_vcs = make_from_callback(Version.from_pijul) 889 | b = "main" 890 | 891 | with chdir(tmp_path): 892 | run("pijul init") 893 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=False, branch=b) 894 | assert from_vcs(fresh=True).vcs == Vcs.Pijul 895 | 896 | (tmp_path / "foo.txt").write_text("hi") 897 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=False, branch=b) 898 | 899 | run("pijul add foo.txt") 900 | assert from_vcs(fresh=True) == Version("0.0.0", distance=0, dirty=True, branch=b) 901 | run('pijul record . -am "Initial commit"') 902 | assert from_vcs() == Version("0.0.0", distance=1, dirty=False, branch=b) 903 | 904 | run("pijul tag create -m v0.1.0") 905 | assert from_vcs() == Version("0.1.0", dirty=False, branch=b) 906 | assert from_vcs(latest_tag=True) == Version("0.1.0", dirty=False, branch=b) 907 | assert run("dunamai from pijul") == "0.1.0" 908 | assert run("dunamai from any") == "0.1.0" 909 | 910 | (tmp_path / "foo.txt").write_text("bye") 911 | assert from_vcs() == Version("0.1.0", dirty=True, branch=b) 912 | 913 | run('pijul record . -am "Second"') 914 | assert from_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 915 | assert from_any_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 916 | 917 | run("pijul tag create -m unmatched") 918 | assert from_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b) 919 | with pytest.raises(ValueError): 920 | from_vcs(latest_tag=True) 921 | 922 | assert from_vcs(path=tmp_path) == Version("0.1.0", distance=1, dirty=False, branch=b) 923 | -------------------------------------------------------------------------------- /poetry.legacy.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "aspy.yaml" 11 | version = "1.3.0" 12 | description = "A few extensions to pyyaml." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | 17 | [package.dependencies] 18 | pyyaml = "*" 19 | 20 | [[package]] 21 | name = "atomicwrites" 22 | version = "1.4.0" 23 | description = "Atomic file writes." 24 | category = "dev" 25 | optional = false 26 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 27 | 28 | [[package]] 29 | name = "attrs" 30 | version = "21.2.0" 31 | description = "Classes Without Boilerplate" 32 | category = "dev" 33 | optional = false 34 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 35 | 36 | [package.extras] 37 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 38 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 39 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 40 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 41 | 42 | [[package]] 43 | name = "backports.entry-points-selectable" 44 | version = "1.1.1" 45 | description = "Compatibility shim providing selectable entry points for older implementations" 46 | category = "dev" 47 | optional = false 48 | python-versions = ">=2.7" 49 | 50 | [package.dependencies] 51 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 52 | 53 | [package.extras] 54 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 55 | testing = ["pytest", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] 56 | 57 | [[package]] 58 | name = "black" 59 | version = "18.9b0" 60 | description = "The uncompromising code formatter." 61 | category = "dev" 62 | optional = false 63 | python-versions = ">=3.6" 64 | 65 | [package.dependencies] 66 | appdirs = "*" 67 | attrs = ">=17.4.0" 68 | click = ">=6.5" 69 | toml = ">=0.9.4" 70 | 71 | [package.extras] 72 | d = ["aiohttp (>=3.3.2)"] 73 | 74 | [[package]] 75 | name = "cfgv" 76 | version = "2.0.1" 77 | description = "Validate configuration and produce human readable error messages." 78 | category = "dev" 79 | optional = false 80 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 81 | 82 | [package.dependencies] 83 | six = "*" 84 | 85 | [[package]] 86 | name = "click" 87 | version = "8.0.3" 88 | description = "Composable command line interface toolkit" 89 | category = "dev" 90 | optional = false 91 | python-versions = ">=3.6" 92 | 93 | [package.dependencies] 94 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 95 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 96 | 97 | [[package]] 98 | name = "colorama" 99 | version = "0.4.4" 100 | description = "Cross-platform colored terminal text." 101 | category = "dev" 102 | optional = false 103 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 104 | 105 | [[package]] 106 | name = "coverage" 107 | version = "5.5" 108 | description = "Code coverage measurement for Python" 109 | category = "dev" 110 | optional = false 111 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 112 | 113 | [package.extras] 114 | toml = ["toml"] 115 | 116 | [[package]] 117 | name = "distlib" 118 | version = "0.3.4" 119 | description = "Distribution utilities" 120 | category = "dev" 121 | optional = false 122 | python-versions = "*" 123 | 124 | [[package]] 125 | name = "filelock" 126 | version = "3.2.1" 127 | description = "A platform independent file lock." 128 | category = "dev" 129 | optional = false 130 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 131 | 132 | [package.extras] 133 | docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] 134 | testing = ["coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] 135 | 136 | [[package]] 137 | name = "flake8" 138 | version = "3.9.2" 139 | description = "the modular source code checker: pep8 pyflakes and co" 140 | category = "dev" 141 | optional = false 142 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 143 | 144 | [package.dependencies] 145 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 146 | mccabe = ">=0.6.0,<0.7.0" 147 | pycodestyle = ">=2.7.0,<2.8.0" 148 | pyflakes = ">=2.3.0,<2.4.0" 149 | 150 | [[package]] 151 | name = "flake8-polyfill" 152 | version = "1.0.2" 153 | description = "Polyfill package for Flake8 plugins" 154 | category = "dev" 155 | optional = false 156 | python-versions = "*" 157 | 158 | [package.dependencies] 159 | flake8 = "*" 160 | 161 | [[package]] 162 | name = "identify" 163 | version = "1.6.2" 164 | description = "File identification library for Python" 165 | category = "dev" 166 | optional = false 167 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 168 | 169 | [package.extras] 170 | license = ["editdistance"] 171 | 172 | [[package]] 173 | name = "importlib-metadata" 174 | version = "2.1.2" 175 | description = "Read metadata from Python packages" 176 | category = "main" 177 | optional = false 178 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 179 | 180 | [package.dependencies] 181 | zipp = ">=0.5" 182 | 183 | [package.extras] 184 | docs = ["sphinx", "rst.linker"] 185 | testing = ["packaging", "pep517", "unittest2", "importlib-resources (>=1.3)"] 186 | 187 | [[package]] 188 | name = "importlib-resources" 189 | version = "3.2.1" 190 | description = "Read resources from Python packages" 191 | category = "dev" 192 | optional = false 193 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 194 | 195 | [package.dependencies] 196 | zipp = {version = ">=0.4", markers = "python_version < \"3.8\""} 197 | 198 | [package.extras] 199 | docs = ["sphinx", "rst.linker", "jaraco.packaging"] 200 | 201 | [[package]] 202 | name = "mccabe" 203 | version = "0.6.1" 204 | description = "McCabe checker, plugin for flake8" 205 | category = "dev" 206 | optional = false 207 | python-versions = "*" 208 | 209 | [[package]] 210 | name = "more-itertools" 211 | version = "8.12.0" 212 | description = "More routines for operating on iterables, beyond itertools" 213 | category = "dev" 214 | optional = false 215 | python-versions = ">=3.5" 216 | 217 | [[package]] 218 | name = "mypy" 219 | version = "0.740" 220 | description = "Optional static typing for Python" 221 | category = "dev" 222 | optional = false 223 | python-versions = ">=3.5" 224 | 225 | [package.dependencies] 226 | mypy-extensions = ">=0.4.0,<0.5.0" 227 | typed-ast = ">=1.4.0,<1.5.0" 228 | typing-extensions = ">=3.7.4" 229 | 230 | [package.extras] 231 | dmypy = ["psutil (>=4.0)"] 232 | 233 | [[package]] 234 | name = "mypy-extensions" 235 | version = "0.4.3" 236 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 237 | category = "dev" 238 | optional = false 239 | python-versions = "*" 240 | 241 | [[package]] 242 | name = "nodeenv" 243 | version = "1.6.0" 244 | description = "Node.js virtual environment builder" 245 | category = "dev" 246 | optional = false 247 | python-versions = "*" 248 | 249 | [[package]] 250 | name = "packaging" 251 | version = "20.9" 252 | description = "Core utilities for Python packages" 253 | category = "main" 254 | optional = false 255 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 256 | 257 | [package.dependencies] 258 | pyparsing = ">=2.0.2" 259 | 260 | [[package]] 261 | name = "pathlib2" 262 | version = "2.3.6" 263 | description = "Object-oriented filesystem paths" 264 | category = "dev" 265 | optional = false 266 | python-versions = "*" 267 | 268 | [package.dependencies] 269 | six = "*" 270 | 271 | [[package]] 272 | name = "pep8-naming" 273 | version = "0.8.2" 274 | description = "Check PEP-8 naming conventions, plugin for flake8" 275 | category = "dev" 276 | optional = false 277 | python-versions = "*" 278 | 279 | [package.dependencies] 280 | flake8-polyfill = ">=1.0.2,<2" 281 | 282 | [[package]] 283 | name = "platformdirs" 284 | version = "2.0.2" 285 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 286 | category = "dev" 287 | optional = false 288 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 289 | 290 | [[package]] 291 | name = "pluggy" 292 | version = "0.13.1" 293 | description = "plugin and hook calling mechanisms for python" 294 | category = "dev" 295 | optional = false 296 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 297 | 298 | [package.dependencies] 299 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 300 | 301 | [package.extras] 302 | dev = ["pre-commit", "tox"] 303 | 304 | [[package]] 305 | name = "pre-commit" 306 | version = "1.21.0" 307 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 308 | category = "dev" 309 | optional = false 310 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 311 | 312 | [package.dependencies] 313 | "aspy.yaml" = "*" 314 | cfgv = ">=2.0.0" 315 | identify = ">=1.0.0" 316 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 317 | importlib-resources = {version = "*", markers = "python_version < \"3.7\""} 318 | nodeenv = ">=0.11.1" 319 | pyyaml = "*" 320 | six = "*" 321 | toml = "*" 322 | virtualenv = ">=15.2" 323 | 324 | [[package]] 325 | name = "py" 326 | version = "1.11.0" 327 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 328 | category = "dev" 329 | optional = false 330 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 331 | 332 | [[package]] 333 | name = "pycodestyle" 334 | version = "2.7.0" 335 | description = "Python style guide checker" 336 | category = "dev" 337 | optional = false 338 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 339 | 340 | [[package]] 341 | name = "pyflakes" 342 | version = "2.3.1" 343 | description = "passive checker of Python programs" 344 | category = "dev" 345 | optional = false 346 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 347 | 348 | [[package]] 349 | name = "pyparsing" 350 | version = "2.4.7" 351 | description = "Python parsing module" 352 | category = "main" 353 | optional = false 354 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 355 | 356 | [[package]] 357 | name = "pytest" 358 | version = "3.10.1" 359 | description = "pytest: simple powerful testing with Python" 360 | category = "dev" 361 | optional = false 362 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 363 | 364 | [package.dependencies] 365 | atomicwrites = ">=1.0" 366 | attrs = ">=17.4.0" 367 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 368 | more-itertools = ">=4.0.0" 369 | pathlib2 = {version = ">=2.2.0", markers = "python_version < \"3.6\""} 370 | pluggy = ">=0.7" 371 | py = ">=1.5.0" 372 | six = ">=1.10.0" 373 | 374 | [[package]] 375 | name = "pytest-cov" 376 | version = "2.9.0" 377 | description = "Pytest plugin for measuring coverage." 378 | category = "dev" 379 | optional = false 380 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 381 | 382 | [package.dependencies] 383 | coverage = ">=4.4" 384 | pytest = ">=3.6" 385 | 386 | [package.extras] 387 | testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"] 388 | 389 | [[package]] 390 | name = "pyyaml" 391 | version = "5.3.1" 392 | description = "YAML parser and emitter for Python" 393 | category = "dev" 394 | optional = false 395 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 396 | 397 | [[package]] 398 | name = "six" 399 | version = "1.16.0" 400 | description = "Python 2 and 3 compatibility utilities" 401 | category = "dev" 402 | optional = false 403 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 404 | 405 | [[package]] 406 | name = "toml" 407 | version = "0.10.2" 408 | description = "Python Library for Tom's Obvious, Minimal Language" 409 | category = "dev" 410 | optional = false 411 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 412 | 413 | [[package]] 414 | name = "tox" 415 | version = "3.24.4" 416 | description = "tox is a generic virtualenv management and test command line tool" 417 | category = "dev" 418 | optional = false 419 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 420 | 421 | [package.dependencies] 422 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 423 | filelock = ">=3.0.0" 424 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 425 | packaging = ">=14" 426 | pluggy = ">=0.12.0" 427 | py = ">=1.4.17" 428 | six = ">=1.14.0" 429 | toml = ">=0.9.4" 430 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 431 | 432 | [package.extras] 433 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 434 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)", "pathlib2 (>=2.3.3)"] 435 | 436 | [[package]] 437 | name = "tox-venv" 438 | version = "0.4.0" 439 | description = "Use Python 3 venvs for Python 3 tox testenvs" 440 | category = "dev" 441 | optional = false 442 | python-versions = "*" 443 | 444 | [package.dependencies] 445 | tox = ">=3.8.1" 446 | 447 | [[package]] 448 | name = "typed-ast" 449 | version = "1.4.3" 450 | description = "a fork of Python 2 and 3 ast modules with type comment support" 451 | category = "dev" 452 | optional = false 453 | python-versions = "*" 454 | 455 | [[package]] 456 | name = "typing-extensions" 457 | version = "3.10.0.2" 458 | description = "Backported and Experimental Type Hints for Python 3.5+" 459 | category = "dev" 460 | optional = false 461 | python-versions = "*" 462 | 463 | [[package]] 464 | name = "virtualenv" 465 | version = "20.10.0" 466 | description = "Virtual Python Environment builder" 467 | category = "dev" 468 | optional = false 469 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 470 | 471 | [package.dependencies] 472 | "backports.entry-points-selectable" = ">=1.0.4" 473 | distlib = ">=0.3.1,<1" 474 | filelock = ">=3.2,<4" 475 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 476 | importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} 477 | platformdirs = ">=2,<3" 478 | six = ">=1.9.0,<2" 479 | 480 | [package.extras] 481 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] 482 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] 483 | 484 | [[package]] 485 | name = "zipp" 486 | version = "1.2.0" 487 | description = "Backport of pathlib-compatible object wrapper for zip files" 488 | category = "main" 489 | optional = false 490 | python-versions = ">=2.7" 491 | 492 | [package.extras] 493 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 494 | testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] 495 | 496 | [metadata] 497 | lock-version = "1.1" 498 | python-versions = "^3.5" 499 | content-hash = "1cdc2129a501d9e2aa43585a3e1d55adc762c47b5a024beac6ce1f2d91d8db3a" 500 | 501 | [metadata.files] 502 | appdirs = [ 503 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 504 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 505 | ] 506 | "aspy.yaml" = [ 507 | {file = "aspy.yaml-1.3.0-py2.py3-none-any.whl", hash = "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc"}, 508 | {file = "aspy.yaml-1.3.0.tar.gz", hash = "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"}, 509 | ] 510 | atomicwrites = [ 511 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 512 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 513 | ] 514 | attrs = [ 515 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 516 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 517 | ] 518 | "backports.entry-points-selectable" = [ 519 | {file = "backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl", hash = "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b"}, 520 | {file = "backports.entry_points_selectable-1.1.1.tar.gz", hash = "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386"}, 521 | ] 522 | black = [ 523 | {file = "black-18.9b0-py36-none-any.whl", hash = "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739"}, 524 | {file = "black-18.9b0.tar.gz", hash = "sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"}, 525 | ] 526 | cfgv = [ 527 | {file = "cfgv-2.0.1-py2.py3-none-any.whl", hash = "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289"}, 528 | {file = "cfgv-2.0.1.tar.gz", hash = "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144"}, 529 | ] 530 | click = [ 531 | {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, 532 | {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, 533 | ] 534 | colorama = [ 535 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 536 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 537 | ] 538 | coverage = [ 539 | {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, 540 | {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, 541 | {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, 542 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, 543 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, 544 | {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, 545 | {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, 546 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, 547 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, 548 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, 549 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, 550 | {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, 551 | {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, 552 | {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, 553 | {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, 554 | {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, 555 | {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, 556 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, 557 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, 558 | {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, 559 | {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, 560 | {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, 561 | {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, 562 | {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, 563 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, 564 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, 565 | {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, 566 | {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, 567 | {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, 568 | {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, 569 | {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, 570 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, 571 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, 572 | {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, 573 | {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, 574 | {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, 575 | {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, 576 | {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, 577 | {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, 578 | {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, 579 | {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, 580 | {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, 581 | {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, 582 | {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, 583 | {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, 584 | {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, 585 | {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, 586 | {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, 587 | {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, 588 | {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, 589 | {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, 590 | {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, 591 | ] 592 | distlib = [ 593 | {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, 594 | {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, 595 | ] 596 | filelock = [ 597 | {file = "filelock-3.2.1-py2.py3-none-any.whl", hash = "sha256:7f07b08d731907441ff40d0c5b81f9512cd968842e0b6264c8bd18a8ce877760"}, 598 | {file = "filelock-3.2.1.tar.gz", hash = "sha256:9cdd29c411ab196cf4c35a1da684f7b9da723696cb356efa45bf5eb1ff313ee3"}, 599 | ] 600 | flake8 = [ 601 | {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, 602 | {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, 603 | ] 604 | flake8-polyfill = [ 605 | {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, 606 | {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, 607 | ] 608 | identify = [ 609 | {file = "identify-1.6.2-py2.py3-none-any.whl", hash = "sha256:8f9879b5b7cca553878d31548a419ec2f227d3328da92fe8202bc5e546d5cbc3"}, 610 | {file = "identify-1.6.2.tar.gz", hash = "sha256:1c2014f6985ed02e62b2e6955578acf069cb2c54859e17853be474bfe7e13bed"}, 611 | ] 612 | importlib-metadata = [ 613 | {file = "importlib_metadata-2.1.2-py2.py3-none-any.whl", hash = "sha256:cd6a92d78385dd145f5f233b3a6919acf5e8e43922aa9b9dbe78573e3540eb56"}, 614 | {file = "importlib_metadata-2.1.2.tar.gz", hash = "sha256:09db40742204610ef6826af16e49f0479d11d0d54687d0169ff7fddf8b3f557f"}, 615 | ] 616 | importlib-resources = [ 617 | {file = "importlib_resources-3.2.1-py2.py3-none-any.whl", hash = "sha256:e2860cf0c4bc999947228d18be154fa3779c5dde0b882bd2d7b3f4d25e698bd6"}, 618 | {file = "importlib_resources-3.2.1.tar.gz", hash = "sha256:a9fe213ab6452708ec1b3f4ec6f2881b8ab3645cb4e5efb7fea2bbf05a91db3b"}, 619 | ] 620 | mccabe = [ 621 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 622 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 623 | ] 624 | more-itertools = [ 625 | {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, 626 | {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, 627 | ] 628 | mypy = [ 629 | {file = "mypy-0.740-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:9371290aa2cad5ad133e4cdc43892778efd13293406f7340b9ffe99d5ec7c1d9"}, 630 | {file = "mypy-0.740-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b428f883d2b3fe1d052c630642cc6afddd07d5cd7873da948644508be3b9d4a7"}, 631 | {file = "mypy-0.740-cp35-cp35m-win_amd64.whl", hash = "sha256:ace6ac1d0f87d4072f05b5468a084a45b4eda970e4d26704f201e06d47ab2990"}, 632 | {file = "mypy-0.740-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:d7574e283f83c08501607586b3167728c58e8442947e027d2d4c7dcd6d82f453"}, 633 | {file = "mypy-0.740-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d5bf0e6ec8ba346a2cf35cb55bf4adfddbc6b6576fcc9e10863daa523e418dbb"}, 634 | {file = "mypy-0.740-cp36-cp36m-win_amd64.whl", hash = "sha256:1521c186a3d200c399bd5573c828ea2db1362af7209b2adb1bb8532cea2fb36f"}, 635 | {file = "mypy-0.740-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:dc889c84241a857c263a2b1cd1121507db7d5b5f5e87e77147097230f374d10b"}, 636 | {file = "mypy-0.740-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6ed3b9b3fdc7193ea7aca6f3c20549b377a56f28769783a8f27191903a54170f"}, 637 | {file = "mypy-0.740-cp37-cp37m-win_amd64.whl", hash = "sha256:31a046ab040a84a0fc38bc93694876398e62bc9f35eca8ccbf6418b7297f4c00"}, 638 | {file = "mypy-0.740-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:672e418425d957e276c291930a3921b4a6413204f53fe7c37cad7bc57b9a3391"}, 639 | {file = "mypy-0.740-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3b1a411909c84b2ae9b8283b58b48541654b918e8513c20a400bb946aa9111ae"}, 640 | {file = "mypy-0.740-cp38-cp38-win_amd64.whl", hash = "sha256:540c9caa57a22d0d5d3c69047cc9dd0094d49782603eb03069821b41f9e970e9"}, 641 | {file = "mypy-0.740-py3-none-any.whl", hash = "sha256:f4748697b349f373002656bf32fede706a0e713d67bfdcf04edf39b1f61d46eb"}, 642 | {file = "mypy-0.740.tar.gz", hash = "sha256:48c8bc99380575deb39f5d3400ebb6a8a1cb5cc669bbba4d3bb30f904e0a0e7d"}, 643 | ] 644 | mypy-extensions = [ 645 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 646 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 647 | ] 648 | nodeenv = [ 649 | {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, 650 | {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, 651 | ] 652 | packaging = [ 653 | {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, 654 | {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, 655 | ] 656 | pathlib2 = [ 657 | {file = "pathlib2-2.3.6-py2.py3-none-any.whl", hash = "sha256:3a130b266b3a36134dcc79c17b3c7ac9634f083825ca6ea9d8f557ee6195c9c8"}, 658 | {file = "pathlib2-2.3.6.tar.gz", hash = "sha256:7d8bcb5555003cdf4a8d2872c538faa3a0f5d20630cb360e518ca3b981795e5f"}, 659 | ] 660 | pep8-naming = [ 661 | {file = "pep8-naming-0.8.2.tar.gz", hash = "sha256:01cb1dab2f3ce9045133d08449f1b6b93531dceacb9ef04f67087c11c723cea9"}, 662 | {file = "pep8_naming-0.8.2-py2.py3-none-any.whl", hash = "sha256:0ec891e59eea766efd3059c3d81f1da304d858220678bdc351aab73c533f2fbb"}, 663 | ] 664 | platformdirs = [ 665 | {file = "platformdirs-2.0.2-py2.py3-none-any.whl", hash = "sha256:0b9547541f599d3d242078ae60b927b3e453f0ad52f58b4d4bc3be86aed3ec41"}, 666 | {file = "platformdirs-2.0.2.tar.gz", hash = "sha256:3b00d081227d9037bbbca521a5787796b5ef5000faea1e43fd76f1d44b06fcfa"}, 667 | ] 668 | pluggy = [ 669 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 670 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 671 | ] 672 | pre-commit = [ 673 | {file = "pre_commit-1.21.0-py2.py3-none-any.whl", hash = "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"}, 674 | {file = "pre_commit-1.21.0.tar.gz", hash = "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850"}, 675 | ] 676 | py = [ 677 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 678 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 679 | ] 680 | pycodestyle = [ 681 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 682 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 683 | ] 684 | pyflakes = [ 685 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, 686 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, 687 | ] 688 | pyparsing = [ 689 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 690 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 691 | ] 692 | pytest = [ 693 | {file = "pytest-3.10.1-py2.py3-none-any.whl", hash = "sha256:3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec"}, 694 | {file = "pytest-3.10.1.tar.gz", hash = "sha256:e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"}, 695 | ] 696 | pytest-cov = [ 697 | {file = "pytest-cov-2.9.0.tar.gz", hash = "sha256:b6a814b8ed6247bd81ff47f038511b57fe1ce7f4cc25b9106f1a4b106f1d9322"}, 698 | {file = "pytest_cov-2.9.0-py2.py3-none-any.whl", hash = "sha256:c87dfd8465d865655a8213859f1b4749b43448b5fae465cb981e16d52a811424"}, 699 | ] 700 | pyyaml = [ 701 | {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, 702 | {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, 703 | {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, 704 | {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, 705 | {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, 706 | {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, 707 | {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, 708 | {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, 709 | {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, 710 | {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, 711 | {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, 712 | {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, 713 | {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, 714 | ] 715 | six = [ 716 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 717 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 718 | ] 719 | toml = [ 720 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 721 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 722 | ] 723 | tox = [ 724 | {file = "tox-3.24.4-py2.py3-none-any.whl", hash = "sha256:5e274227a53dc9ef856767c21867377ba395992549f02ce55eb549f9fb9a8d10"}, 725 | {file = "tox-3.24.4.tar.gz", hash = "sha256:c30b57fa2477f1fb7c36aa1d83292d5c2336cd0018119e1b1c17340e2c2708ca"}, 726 | ] 727 | tox-venv = [ 728 | {file = "tox-venv-0.4.0.tar.gz", hash = "sha256:ea29dc7b21a03951e1e2bd7f3474bbf315657c5454224a5674b2896e9bbb795c"}, 729 | {file = "tox_venv-0.4.0-py2.py3-none-any.whl", hash = "sha256:22c2aba71a991d4adf6902253fa07b3a241d28e4e901cbc9dc86ee8eeaa8d4b4"}, 730 | ] 731 | typed-ast = [ 732 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, 733 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, 734 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, 735 | {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, 736 | {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, 737 | {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, 738 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, 739 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, 740 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, 741 | {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, 742 | {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, 743 | {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, 744 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, 745 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, 746 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, 747 | {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, 748 | {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, 749 | {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, 750 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, 751 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, 752 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, 753 | {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, 754 | {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, 755 | {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, 756 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, 757 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, 758 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, 759 | {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, 760 | {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, 761 | {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, 762 | ] 763 | typing-extensions = [ 764 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 765 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 766 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 767 | ] 768 | virtualenv = [ 769 | {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, 770 | {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, 771 | ] 772 | zipp = [ 773 | {file = "zipp-1.2.0-py2.py3-none-any.whl", hash = "sha256:e0d9e63797e483a30d27e09fffd308c59a700d365ec34e93cc100844168bf921"}, 774 | {file = "zipp-1.2.0.tar.gz", hash = "sha256:c70410551488251b0fee67b460fb9a536af8d6f9f008ad10ac51f615b6a521b1"}, 775 | ] 776 | --------------------------------------------------------------------------------