mirror of
https://github.com/TECHNOFAB11/bumpver.git
synced 2025-12-12 06:20:08 +01:00
Merge branch 'pycalver2' into 'master'
Pycalver2 Closes #8, #12, #2, #11, #10, #9, and #7 See merge request mbarkhau/pycalver!4
This commit is contained in:
commit
8d48b88a1c
63 changed files with 6752 additions and 3266 deletions
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
|
|
@ -1,10 +1,6 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
|
||||
|
|
@ -21,9 +17,9 @@ jobs:
|
|||
path: |
|
||||
~/miniconda3
|
||||
build/*.txt
|
||||
key: ${{ runner.OS }}-conda-cache-${{ hashFiles('requirements/*.txt', 'setup.py', 'makefile*') }}
|
||||
key: ${{ runner.OS }}-conda-cache-${{ hashFiles('requirements/*.txt', 'setup.py', 'Makefile*') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-conda-cache-${{ hashFiles('requirements/*.txt', 'setup.py', 'makefile*') }}
|
||||
${{ runner.OS }}-conda-cache-${{ hashFiles('requirements/*.txt', 'setup.py', 'Makefile*') }}
|
||||
|
||||
- name: make conda
|
||||
run:
|
||||
|
|
|
|||
|
|
@ -18,18 +18,17 @@ lint:
|
|||
allow_failure: false
|
||||
|
||||
|
||||
unit:
|
||||
test:
|
||||
# NOTE: Resource_group is conservative and can be disabled
|
||||
# for simple tests. It should be enabled if the tests
|
||||
# need exclusive access to some resource common external
|
||||
# resource. This will prevent multiple pipelines from
|
||||
# need exclusive access to some common resource. The
|
||||
# resource_group will prevent multiple pipelines from
|
||||
# running concurrently.
|
||||
# resource_group: test-unit
|
||||
stage: test
|
||||
image: registry.gitlab.com/mbarkhau/pycalver/base
|
||||
script:
|
||||
- make test
|
||||
- make test_compat
|
||||
coverage: '/^(TOTAL|src).*?(\d+\%)$/'
|
||||
artifacts:
|
||||
reports:
|
||||
|
|
@ -39,6 +38,18 @@ unit:
|
|||
- reports/testcov/
|
||||
allow_failure: false
|
||||
|
||||
test_compat:
|
||||
# NOTE: Resource_group is conservative and can be disabled
|
||||
# for simple tests. It should be enabled if the tests
|
||||
# need exclusive access to some common resource. The
|
||||
# resource_group will prevent multiple pipelines from
|
||||
# running concurrently.
|
||||
# resource_group: test-unit
|
||||
stage: test
|
||||
image: registry.gitlab.com/mbarkhau/pycalver/base
|
||||
script:
|
||||
- make test_compat
|
||||
allow_failure: false
|
||||
|
||||
pages:
|
||||
stage: build
|
||||
|
|
|
|||
99
CHANGELOG.md
99
CHANGELOG.md
|
|
@ -1,63 +1,126 @@
|
|||
# Changelog for https://gitlab.com/mbarkhau/pycalver
|
||||
# Changelog for https://github.com/mbarkhau/pycalver
|
||||
|
||||
|
||||
## v201907.0036
|
||||
## BumpVer 2020.1100-beta
|
||||
|
||||
Rename package and module from PyCalVer to BumpVer. This name change is due to confusion that this project is either Python specific, or only suitible for CalVer versioning schemes, neither of which is the case.
|
||||
|
||||
This release includes a new syntax for patterns.
|
||||
|
||||
```
|
||||
version_pattern = "vYYYY0M.BUILD[-RELEASE]" # new style
|
||||
version_pattern = "v{year}{month}{build}{release}" # old style
|
||||
|
||||
version_pattern = "MAJOR.MINOR.PATCH" # new style semver
|
||||
version_pattern = "{MAJOR}.{MINOR}.{PATCH}" # old style semver
|
||||
```
|
||||
|
||||
The main reasons for this switch were:
|
||||
- To enable optional parts using braces `[PART]`.
|
||||
- To align the syntax with the conventions used on CalVer.org
|
||||
|
||||
The previous syntax will continue to be supported, but all documentation has been updated to primarily reference new style patterns.
|
||||
|
||||
- Switch main repo from gitlab to github.
|
||||
- New [gitlab#7][gitlab_i7]: New style pattern syntax.
|
||||
- Better support for week numbers.
|
||||
- Better support for optional parts.
|
||||
- New: `BUILD` part now starts at `1000` instead of `0001` to avoid truncation of leading zeros.
|
||||
- New: Add `INC0` (0-based) and `INC1` (1-based) parts that do auto increment and rollover.
|
||||
- New: `MAJOR`/`MINOR`/`PATCH`/`INC` will roll over when a date part changes to their left.
|
||||
- New [gitlab#2][gitlab_i2]: Added `grep` sub-command to help with debugging of patterns.
|
||||
- New [gitlab#10][gitlab_i10]: `--pin-date` to keep date parts unchanged, and only increment non-date parts.
|
||||
- New: Added `--date=<iso-date>` parameter to set explicit date (instead of current date).
|
||||
- New: Added `--release-num` to increment the `alphaN`/`betaN`/`a0`/`b0`/etc. release number
|
||||
- New: Added better error messages to debug regular expressions.
|
||||
- New [gitlab#9][gitlab_i9]: Make commit message configurable.
|
||||
- Fix [gitlab#12][gitlab_i12]: Error with sorting non-lexical version tags (e.g. SemVer).
|
||||
- Fix [gitlab#11][gitlab_i11]: Show regexp when `--verbose` is used.
|
||||
- Fix [gitlab#8][gitlab_i8]: `bumpver update` will now also push HEAD (previously only the tag itself was pushed).
|
||||
- Fix: Disallow `--release=dev`. The semantics of a `dev` releases are different than for other release tags and further development would be required to support them correctly.
|
||||
- Fix: Entries in `file_patterns` were ignored if there were multiple entries for the same file.
|
||||
|
||||
This release no longer includes the `pycalver.lexid` module, which has been moved into its own package: [pypi.org/project/lexid/](https://pypi.org/project/lexid/).
|
||||
|
||||
Many thanks to contributors of this release: @LucidOne, @khanguslee, @chaudum
|
||||
|
||||
[gitlab_i7]:https://gitlab.com/mbarkhau/pycalver/-/issues/7
|
||||
[gitlab_i2]: https://gitlab.com/mbarkhau/pycalver/-/issues/2
|
||||
[gitlab_i10]: https://gitlab.com/mbarkhau/pycalver/-/issues/10
|
||||
[gitlab_i9]: https://gitlab.com/mbarkhau/pycalver/-/issues/9
|
||||
[gitlab_i12]: https://gitlab.com/mbarkhau/pycalver/-/issues/12
|
||||
[gitlab_i11]: https://gitlab.com/mbarkhau/pycalver/-/issues/11
|
||||
[gitlab_i8]: https://gitlab.com/mbarkhau/pycalver/-/issues/8
|
||||
|
||||
|
||||
## PyCalVer v202010.1042
|
||||
|
||||
- Add deprication warning to README.md
|
||||
|
||||
|
||||
## PyCalVer v201907.0036
|
||||
|
||||
- Fix: Don't use git/hg command if `commit=False` is configured (thanks @valentin87)
|
||||
|
||||
|
||||
## v201907.0035
|
||||
## PyCalVer v201907.0035
|
||||
|
||||
- Fix gitlab#6: Add parts `{month_short}`, `{dom_short}`, `{doy_short}`.
|
||||
- Fix gitlab#5: Better warning when using bump with semver (one of --major/--minor/--patch is required)
|
||||
- Fix gitlab#4: Make {release} part optional, so that versions generated by --release=final are parsed.
|
||||
- Fix [gitlab#6][gitlab_i6]: Add parts `{month_short}`, `{dom_short}`, `{doy_short}`.
|
||||
- Fix [gitlab#5][gitlab_i5]: Better warning when using bump with SemVer (one of --major/--minor/--patch is required)
|
||||
- Fix [gitlab#4][gitlab_i4]: Make {release} part optional, so that versions generated by --release=final are parsed.
|
||||
|
||||
[gitlab_i6]: https://gitlab.com/mbarkhau/pycalver/-/issues/6
|
||||
[gitlab_i5]: https://gitlab.com/mbarkhau/pycalver/-/issues/5
|
||||
[gitlab_i4]: https://gitlab.com/mbarkhau/pycalver/-/issues/4
|
||||
|
||||
|
||||
## v201903.0030
|
||||
## PyCalVer v201903.0030
|
||||
|
||||
- Fix: Use pattern from config instead of hardcoded {pycalver} pattern.
|
||||
- Fix: Use pattern from config instead of hard-coded {pycalver} pattern.
|
||||
- Fix: Better error messages for git/hg issues.
|
||||
- Add: Implicit default pattern for config file.
|
||||
|
||||
|
||||
## v201903.0028
|
||||
## PyCalVer v201903.0028
|
||||
|
||||
- Fix: Add warnings when configured files are not under version control.
|
||||
- Add: Coloured output for bump --dry
|
||||
- Add: Colored output for bump --dry
|
||||
|
||||
|
||||
## v201902.0027
|
||||
## PyCalVer v201902.0027
|
||||
|
||||
- Fix: Allow --release=post
|
||||
- Fix: Better error reporting for bad patterns
|
||||
- Fix: Regex escaping issue with "?"
|
||||
|
||||
|
||||
## v201902.0024
|
||||
## PyCalVer v201902.0024
|
||||
|
||||
- Added: Support for globs in file patterns.
|
||||
- Fixed: Better error reporting for invalid config.
|
||||
|
||||
|
||||
## v201902.0020
|
||||
## PyCalVer v201902.0020
|
||||
|
||||
- Added: Support for many more custom version patterns.
|
||||
|
||||
|
||||
## v201812.0018
|
||||
## PyCalVer v201812.0018
|
||||
|
||||
- Fixed: Better handling of pattern replacements with "-final" releases.
|
||||
|
||||
|
||||
## v201812.0017
|
||||
## PyCalVer v201812.0017
|
||||
|
||||
- Fixed github#2. `pycalver init` was broken.
|
||||
- Fixed [github#2]. `pycalver init` was broken.
|
||||
- Fixed pattern escaping issues.
|
||||
- Added lots more tests for cli.
|
||||
- Cleaned up documentation.
|
||||
|
||||
[gihlab_i2]: https://github.com/mbarkhau/pycalver/-/issues/2
|
||||
|
||||
## v201812.0011-beta
|
||||
|
||||
## PyCalVer v201812.0011-beta
|
||||
|
||||
- Add version tags using git/hg.
|
||||
- Use git/hg tags as SSOT for most recent version.
|
||||
|
|
@ -65,6 +128,6 @@
|
|||
- Move to https://gitlab.com/mbarkhau/pycalver
|
||||
|
||||
|
||||
## v201809.0001-alpha
|
||||
## PyCalVer v201809.0001-alpha
|
||||
|
||||
- Initial release
|
||||
|
|
|
|||
|
|
@ -213,9 +213,9 @@ projects by reducing the burden of project setup to a minimum.
|
|||
CHANGELOG.md # short documentation of release history
|
||||
LICENSE # for public libraries (MIT preferred)
|
||||
|
||||
makefile # project specific configuration
|
||||
Makefile # project specific configuration
|
||||
# variables and make targets
|
||||
makefile.bootstrapit.make # bootstrapit make include library
|
||||
Makefile.bootstrapit.make # bootstrapit make include library
|
||||
|
||||
docker_base.Dockerfile # base image for CI (only conda envs)
|
||||
Dockerfile # image with source of the project
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ ADD pylint-ignore.md pylint-ignore.md
|
|||
ADD README.md README.md
|
||||
ADD CHANGELOG.md CHANGELOG.md
|
||||
ADD LICENSE LICENSE
|
||||
ADD makefile makefile
|
||||
ADD makefile.bootstrapit.make makefile.bootstrapit.make
|
||||
ADD Makefile Makefile
|
||||
ADD Makefile.bootstrapit.make Makefile.bootstrapit.make
|
||||
ADD scripts/exit_0_if_empty.py scripts/exit_0_if_empty.py
|
||||
|
||||
ENV PYTHONPATH="src/:vendor/"
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -1,4 +1,4 @@
|
|||
MIT License Copyright (c) 2020 Manuel Barkhau (mbarkhau@gmail.com)
|
||||
MIT License Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ DEVELOPMENT_PYTHON_VERSION := python=3.8
|
|||
SUPPORTED_PYTHON_VERSIONS := python=2.7 python=3.5 python=3.6 python=3.8 pypy2.7 pypy3.5
|
||||
|
||||
|
||||
include makefile.bootstrapit.make
|
||||
include Makefile.bootstrapit.make
|
||||
|
||||
## -- Extra/Custom/Project Specific Tasks --
|
||||
|
||||
|
|
@ -61,3 +61,19 @@ test_compat: $(COMPAT_TEST_FILES)
|
|||
ENABLE_BACKTRACE=0 PYTHONPATH="" ENV=$${ENV-dev} \
|
||||
$${env_py} -m pytest --verbose compat_test/; \
|
||||
done;
|
||||
|
||||
rm -rf compat_test/
|
||||
|
||||
|
||||
pycalver_deps.svg:
|
||||
pydeps src/pycalver \
|
||||
--no-show --noise-level 3 \
|
||||
--reverse --include-missing \
|
||||
-x 'click.*' 'toml.*' 'pretty_traceback.*' \
|
||||
-o pycalver_deps.svg
|
||||
|
||||
|
||||
## Update cli reference in README.md
|
||||
README.md: src/pycalver2/cli.py scripts/update_readme_examples.py Makefile
|
||||
@git add README.md
|
||||
@$(DEV_ENV)/bin/python scripts/update_readme_examples.py
|
||||
|
|
@ -57,8 +57,7 @@ CONDA_ENV_BIN_PYTHON_PATHS := \
|
|||
empty :=
|
||||
literal_space := $(empty) $(empty)
|
||||
|
||||
BDIST_WHEEL_PYTHON_TAG := \
|
||||
$(subst python,py,$(subst $(literal_space),.,$(subst .,,$(subst =,,$(SUPPORTED_PYTHON_VERSIONS)))))
|
||||
BDIST_WHEEL_PYTHON_TAG := py2.py3
|
||||
|
||||
SDIST_FILE_CMD = ls -1t dist/*.tar.gz | head -n 1
|
||||
|
||||
|
|
@ -182,7 +181,7 @@ help:
|
|||
helpMessage = ""; \
|
||||
} \
|
||||
}' \
|
||||
makefile.bootstrapit.make makefile
|
||||
Makefile.bootstrapit.make Makefile
|
||||
|
||||
@if [[ ! -f $(DEV_ENV_PY) ]]; then \
|
||||
echo "Missing python interpreter at $(DEV_ENV_PY) !"; \
|
||||
|
|
@ -236,7 +235,7 @@ helpverbose:
|
|||
helpMessage = ""; \
|
||||
} \
|
||||
}' \
|
||||
makefile.bootstrapit.make makefile
|
||||
Makefile.bootstrapit.make Makefile
|
||||
|
||||
|
||||
## -- Project Setup --
|
||||
|
|
@ -301,7 +300,6 @@ git_hooks:
|
|||
lint_isort:
|
||||
@printf "isort ...\n"
|
||||
@$(DEV_ENV)/bin/isort \
|
||||
--recursive \
|
||||
--check-only \
|
||||
--line-width=$(MAX_LINE_LEN) \
|
||||
--project $(MODULE_NAME) \
|
||||
|
|
@ -336,6 +334,15 @@ lint_flake8:
|
|||
@printf "\e[1F\e[9C ok\n"
|
||||
|
||||
|
||||
## Run pylint --errors-only.
|
||||
.PHONY: lint_pylint_errors
|
||||
lint_pylint_errors:
|
||||
@printf "pylint ..\n";
|
||||
@$(DEV_ENV)/bin/pylint --errors-only --jobs=4 --rcfile=setup.cfg \
|
||||
src/ test/
|
||||
@printf "\e[1F\e[9C ok\n"
|
||||
|
||||
|
||||
## Run pylint.
|
||||
.PHONY: lint_pylint
|
||||
lint_pylint:
|
||||
|
|
@ -395,7 +402,7 @@ test:
|
|||
--cov-report term \
|
||||
--html=reports/pytest/index.html \
|
||||
--junitxml reports/pytest.xml \
|
||||
-k "$${PYTEST_FILTER}" \
|
||||
-k "$${PYTEST_FILTER-$${FLTR}}" \
|
||||
$(shell cd src/ && ls -1 */__init__.py | awk '{ sub(/\/__init__.py/, "", $$1); print "--cov "$$1 }') \
|
||||
test/ src/;
|
||||
|
||||
|
|
@ -417,7 +424,6 @@ test:
|
|||
.PHONY: fmt_isort
|
||||
fmt_isort:
|
||||
@$(DEV_ENV)/bin/isort \
|
||||
--recursive \
|
||||
--line-width=$(MAX_LINE_LEN) \
|
||||
--project $(MODULE_NAME) \
|
||||
src/ test/;
|
||||
|
|
@ -515,7 +521,7 @@ devtest:
|
|||
--capture=no \
|
||||
--exitfirst \
|
||||
--failed-first \
|
||||
-k "$${PYTEST_FILTER}" \
|
||||
-k "$${PYTEST_FILTER-$${FLTR}}" \
|
||||
test/ src/;
|
||||
|
||||
@rm -rf "src/__pycache__";
|
||||
|
|
@ -552,14 +558,16 @@ freeze:
|
|||
## Bump Version number in all files
|
||||
.PHONY: bump_version
|
||||
bump_version:
|
||||
$(DEV_ENV)/bin/pycalver bump;
|
||||
$(DEV_ENV)/bin/bumpver update;
|
||||
|
||||
|
||||
## Create python sdist and bdist_wheel files
|
||||
.PHONY: dist_build
|
||||
dist_build:
|
||||
@rm -rf build/lib3to6_out/
|
||||
@rm -rf build/bdist*
|
||||
$(DEV_ENV_PY) setup.py sdist;
|
||||
$(DEV_ENV_PY) setup.py bdist_wheel --python-tag=py2.py3;
|
||||
$(DEV_ENV_PY) setup.py bdist_wheel --python-tag=$(BDIST_WHEEL_PYTHON_TAG);
|
||||
@rm -rf src/*.egg-info
|
||||
|
||||
|
||||
|
|
@ -4,19 +4,22 @@
|
|||
AUTHOR_NAME="Manuel Barkhau"
|
||||
AUTHOR_EMAIL="mbarkhau@gmail.com"
|
||||
|
||||
KEYWORDS="version versioning bumpversion calver"
|
||||
DESCRIPTION="CalVer for python packages."
|
||||
KEYWORDS="version bumpver calver semver versioning bumpversion pep440"
|
||||
DESCRIPTION="Bump version numbers in project files."
|
||||
|
||||
LICENSE_ID="MIT"
|
||||
|
||||
PACKAGE_NAME="pycalver"
|
||||
PACKAGE_NAME="bumpver"
|
||||
GIT_REPO_NAMESPACE="mbarkhau"
|
||||
GIT_REPO_DOMAIN="gitlab.com"
|
||||
GIT_REPO_DOMAIN="github.com"
|
||||
|
||||
PACKAGE_VERSION="v202007.0036"
|
||||
PACKAGE_VERSION="2020.1041-beta"
|
||||
|
||||
DEFAULT_PYTHON_VERSION="python=3.8"
|
||||
SUPPORTED_PYTHON_VERSIONS="python=2.7 python=3.6 pypy2.7 pypy3.5 python=3.8"
|
||||
|
||||
DOCKER_REGISTRY_DOMAIN=registry.gitlab.com
|
||||
|
||||
DEFAULT_PYTHON_VERSION="python=3.6"
|
||||
SUPPORTED_PYTHON_VERSIONS="python=2.7 python=3.5 python=3.6 python=3.7 pypy2.7 pypy3.5"
|
||||
|
||||
IS_PUBLIC=1
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Stages:
|
||||
# root : Common image, both for the builder and for the final image.
|
||||
# This contains only minimal dependencies required in both cases
|
||||
# for miniconda and the makefile.
|
||||
# for miniconda and the Makefile.
|
||||
# env_builder: stage in which the conda envrionment is created
|
||||
# and dependencies are installed
|
||||
# base : the final image containing only the required environment files,
|
||||
|
|
@ -37,8 +37,8 @@ RUN if ! test -z "${ENV_SSH_PRIVATE_RSA_KEY}"; then \
|
|||
ADD requirements/ requirements/
|
||||
ADD scripts/ scripts/
|
||||
|
||||
ADD makefile.bootstrapit.make makefile.bootstrapit.make
|
||||
ADD makefile makefile
|
||||
ADD Makefile.bootstrapit.make Makefile.bootstrapit.make
|
||||
ADD Makefile Makefile
|
||||
|
||||
# install envs (relatively stable)
|
||||
ADD requirements/conda.txt requirements/conda.txt
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
Individual files contain the following tag instead of the full license text.
|
||||
|
||||
This file is part of the pycalver project
|
||||
https://gitlab.com/mbarkhau/pycalver
|
||||
https://github.com/mbarkhau/pycalver
|
||||
|
||||
Copyright (c) 2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
SPDX-License-Identifier: MIT
|
||||
|
||||
This enables machine processing of license information based on the SPDX
|
||||
|
|
|
|||
428
pycalver1k.svg
Normal file
428
pycalver1k.svg
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 33.866666 33.866668"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.0 (b51213c273, 2020-08-10)"
|
||||
sodipodi:docname="pycalver1k.svg"
|
||||
inkscape:export-filename="/home/mbarkhau/foss/pycalver/pycalver1k2_128.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient1163">
|
||||
<stop
|
||||
style="stop-color:#e89a00;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop1159" />
|
||||
<stop
|
||||
style="stop-color:#ffd42a;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop1161" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1163"
|
||||
id="linearGradient1165"
|
||||
x1="17.506153"
|
||||
y1="278.55835"
|
||||
x2="17.63979"
|
||||
y2="282.83472"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.94649427,0,0,1.0042072,0.90603086,-0.95465177)" />
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath1193">
|
||||
<g
|
||||
style="fill:#45b848;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="use1195"
|
||||
inkscape:label="Clip">
|
||||
<g
|
||||
style="fill:#45b848;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="g1278"
|
||||
transform="translate(0,1.1994176)">
|
||||
<circle
|
||||
cy="270.14941"
|
||||
cx="23.779428"
|
||||
id="circle1274"
|
||||
style="fill:#45b848;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
r="2.3812499" />
|
||||
<rect
|
||||
y="264.03714"
|
||||
x="22.339876"
|
||||
height="6.5171237"
|
||||
width="2.8791037"
|
||||
id="rect1276"
|
||||
style="fill:#45b848;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#45b848;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="g1284"
|
||||
transform="translate(0,1.5112466)">
|
||||
<circle
|
||||
style="fill:#45b848;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="circle1280"
|
||||
cx="10.087241"
|
||||
cy="269.573"
|
||||
r="2.3812499" />
|
||||
<rect
|
||||
style="fill:#45b848;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="rect1282"
|
||||
width="2.8791037"
|
||||
height="6.5171237"
|
||||
x="8.6476898"
|
||||
y="263.9899" />
|
||||
</g>
|
||||
</g>
|
||||
</clipPath>
|
||||
<mask
|
||||
maskUnits="userSpaceOnUse"
|
||||
id="mask1425">
|
||||
<g
|
||||
id="g1441"
|
||||
transform="translate(-1.5416735,-2.3386165)">
|
||||
<rect
|
||||
y="267.51743"
|
||||
x="-0.8018086"
|
||||
height="9.5214758"
|
||||
width="38.553631"
|
||||
id="rect1427"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#aa8800;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers" />
|
||||
<g
|
||||
transform="translate(1.541672,3.8452511)"
|
||||
id="g1433"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
|
||||
<circle
|
||||
r="2.3812499"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="circle1429"
|
||||
cx="23.779428"
|
||||
cy="270.14941" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="rect1431"
|
||||
width="2.8791037"
|
||||
height="6.5171237"
|
||||
x="22.339876"
|
||||
y="264.03714" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(1.541672,4.1570801)"
|
||||
id="g1439"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
|
||||
<circle
|
||||
r="2.3812499"
|
||||
cy="269.573"
|
||||
cx="10.087241"
|
||||
id="circle1435"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
<rect
|
||||
y="263.9899"
|
||||
x="8.6476898"
|
||||
height="6.5171237"
|
||||
width="2.8791037"
|
||||
id="rect1437"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1163"
|
||||
id="linearGradient1165-3"
|
||||
x1="17.605284"
|
||||
y1="269.99991"
|
||||
x2="17.63979"
|
||||
y2="282.83472"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0541065,0,0,0.87055183,-0.916204,-223.64659)" />
|
||||
<mask
|
||||
maskUnits="userSpaceOnUse"
|
||||
id="mask1129">
|
||||
<g
|
||||
id="g1145"
|
||||
transform="translate(-1.5416733,-2.3386129)">
|
||||
<rect
|
||||
y="267.51743"
|
||||
x="-0.8018086"
|
||||
height="9.5214758"
|
||||
width="38.553631"
|
||||
id="rect1131"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#aa8800;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers" />
|
||||
<g
|
||||
transform="translate(0.07525963,4.2889947)"
|
||||
id="g1137"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
|
||||
<ellipse
|
||||
cy="268.73132"
|
||||
cx="10.111843"
|
||||
id="ellipse1133"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
rx="2.2523122"
|
||||
ry="2.629046" />
|
||||
<rect
|
||||
y="263.9899"
|
||||
x="8.7405252"
|
||||
height="6.5171237"
|
||||
width="2.7426364"
|
||||
id="rect1135"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(16.669414,4.2889947)"
|
||||
id="g1143"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
|
||||
<ellipse
|
||||
cy="268.73132"
|
||||
cx="10.111843"
|
||||
id="ellipse1139"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
rx="2.2523122"
|
||||
ry="2.629046" />
|
||||
<rect
|
||||
y="263.9899"
|
||||
x="8.7405252"
|
||||
height="6.5171237"
|
||||
width="2.7426364"
|
||||
id="rect1141"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<mask
|
||||
maskUnits="userSpaceOnUse"
|
||||
id="mask1129-3">
|
||||
<g
|
||||
id="g1145-6"
|
||||
transform="translate(-1.5416733,-2.3386129)">
|
||||
<rect
|
||||
y="267.51743"
|
||||
x="-0.8018086"
|
||||
height="9.5214758"
|
||||
width="38.553631"
|
||||
id="rect1131-7"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#aa8800;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers" />
|
||||
<g
|
||||
transform="translate(0.07525963,4.2889947)"
|
||||
id="g1137-5"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
|
||||
<ellipse
|
||||
cy="268.73132"
|
||||
cx="10.111843"
|
||||
id="ellipse1133-3"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
rx="2.2523122"
|
||||
ry="2.629046" />
|
||||
<rect
|
||||
y="263.9899"
|
||||
x="8.7405252"
|
||||
height="6.5171237"
|
||||
width="2.7426364"
|
||||
id="rect1135-5"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(16.669414,4.2889947)"
|
||||
id="g1143-6"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
|
||||
<ellipse
|
||||
cy="268.73132"
|
||||
cx="10.111843"
|
||||
id="ellipse1139-2"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
rx="2.2523122"
|
||||
ry="2.629046" />
|
||||
<rect
|
||||
y="263.9899"
|
||||
x="8.7405252"
|
||||
height="6.5171237"
|
||||
width="2.7426364"
|
||||
id="rect1141-9"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="61.591322"
|
||||
inkscape:cy="64.987937"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer8"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="2512"
|
||||
inkscape:window-height="1376"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:document-rotation="0" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="debug_bg"
|
||||
style="display:none"
|
||||
sodipodi:insensitive="true">
|
||||
<rect
|
||||
style="fill:#ff00ff;fill-opacity:1;stroke:none;stroke-width:1.92171;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="rect954"
|
||||
width="33.848568"
|
||||
height="33.868378"
|
||||
x="0.044362877"
|
||||
y="0.0074945446" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer6"
|
||||
inkscape:label="bg"
|
||||
style="display:inline">
|
||||
<path
|
||||
id="rect880"
|
||||
style="display:inline;fill:#ffffff;stroke-width:7.16557"
|
||||
d="m 1.585409,3.7099638 30.618783,-0.052171 0.725957,0.7967657 -0.0046,27.3188725 -0.528162,0.557572 L 1.1974464,32.32312 0.94904837,32.088188 0.93651797,4.3514519 Z"
|
||||
sodipodi:nodetypes="ccccccccc" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="gradient"
|
||||
style="display:inline">
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:url(#linearGradient1165-3);fill-opacity:1;stroke:#000000;stroke-width:1.21634;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect815"
|
||||
width="29.469652"
|
||||
height="13.742671"
|
||||
x="2.1985073"
|
||||
y="17.262745" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="text bottom"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-263.13332)"
|
||||
style="display:inline">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:14.8167px;line-height:0.85;font-family:Monoid;-inkscape-font-specification:Monoid;letter-spacing:-0.111125px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
|
||||
x="2.2388713"
|
||||
y="292.59897"
|
||||
id="text859"><tspan
|
||||
sodipodi:role="line"
|
||||
x="2.2388713"
|
||||
y="292.59897"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:14.8167px;font-family:'Iosevka Term SS05';-inkscape-font-specification:'Iosevka Term SS05 Bold';stroke-width:0.264583"
|
||||
id="tspan953">2020</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer9"
|
||||
inkscape:label="frame top"
|
||||
style="display:inline">
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect815-0"
|
||||
width="30.546177"
|
||||
height="13.588312"
|
||||
x="1.6602445"
|
||||
y="-15.922193"
|
||||
transform="scale(1,-1)" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer8"
|
||||
inkscape:label="text top"
|
||||
style="display:inline">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:16.9333px;line-height:1.25;font-family:sans-serif;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.265;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
x="4.7286582"
|
||||
y="13.507061"
|
||||
id="text921"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan919"
|
||||
x="4.7286582"
|
||||
y="13.507061"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16.9333px;font-family:'Iosevka Fixed SS05';-inkscape-font-specification:'Iosevka Fixed SS05 Medium';fill:#ffffff;stroke-width:0.265;stroke-miterlimit:4;stroke-dasharray:none">ver</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="frame"
|
||||
style="display:inline">
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#aa8800;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect1398"
|
||||
width="30.684868"
|
||||
height="0.76833469"
|
||||
x="1.5910856"
|
||||
y="16.622082" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer7"
|
||||
inkscape:label="top frame"
|
||||
sodipodi:insensitive="true"
|
||||
style="display:none">
|
||||
<rect
|
||||
style="display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2.32913;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="rect900"
|
||||
width="26.775116"
|
||||
height="6.1376147"
|
||||
x="3.5457754"
|
||||
y="268.57437"
|
||||
clip-path="none"
|
||||
mask="url(#mask1129-3)"
|
||||
transform="matrix(1.0545213,0,0,0.89332824,-0.92322741,-236.38373)" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer5"
|
||||
inkscape:label="pegs"
|
||||
style="display:none"
|
||||
sodipodi:insensitive="true">
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#aa8800;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect1400"
|
||||
width="1.5874993"
|
||||
height="5.499999"
|
||||
x="7.3927369"
|
||||
y="1.7978847" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#aa8800;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect1400-6"
|
||||
width="1.5874993"
|
||||
height="5.499999"
|
||||
x="24.886431"
|
||||
y="1.7978847" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
BIN
pycalver1k2_128.png
Normal file
BIN
pycalver1k2_128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
BIN
pycalver1k_128.png
Normal file
BIN
pycalver1k_128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
200
pylint-ignore.md
200
pylint-ignore.md
|
|
@ -1,204 +1,22 @@
|
|||
# `pylint-ignore`
|
||||
# Pylint-Ignore
|
||||
|
||||
**WARNING: This file is programatically generated.**
|
||||
|
||||
This file is parsed by `pylint-ignore` to determine which `pylint`
|
||||
messages should be ignored.
|
||||
This file is parsed by [`pylint-ignore`](https://pypi.org/project/pylint-ignore/)
|
||||
to determine which
|
||||
[Pylint messages](https://pylint.pycqa.org/en/stable/technical_reference/features.html)
|
||||
should be ignored.
|
||||
|
||||
- Do not edit this file manually.
|
||||
- To update, use `pylint-ignore --update-ignorefile`
|
||||
|
||||
The recommended approach to using `pylint-ignore` is:
|
||||
|
||||
- If a message refers to a valid issue, update your code rather than
|
||||
1. If a message refers to a valid issue, update your code rather than
|
||||
ignoring the message.
|
||||
- If a message should *always* be ignored (globally), then to do so
|
||||
2. If a message should *always* be ignored (globally), then to do so
|
||||
via the usual `pylintrc` or `setup.cfg` files rather than this
|
||||
`pylint-ignore.md` file.
|
||||
- If a message is a false positive, add a comment of this form to your code:
|
||||
`# pylint:disable=<symbol> ; explanation why this is a false positive`
|
||||
|
||||
|
||||
## File test/test_version.py - Line 145 - E1101 (no-member)
|
||||
|
||||
- `message: Class 'VersionInfo' has no '_fields' member`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-07-19T19:06:44`
|
||||
|
||||
```
|
||||
134: def test_part_field_mapping():
|
||||
...
|
||||
143:
|
||||
144: a_fields = set(version.PATTERN_PART_FIELDS.values())
|
||||
> 145: b_fields = set(version.VersionInfo._fields)
|
||||
146:
|
||||
147: assert a_fields == b_fields
|
||||
```
|
||||
|
||||
|
||||
## File src/pycalver/rewrite.py - Line 168 - E1101 (no-member)
|
||||
|
||||
- `message: Instance of 'RewrittenFileData' has no '_replace' member`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-07-19T18:50:33`
|
||||
|
||||
```
|
||||
138: def iter_rewritten(
|
||||
...
|
||||
166:
|
||||
167: rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
||||
> 168: yield rfd._replace(path=str(file_path))
|
||||
169:
|
||||
170:
|
||||
```
|
||||
|
||||
|
||||
## File src/pycalver/rewrite.py - Line 217 - E1101 (no-member)
|
||||
|
||||
- `message: Instance of 'RewrittenFileData' has no '_replace' member`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-07-19T18:50:33`
|
||||
|
||||
```
|
||||
189: def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) -> str:
|
||||
...
|
||||
215: raise NoPatternMatch(errmsg)
|
||||
216:
|
||||
> 217: rfd = rfd._replace(path=str(file_path))
|
||||
218: lines = diff_lines(rfd)
|
||||
219: if len(lines) == 0:
|
||||
```
|
||||
|
||||
|
||||
## File src/pycalver/version.py - Line 235 - E1101 (no-member)
|
||||
|
||||
- `message: Class 'CalendarInfo' has no '_fields' member`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-07-19T18:50:33`
|
||||
|
||||
```
|
||||
221: def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool:
|
||||
...
|
||||
233: False
|
||||
234: """
|
||||
> 235: for field in CalendarInfo._fields:
|
||||
236: maybe_val: typ.Any = getattr(nfo, field, None)
|
||||
237: if isinstance(maybe_val, int):
|
||||
```
|
||||
|
||||
|
||||
## File src/pycalver/version.py - Line 481 - E1101 (no-member)
|
||||
|
||||
- `message: Instance of 'CalendarInfo' has no '_asdict' member`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-07-19T18:50:33`
|
||||
|
||||
```
|
||||
454: def incr(
|
||||
...
|
||||
479:
|
||||
480: if old_date <= cur_date:
|
||||
> 481: cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict())
|
||||
482: else:
|
||||
483: logger.warning(f"Version appears to be from the future '{old_version}'")
|
||||
```
|
||||
|
||||
|
||||
## File src/pycalver/version.py - Line 481 - E1101 (no-member)
|
||||
|
||||
- `message: Instance of 'VersionInfo' has no '_replace' member`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-07-19T18:50:33`
|
||||
|
||||
```
|
||||
454: def incr(
|
||||
...
|
||||
479:
|
||||
480: if old_date <= cur_date:
|
||||
> 481: cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict())
|
||||
482: else:
|
||||
483: logger.warning(f"Version appears to be from the future '{old_version}'")
|
||||
```
|
||||
|
||||
|
||||
## File test/util.py - Line 10 - R0903 (too-few-public-methods)
|
||||
|
||||
- `message: Too few public methods (1/2)`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-07-19T19:06:44`
|
||||
|
||||
```
|
||||
8:
|
||||
9:
|
||||
> 10: class Shell:
|
||||
11: def __init__(self, cwd):
|
||||
12: self.cwd = cwd
|
||||
```
|
||||
|
||||
|
||||
## File src/pycalver/vcs.py - Line 75 - W0511 (fixme)
|
||||
|
||||
- `message: TODO (mb 2018-11-15): Detect encoding of output?`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-07-19T18:50:33`
|
||||
|
||||
```
|
||||
65: def __call__(self, cmd_name: str, env: Env = None, **kwargs: str) -> str:
|
||||
...
|
||||
73: output_data: bytes = sp.check_output(cmd_str.split(), env=env, stderr=sp.STDOUT)
|
||||
74:
|
||||
> 75: # TODO (mb 2018-11-15): Detect encoding of output?
|
||||
76: _encoding = "utf-8"
|
||||
77: return output_data.decode(_encoding)
|
||||
```
|
||||
|
||||
|
||||
## File src/pycalver/cli.py - Line 78 - W0603 (global-statement)
|
||||
|
||||
- `message: Using the global statement`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-07-19T18:50:33`
|
||||
|
||||
```
|
||||
76: def cli(verbose: int = 0) -> None:
|
||||
77: """Automatically update PyCalVer version strings on python projects."""
|
||||
> 78: global _VERBOSE
|
||||
79: _VERBOSE = verbose
|
||||
80:
|
||||
```
|
||||
|
||||
|
||||
## File src/pycalver/vcs.py - Line 104 - W0703 (broad-except)
|
||||
|
||||
- `message: Catching too general exception Exception`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-07-19T18:50:33`
|
||||
|
||||
```
|
||||
98: def has_remote(self) -> bool:
|
||||
...
|
||||
102: return False
|
||||
103: return True
|
||||
> 104: except Exception:
|
||||
105: return False
|
||||
106:
|
||||
```
|
||||
|
||||
|
||||
## File src/pycalver/cli.py - Line 292 - W0703 (broad-except)
|
||||
|
||||
- `message: Catching too general exception Exception`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-07-19T18:50:33`
|
||||
|
||||
```
|
||||
289: def _try_print_diff(cfg: config.Config, new_version: str) -> None:
|
||||
...
|
||||
290: try:
|
||||
291: _print_diff(cfg, new_version)
|
||||
> 292: except Exception as ex:
|
||||
293: logger.error(str(ex))
|
||||
294: sys.exit(1)
|
||||
```
|
||||
|
||||
3. If a message is a false positive, add a comment of this form to your code:
|
||||
`# pylint:disable=<symbol> ; explain why this is a false positive`
|
||||
|
||||
|
|
|
|||
|
|
@ -46,3 +46,6 @@ graphviz
|
|||
|
||||
# run failed tests first
|
||||
pytest-cache
|
||||
|
||||
# to update the readme examples
|
||||
rich
|
||||
|
|
|
|||
|
|
@ -18,17 +18,17 @@ flake8-docstrings
|
|||
flake8-builtins
|
||||
flake8-comprehensions
|
||||
flake8-junit-report
|
||||
flake8-2020
|
||||
pylint-ignore>=2020.1013
|
||||
mypy
|
||||
# pylint doesn't support isort>=5 for now
|
||||
# https://github.com/PyCQA/pylint/issues/3722
|
||||
isort<5
|
||||
mypy>=0.790
|
||||
isort
|
||||
|
||||
# http://doc.pytest.org/en/latest/py27-py34-deprecation.html
|
||||
# The pytest 4.6 series will be the last to support Python 2.7
|
||||
# and 3.4, and is scheduled to be released by mid-2019.
|
||||
# pytest 5.0 and onwards will support only Python 3.5+.
|
||||
pytest<5.0
|
||||
pytest; python_version >= "3.5"
|
||||
pytest<5.0; python_version < "3.5"
|
||||
pytest-cov
|
||||
# https://github.com/pytest-dev/pytest-html/blob/master/CHANGES.rst
|
||||
# pytest-html 2.0+ doesn't support python2.7
|
||||
|
|
|
|||
|
|
@ -11,4 +11,9 @@ pathlib2
|
|||
typing; python_version < "3.5"
|
||||
click
|
||||
toml
|
||||
six
|
||||
lexid
|
||||
colorama>=0.4
|
||||
|
||||
# needed pkg_resources.parse_version
|
||||
setuptools<46.0.0; python_version < "3.5"
|
||||
setuptools>=46.0.0; python_version >= "3.5"
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
import sys
|
||||
|
||||
print(sys.version)
|
||||
|
|
@ -75,6 +75,20 @@ if [[ -f "makefile.extra.make" && -f "makefile.config.make" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# One time update of makefile capitalization
|
||||
if [[ -f "makefile" && -f "makefile.bootstrapit.make" ]]; then
|
||||
printf "Change capitalization of makefile -> Makefile # because too many rustled jimmies\n\n"
|
||||
printf " mv makefile Makefile\n"
|
||||
printf " mv makefile.bootstrapit.make Makefile.bootstrapit.make\n"
|
||||
sed -i 's/include makefile.bootstrapit.make/include Makefile.bootstrapit.make/g' makefile
|
||||
git add makefile
|
||||
git mv makefile Makefile;
|
||||
git mv makefile.bootstrapit.make Makefile.bootstrapit.make;
|
||||
|
||||
printf "Please commit the renamed files and run bootstrapit_update.sh again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Argument parsing from
|
||||
# https://stackoverflow.com/a/14203146/62997
|
||||
UPDATE_ALL=0
|
||||
|
|
@ -346,9 +360,7 @@ elif [[ -z "${IGNORE_IF_EXISTS[*]}" ]]; then
|
|||
"CHANGELOG.md"
|
||||
"README.md"
|
||||
"setup.py"
|
||||
"makefile"
|
||||
"requirements/pypi.txt"
|
||||
"requirements/development.txt"
|
||||
"requirements/conda.txt"
|
||||
"requirements/vendor.txt"
|
||||
"src/${MODULE_NAME}/__init__.py"
|
||||
|
|
@ -398,8 +410,8 @@ copy_template MANIFEST.in;
|
|||
copy_template setup.py;
|
||||
copy_template setup.cfg;
|
||||
|
||||
copy_template makefile;
|
||||
copy_template makefile.bootstrapit.make;
|
||||
copy_template Makefile;
|
||||
copy_template Makefile.bootstrapit.make;
|
||||
copy_template activate;
|
||||
copy_template docker_base.Dockerfile;
|
||||
copy_template Dockerfile;
|
||||
|
|
|
|||
199
scripts/update_readme_examples.py
Normal file
199
scripts/update_readme_examples.py
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import io
|
||||
import sys
|
||||
import shlex
|
||||
import random
|
||||
import difflib
|
||||
import datetime as dt
|
||||
import subprocess as sp
|
||||
import pkg_resources
|
||||
|
||||
import rich
|
||||
import rich.box
|
||||
import rich.table
|
||||
|
||||
from bumpcalver import v2version
|
||||
|
||||
|
||||
def update(content, marker, value):
|
||||
begin_marker = f"<!-- BEGIN {marker} -->"
|
||||
end_marker = f"<!-- END {marker} -->"
|
||||
|
||||
prefix, rest = content.split(begin_marker)
|
||||
_ , suffix = rest.split(end_marker)
|
||||
return prefix + begin_marker + value + end_marker + suffix
|
||||
|
||||
|
||||
def _color_line(line):
|
||||
if line.startswith("+++") or line.startswith("---"):
|
||||
return line
|
||||
elif line.startswith("+"):
|
||||
return "\u001b[32m" + line + "\u001b[0m"
|
||||
elif line.startswith("-"):
|
||||
return "\u001b[31m" + line + "\u001b[0m"
|
||||
elif line.startswith("@"):
|
||||
return "\u001b[36m" + line + "\u001b[0m"
|
||||
else:
|
||||
return line
|
||||
|
||||
|
||||
def print_diff(old_content, new_content):
|
||||
diff_lines = difflib.unified_diff(
|
||||
a=old_content.splitlines(),
|
||||
b=new_content.splitlines(),
|
||||
lineterm="",
|
||||
)
|
||||
|
||||
for line in diff_lines:
|
||||
print(_color_line(line))
|
||||
|
||||
|
||||
def update_md_code_output(content, command):
|
||||
output_data = sp.check_output(shlex.split(command))
|
||||
output = output_data.decode("utf-8")
|
||||
|
||||
replacement = "\n\n```\n" + "$ " + command + "\n" + output + "```\n\n"
|
||||
return update(content, command, replacement)
|
||||
|
||||
|
||||
def weeknum_example():
|
||||
base_date = dt.date(2020, 12, 26)
|
||||
|
||||
rows = []
|
||||
for i in range(10):
|
||||
d = base_date + dt.timedelta(days=i)
|
||||
row = d.strftime("%Y-%m-%d (%a): %Y %W %U %G %V")
|
||||
rows.append(row)
|
||||
|
||||
content = "\n".join([" YYYY WW UU GGGG VV"] + rows)
|
||||
return "\n\n```\n" + content + "\n```\n\n"
|
||||
|
||||
|
||||
def pattern_examples():
|
||||
patterns = [
|
||||
("MAJOR.MINOR.PATCH[PYTAGNUM]" , ""),
|
||||
("MAJOR.MINOR[.PATCH[PYTAGNUM]]", ""),
|
||||
("YYYY.BUILD[PYTAGNUM]" , ""),
|
||||
("YYYY.BUILD[-TAG]" , ""),
|
||||
("YYYY.INC0[PYTAGNUM]" , ""),
|
||||
("YYYY0M.PATCH[-TAG]" , "¹"),
|
||||
("YYYY0M.BUILD[-TAG]" , ""),
|
||||
("YYYY.0M" , ""),
|
||||
("YYYY.MM" , ""),
|
||||
("YYYY.WW" , ""),
|
||||
("YYYY.MM.PATCH[PYTAGNUM]" , ""),
|
||||
("YYYY.0M.PATCH[PYTAGNUM]" , "¹"),
|
||||
("YYYY.MM.INC0" , ""),
|
||||
("YYYY.MM.DD" , ""),
|
||||
("YYYY.0M.0D" , ""),
|
||||
("YY.0M.PATCH" , "²"),
|
||||
]
|
||||
|
||||
rand = random.Random(0)
|
||||
field_values = [
|
||||
{
|
||||
'year_y': rand.randrange(2020, 2023),
|
||||
'month' : rand.randrange( 1, 12),
|
||||
'dom' : rand.randrange( 1, 28),
|
||||
'major' : rand.randrange( 0, 1),
|
||||
'minor' : rand.randrange( 0, 20),
|
||||
'patch' : rand.randrange( 0, 20),
|
||||
'inc0' : rand.randrange( 0, 20),
|
||||
'bid' : rand.randrange(1000, 1500),
|
||||
'tag' : rand.choice(["final", "beta"]),
|
||||
}
|
||||
for _ in range(100)
|
||||
]
|
||||
|
||||
rows = []
|
||||
for raw_pattern, lexico_caveat in patterns:
|
||||
sort_keys = ['year_y']
|
||||
if "0M" in raw_pattern or "MM" in raw_pattern:
|
||||
sort_keys.append('month')
|
||||
if "0D" in raw_pattern or "DD" in raw_pattern:
|
||||
sort_keys.append('dom')
|
||||
if "PATCH" in raw_pattern:
|
||||
sort_keys.append('patch')
|
||||
if "INC0" in raw_pattern:
|
||||
sort_keys.append('inc0')
|
||||
if "BUILD" in raw_pattern:
|
||||
sort_keys.append('bid')
|
||||
if "PYTAG" in raw_pattern:
|
||||
sort_keys.append('tag')
|
||||
|
||||
field_values.sort(key=lambda fv: tuple(fv[k] for k in sort_keys))
|
||||
field_values[-1]['year_y'] = 2101
|
||||
|
||||
example_versions = []
|
||||
notag_versions = []
|
||||
pep440_versions = []
|
||||
|
||||
for fvals in field_values:
|
||||
vinfo = v2version.parse_field_values_to_vinfo(fvals)
|
||||
example_version = v2version.format_version(vinfo, raw_pattern)
|
||||
example_versions.append(example_version)
|
||||
|
||||
pep440_version = str(pkg_resources.parse_version(example_version))
|
||||
pep440_versions.append(pep440_version)
|
||||
|
||||
notag_fvals = fvals.copy()
|
||||
notag_fvals['tag'] = 'final'
|
||||
|
||||
notag_vinfo = v2version.parse_field_values_to_vinfo(notag_fvals)
|
||||
notag_version = v2version.format_version(notag_vinfo, raw_pattern)
|
||||
notag_versions.append(notag_version)
|
||||
|
||||
sample = rand.sample(sorted(example_versions, key=len, reverse=True)[:-1], 2)
|
||||
sample.sort(key=pkg_resources.parse_version)
|
||||
|
||||
is_pep440 = pep440_versions == example_versions
|
||||
is_lexico = sorted(notag_versions) == notag_versions
|
||||
|
||||
pattern_col = f"`{raw_pattern}`"
|
||||
pep440_col = "yes" if is_pep440 else "no"
|
||||
lexico_col = ("yes" if is_lexico else "no") + lexico_caveat
|
||||
sample_str = " ".join([v.ljust(16) for v in sample]).strip()
|
||||
examples_col = "`" + sample_str + "`"
|
||||
|
||||
# row = (pattern_col, examples_col, pep440_col)
|
||||
# sort_key = (is_pep440 , -len(raw_pattern))
|
||||
|
||||
row = (pattern_col, examples_col, pep440_col, lexico_col)
|
||||
sort_key = (is_pep440 , is_lexico , -len(raw_pattern))
|
||||
|
||||
rows.append((sort_key, row))
|
||||
|
||||
# rows.sort(reverse=True)
|
||||
|
||||
patterns_table = rich.table.Table(show_header=True, box=rich.box.ASCII)
|
||||
patterns_table.add_column("pattern")
|
||||
patterns_table.add_column("examples")
|
||||
patterns_table.add_column("PEP440")
|
||||
patterns_table.add_column("lexico.")
|
||||
|
||||
for _, row in rows:
|
||||
patterns_table.add_row(*row)
|
||||
|
||||
buf = io.StringIO()
|
||||
rich.print(patterns_table, file=buf)
|
||||
table_str = buf.getvalue()
|
||||
table_str = "\n".join(table_str.splitlines()[1:-1])
|
||||
table_str = table_str.replace("-+-", "-|-")
|
||||
return "\n\n" + table_str + "\n\n"
|
||||
|
||||
|
||||
old_content = io.open("README.md").read()
|
||||
|
||||
new_content = old_content
|
||||
new_content = update_md_code_output(new_content, "bumpver --help")
|
||||
new_content = update_md_code_output(new_content, "bumpver update --help")
|
||||
new_content = update(new_content, "pattern_examples", pattern_examples())
|
||||
new_content = update(new_content, "weeknum_example" , weeknum_example())
|
||||
|
||||
|
||||
if old_content == new_content:
|
||||
print("Nothing changed")
|
||||
elif "--dry" in sys.argv:
|
||||
print_diff(old_content, new_content)
|
||||
else:
|
||||
with io.open("README.md", mode="w") as fobj:
|
||||
fobj.write(new_content)
|
||||
59
setup.cfg
59
setup.cfg
|
|
@ -19,14 +19,15 @@ warn_redundant_casts = True
|
|||
|
||||
|
||||
[tool:isort]
|
||||
known_third_party = click,pathlib2
|
||||
known_first_party = bumpver
|
||||
known_third_party = click,pathlib2,lexid,pkg_resources
|
||||
force_single_line = True
|
||||
length_sort = True
|
||||
|
||||
|
||||
[flake8]
|
||||
max-line-length = 100
|
||||
max-complexity = 10
|
||||
max-complexity = 12
|
||||
ignore =
|
||||
# Missing trailing comma (handled by sjfmt)
|
||||
C812
|
||||
|
|
@ -74,7 +75,7 @@ ignore =
|
|||
D400
|
||||
# First line should be in imperative mood
|
||||
D401
|
||||
select = A,AAA,D,C,E,F,W,H,B,D212,D404,D405,D406,B901,B950
|
||||
select = A,AAA,D,C,E,F,W,H,B,D212,D404,D405,D406,B901,B950,YTT
|
||||
exclude =
|
||||
.git
|
||||
__pycache__
|
||||
|
|
@ -82,37 +83,40 @@ exclude =
|
|||
dist/
|
||||
.mypy_cache
|
||||
|
||||
# Hopefully this can be resolved, so D404, D405 start working
|
||||
# https://github.com/PyCQA/pydocstyle/pull/188
|
||||
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --doctest-modules
|
||||
|
||||
|
||||
[pycalver]
|
||||
current_version = v202007.1036
|
||||
version_pattern = "{pycalver}"
|
||||
[bumpver]
|
||||
current_version = "2020.1099-beta"
|
||||
version_pattern = "YYYY.BUILD[-TAG]"
|
||||
commit_message = "bump {old_version} -> {new_version}"
|
||||
commit = True
|
||||
tag = True
|
||||
push = True
|
||||
|
||||
[pycalver:file_patterns]
|
||||
[bumpver:file_patterns]
|
||||
bootstrapit.sh =
|
||||
PACKAGE_VERSION="{pycalver}"
|
||||
PACKAGE_VERSION="{version}"
|
||||
setup.cfg =
|
||||
current_version = {pycalver}
|
||||
current_version = "{version}"
|
||||
setup.py =
|
||||
version="{pep440_pycalver}"
|
||||
src/pycalver/__init__.py =
|
||||
__version__ = "{pycalver}"
|
||||
src/pycalver/cli.py =
|
||||
click.version_option(version="{pycalver}")
|
||||
version="{pep440_version}",
|
||||
src/bumpver/__init__.py =
|
||||
__version__ = "{version}"
|
||||
src/bumpver/cli.py =
|
||||
@click.version_option(version="{version}")
|
||||
src/bumpver/*.py =
|
||||
Copyright (c) 2018-YYYY
|
||||
LICENSE =
|
||||
Copyright (c) 2018-YYYY
|
||||
license.header =
|
||||
Copyright (c) 2018-YYYY
|
||||
README.md =
|
||||
[PyCalVer {version}]
|
||||
img.shields.io/static/v1.svg?label=PyCalVer&message={version}&color=blue
|
||||
Successfully installed pycalver-{pep440_version}
|
||||
pycalver, version {version}
|
||||
\[CalVer {version}\]
|
||||
img.shields.io/static/v1.svg?label=CalVer&message={version}&color=blue
|
||||
Successfully installed bumpver-{pep440_version}
|
||||
|
||||
|
||||
[tool:pylint]
|
||||
|
|
@ -130,7 +134,10 @@ output-format = colorized
|
|||
max-locals = 20
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args = 8
|
||||
max-args = 12
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches = 14
|
||||
|
||||
good-names = logger,i,ex
|
||||
|
||||
|
|
@ -146,6 +153,8 @@ ignore-comments=yes
|
|||
ignore-docstrings=yes
|
||||
ignore-imports=yes
|
||||
|
||||
ignored-argument-names=args|kwargs
|
||||
|
||||
# https://pylint.pycqa.org/en/stable/technical_reference/features.html
|
||||
disable =
|
||||
bad-continuation,
|
||||
|
|
@ -161,3 +170,9 @@ disable =
|
|||
missing-class-docstring,
|
||||
missing-function-docstring,
|
||||
raise-missing-from,
|
||||
duplicate-code,
|
||||
ungrouped-imports,
|
||||
|
||||
generated-members =
|
||||
# members of typing.NamedTuple
|
||||
"(_replace|_asdict|_fields)",
|
||||
|
|
|
|||
73
setup.py
73
setup.py
|
|
@ -1,7 +1,7 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://gitlab.com/mbarkhau/pycalver
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
|
|
@ -26,46 +26,9 @@ install_requires = [
|
|||
]
|
||||
|
||||
|
||||
package_dir = {"": "src"}
|
||||
|
||||
|
||||
if any(arg.startswith("bdist") for arg in sys.argv):
|
||||
try:
|
||||
import lib3to6
|
||||
package_dir = lib3to6.fix(package_dir)
|
||||
except ImportError:
|
||||
if sys.version_info < (3, 6):
|
||||
raise
|
||||
else:
|
||||
sys.stderr.write((
|
||||
"WARNING: Creating non-universal bdist of pycalver, "
|
||||
"this should only be used for development.\n"
|
||||
))
|
||||
|
||||
|
||||
long_description = "\n\n".join((read("README.md"), read("CHANGELOG.md")))
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
name="pycalver",
|
||||
license="MIT",
|
||||
author="Manuel Barkhau",
|
||||
author_email="mbarkhau@gmail.com",
|
||||
url="https://gitlab.com/mbarkhau/pycalver",
|
||||
version="202007.36",
|
||||
keywords="version versioning bumpversion calver",
|
||||
description="CalVer for python libraries.",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
packages=['pycalver'],
|
||||
package_dir=package_dir,
|
||||
install_requires=install_requires,
|
||||
entry_points="""
|
||||
[console_scripts]
|
||||
pycalver=pycalver.cli:cli
|
||||
""",
|
||||
zip_safe=True,
|
||||
|
||||
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
|
|
@ -84,5 +47,35 @@ setuptools.setup(
|
|||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
],
|
||||
]
|
||||
|
||||
package_dir = {"": "src"}
|
||||
|
||||
|
||||
if any(arg.startswith("bdist") for arg in sys.argv):
|
||||
import lib3to6
|
||||
package_dir = lib3to6.fix(package_dir)
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
name="bumpver",
|
||||
license="MIT",
|
||||
author="Manuel Barkhau",
|
||||
author_email="mbarkhau@gmail.com",
|
||||
url="https://github.com/mbarkhau/bumpver",
|
||||
version="2020.1041b0",
|
||||
keywords="version bumpver calver semver versioning bumpversion pep440",
|
||||
description="Bump version numbers in project files.",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
packages=setuptools.find_packages("src/"),
|
||||
package_dir=package_dir,
|
||||
install_requires=install_requires,
|
||||
entry_points="""
|
||||
[console_scripts]
|
||||
bumpver=bumpver.cli:cli
|
||||
""",
|
||||
python_requires=">=2.7",
|
||||
zip_safe=True,
|
||||
classifiers=classifiers,
|
||||
)
|
||||
|
|
|
|||
8
src/bumpver/__init__.py
Normal file
8
src/bumpver/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""BumpVer: A CLI program for versioning."""
|
||||
|
||||
__version__ = "2020.1041-beta"
|
||||
15
src/bumpver/__main__.py
Normal file
15
src/bumpver/__main__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env python
|
||||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
__main__ module for BumpVer.
|
||||
|
||||
Enables use as module: $ python -m bumpver --version
|
||||
"""
|
||||
from . import cli
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli.cli()
|
||||
723
src/bumpver/cli.py
Executable file
723
src/bumpver/cli.py
Executable file
|
|
@ -0,0 +1,723 @@
|
|||
#!/usr/bin/env python
|
||||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""cli module for BumpVer."""
|
||||
import io
|
||||
import sys
|
||||
import typing as typ
|
||||
import logging
|
||||
import datetime as dt
|
||||
import subprocess as sp
|
||||
|
||||
import click
|
||||
import colorama
|
||||
import pkg_resources
|
||||
|
||||
from . import vcs
|
||||
from . import config
|
||||
from . import rewrite
|
||||
from . import version
|
||||
from . import patterns
|
||||
from . import regexfmt
|
||||
from . import v1rewrite
|
||||
from . import v1version
|
||||
from . import v2rewrite
|
||||
from . import v2version
|
||||
from . import v1patterns
|
||||
from . import v2patterns
|
||||
|
||||
try:
|
||||
import pretty_traceback
|
||||
|
||||
pretty_traceback.install()
|
||||
except ImportError:
|
||||
pass # no need to fail because of missing dev dependency
|
||||
|
||||
|
||||
click.disable_unicode_literals_warning = True
|
||||
|
||||
logger = logging.getLogger("bumpver.cli")
|
||||
|
||||
|
||||
_VERBOSE = 0
|
||||
|
||||
|
||||
def _configure_logging(verbose: int = 0) -> None:
|
||||
# pylint:disable=global-statement; global flag is global.
|
||||
global _VERBOSE
|
||||
_VERBOSE = verbose
|
||||
|
||||
if verbose >= 2:
|
||||
log_format = "%(asctime)s.%(msecs)03d %(levelname)-7s %(name)-17s - %(message)s"
|
||||
log_level = logging.DEBUG
|
||||
elif verbose == 1:
|
||||
log_format = "%(levelname)-7s - %(message)s"
|
||||
log_level = logging.INFO
|
||||
else:
|
||||
log_format = "%(levelname)-7s - %(message)s"
|
||||
log_level = logging.INFO
|
||||
|
||||
logging.basicConfig(level=log_level, format=log_format, datefmt="%Y-%m-%dT%H:%M:%S")
|
||||
logger.debug("Logging configured.")
|
||||
|
||||
|
||||
VALID_RELEASE_TAG_VALUES = ("alpha", "beta", "rc", "post", "final")
|
||||
|
||||
|
||||
_current_date = dt.date.today().isoformat()
|
||||
|
||||
|
||||
def _validate_date(date: typ.Optional[str], pin_date: bool) -> typ.Optional[dt.date]:
|
||||
if date and pin_date:
|
||||
logger.error(f"Can only use either --pin-date or --date='{date}', not both.")
|
||||
sys.exit(1)
|
||||
|
||||
if date is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
dt_val = dt.datetime.strptime(date, "%Y-%m-%d")
|
||||
return dt_val.date()
|
||||
except ValueError:
|
||||
logger.error(
|
||||
f"Invalid parameter --date='{date}', must match format YYYY-0M-0D.", exc_info=True
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _validate_release_tag(tag: typ.Optional[str]) -> None:
|
||||
if tag is None:
|
||||
return
|
||||
|
||||
if tag in VALID_RELEASE_TAG_VALUES:
|
||||
return
|
||||
|
||||
logger.error(f"Invalid argument --tag={tag}")
|
||||
logger.error(f"Valid arguments are: {', '.join(VALID_RELEASE_TAG_VALUES)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _validate_flags(
|
||||
raw_pattern: str,
|
||||
major : bool,
|
||||
minor : bool,
|
||||
patch : bool,
|
||||
) -> None:
|
||||
if "{" in raw_pattern and "}" in raw_pattern:
|
||||
# only validate for new style patterns
|
||||
return
|
||||
|
||||
valid = True
|
||||
if major and "MAJOR" not in raw_pattern:
|
||||
logger.error(f"Flag --major is not applicable to pattern '{raw_pattern}'")
|
||||
valid = False
|
||||
if minor and "MINOR" not in raw_pattern:
|
||||
logger.error(f"Flag --minor is not applicable to pattern '{raw_pattern}'")
|
||||
valid = False
|
||||
if patch and "PATCH" not in raw_pattern:
|
||||
logger.error(f"Flag --patch is not applicable to pattern '{raw_pattern}'")
|
||||
valid = False
|
||||
|
||||
if not valid:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _log_no_change(subcmd: str, version_pattern: str, old_version: str) -> None:
|
||||
msg = f"Invalid version '{old_version}' and/or pattern '{version_pattern}'."
|
||||
logger.error(msg)
|
||||
|
||||
is_semver = "{semver}" in version_pattern or (
|
||||
"MAJOR" in version_pattern and "MAJOR" in version_pattern and "PATCH" in version_pattern
|
||||
)
|
||||
if is_semver:
|
||||
logger.warning(f"calver {subcmd} [--major/--minor/--patch] required for use with SemVer.")
|
||||
else:
|
||||
available_flags = [
|
||||
"--" + part.lower() for part in ['MAJOR', 'MINOR', 'PATCH'] if part in version_pattern
|
||||
]
|
||||
if available_flags:
|
||||
available_flags_str = "/".join(available_flags)
|
||||
logger.info(f"Perhaps try: bumpver {subcmd} {available_flags_str} ")
|
||||
|
||||
|
||||
def _get_normalized_pattern(raw_pattern: str, version_pattern: typ.Optional[str]) -> str:
|
||||
is_version_pattern_required = "{version}" in raw_pattern or "{pep440_version}" in raw_pattern
|
||||
|
||||
if is_version_pattern_required and version_pattern is None:
|
||||
logger.error(
|
||||
"Argument --version-pattern=<PATTERN> is required"
|
||||
" for placeholders: {version}/{pep440_version}."
|
||||
)
|
||||
sys.exit(1)
|
||||
elif version_pattern is None:
|
||||
_version_pattern = "INVALID" # pacify mypy, it's not referenced in raw_pattern
|
||||
else:
|
||||
_version_pattern = version_pattern
|
||||
|
||||
if is_version_pattern_required:
|
||||
return v2patterns.normalize_pattern(_version_pattern, raw_pattern)
|
||||
else:
|
||||
return raw_pattern
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version="2020.1041-beta")
|
||||
@click.help_option()
|
||||
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
|
||||
def cli(verbose: int = 0) -> None:
|
||||
"""Automatically update CalVer version strings in plaintext files."""
|
||||
if verbose:
|
||||
_configure_logging(verbose=max(_VERBOSE, verbose))
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("old_version")
|
||||
@click.argument("pattern")
|
||||
@click.option('-v' , '--verbose', count=True, help="Control log level. -vv for debug level.")
|
||||
@click.option("--major", is_flag=True, default=False, help="Increment major component.")
|
||||
@click.option("-m" , "--minor", is_flag=True, default=False, help="Increment minor component.")
|
||||
@click.option("-p" , "--patch", is_flag=True, default=False, help="Increment patch component.")
|
||||
@click.option(
|
||||
"--tag",
|
||||
default=None,
|
||||
metavar="<NAME>",
|
||||
help=(
|
||||
f"Override release tag of current_version. Valid options are: "
|
||||
f"{', '.join(VALID_RELEASE_TAG_VALUES)}."
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--tag-num",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Increment release tag number (rc1, rc2, rc3..).",
|
||||
)
|
||||
@click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.")
|
||||
@click.option(
|
||||
"--date",
|
||||
default=None,
|
||||
metavar="<ISODATE>",
|
||||
help=f"Set explicit date in format YYYY-0M-0D (e.g. {_current_date}).",
|
||||
)
|
||||
def test(
|
||||
old_version: str,
|
||||
pattern : str,
|
||||
verbose : int = 0,
|
||||
tag : str = None,
|
||||
major : bool = False,
|
||||
minor : bool = False,
|
||||
patch : bool = False,
|
||||
tag_num : bool = False,
|
||||
pin_date : bool = False,
|
||||
date : typ.Optional[str] = None,
|
||||
) -> None:
|
||||
"""Increment a version number for demo purposes."""
|
||||
_configure_logging(verbose=max(_VERBOSE, verbose))
|
||||
_validate_release_tag(tag)
|
||||
|
||||
raw_pattern = pattern # use internal naming convention
|
||||
|
||||
_validate_flags(raw_pattern, major, minor, patch)
|
||||
_date = _validate_date(date, pin_date)
|
||||
|
||||
new_version = incr_dispatch(
|
||||
old_version,
|
||||
raw_pattern=raw_pattern,
|
||||
major=major,
|
||||
minor=minor,
|
||||
patch=patch,
|
||||
tag=tag,
|
||||
tag_num=tag_num,
|
||||
pin_date=pin_date,
|
||||
date=_date,
|
||||
)
|
||||
if new_version is None:
|
||||
_log_no_change('test', raw_pattern, old_version)
|
||||
sys.exit(1)
|
||||
|
||||
pep440_version = version.to_pep440(new_version)
|
||||
|
||||
click.echo(f"New Version: {new_version}")
|
||||
if new_version != pep440_version:
|
||||
click.echo(f"PEP440 : {pep440_version}")
|
||||
|
||||
|
||||
def _grep_text(pattern: patterns.Pattern, text: str, color: bool) -> typ.Iterable[str]:
|
||||
all_lines = text.splitlines()
|
||||
for match in pattern.regexp.finditer(text):
|
||||
match_start, match_end = match.span()
|
||||
|
||||
line_idx = text[:match_start].count("\n")
|
||||
line_start = text.rfind("\n", 0, match_start) + 1
|
||||
line_end = text.find("\n", match_end, -1)
|
||||
if color:
|
||||
matched_line = (
|
||||
text[line_start:match_start]
|
||||
+ colorama.Style.BRIGHT
|
||||
+ text[match_start:match_end]
|
||||
+ colorama.Style.RESET_ALL
|
||||
+ text[match_end:line_end]
|
||||
)
|
||||
else:
|
||||
matched_line = (
|
||||
text[line_start:match_start]
|
||||
+ text[match_start:match_end]
|
||||
+ text[match_end:line_end]
|
||||
)
|
||||
|
||||
lines_offset = max(0, line_idx - 1) + 1
|
||||
lines = all_lines[line_idx - 1 : line_idx + 2]
|
||||
|
||||
if line_idx == 0:
|
||||
lines[0] = matched_line
|
||||
else:
|
||||
lines[1] = matched_line
|
||||
|
||||
prefixed_lines = [f"{lines_offset + i:>4}: {line}" for i, line in enumerate(lines)]
|
||||
yield "\n".join(prefixed_lines)
|
||||
|
||||
|
||||
def _grep(
|
||||
raw_pattern: str,
|
||||
file_ios : typ.Tuple[io.TextIOWrapper],
|
||||
color : bool,
|
||||
) -> None:
|
||||
pattern = v2patterns.compile_pattern(raw_pattern)
|
||||
|
||||
match_count = 0
|
||||
for file_io in file_ios:
|
||||
text = file_io.read()
|
||||
|
||||
match_strs = list(_grep_text(pattern, text, color))
|
||||
if len(match_strs) > 0:
|
||||
if len(file_ios) > 1:
|
||||
print(file_io.name)
|
||||
for match_str in match_strs:
|
||||
print(match_str)
|
||||
print()
|
||||
|
||||
match_count += len(match_strs)
|
||||
|
||||
if match_count == 0:
|
||||
logger.error(f"Pattern not found: '{raw_pattern}'")
|
||||
|
||||
if match_count == 0 or _VERBOSE:
|
||||
pyexpr_regex = regexfmt.pyexpr_regex(pattern.regexp.pattern)
|
||||
|
||||
print("# " + regexfmt.regex101_url(pattern.regexp.pattern))
|
||||
print(pyexpr_regex)
|
||||
print()
|
||||
|
||||
if match_count == 0:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"-v",
|
||||
"--verbose",
|
||||
count=True,
|
||||
help="Control log level. -vv for debug level.",
|
||||
)
|
||||
@click.option(
|
||||
"--version-pattern",
|
||||
default=None,
|
||||
metavar="<PATTERN>",
|
||||
help="Pattern to use for placeholders: {version}/{pep440_version}",
|
||||
)
|
||||
@click.argument("pattern")
|
||||
@click.argument('files', nargs=-1, type=click.File('r'))
|
||||
def grep(
|
||||
pattern : str,
|
||||
files : typ.Tuple[io.TextIOWrapper],
|
||||
version_pattern: typ.Optional[str] = None,
|
||||
verbose : int = 0,
|
||||
) -> None:
|
||||
"""Search file(s) for a version pattern."""
|
||||
verbose = max(_VERBOSE, verbose)
|
||||
_configure_logging(verbose)
|
||||
|
||||
raw_pattern = pattern # use internal naming convention
|
||||
normalized_pattern = _get_normalized_pattern(raw_pattern, version_pattern)
|
||||
|
||||
isatty = getattr(sys.stdout, 'isatty', lambda: False)
|
||||
|
||||
if isatty():
|
||||
colorama.init()
|
||||
try:
|
||||
_grep(normalized_pattern, files, color=True)
|
||||
finally:
|
||||
colorama.deinit()
|
||||
else:
|
||||
_grep(normalized_pattern, files, color=False)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
|
||||
@click.option(
|
||||
"-f/-n", "--fetch/--no-fetch", is_flag=True, default=True, help="Sync tags from remote origin."
|
||||
)
|
||||
def show(verbose: int = 0, fetch: bool = True) -> None:
|
||||
"""Show current version of your project."""
|
||||
_configure_logging(verbose=max(_VERBOSE, verbose))
|
||||
|
||||
_, cfg = config.init(project_path=".")
|
||||
|
||||
if cfg is None:
|
||||
logger.error("Could not parse configuration. Perhaps try 'bumpver init'.")
|
||||
sys.exit(1)
|
||||
|
||||
cfg = _update_cfg_from_vcs(cfg, fetch)
|
||||
click.echo(f"Current Version: {cfg.current_version}")
|
||||
click.echo(f"PEP440 : {cfg.pep440_version}")
|
||||
|
||||
|
||||
def _colored_diff_lines(diff: str) -> typ.Iterable[str]:
|
||||
for line in diff.splitlines():
|
||||
if line.startswith("+++") or line.startswith("---"):
|
||||
yield line
|
||||
elif line.startswith("+"):
|
||||
yield "\u001b[32m" + line + "\u001b[0m"
|
||||
elif line.startswith("-"):
|
||||
yield "\u001b[31m" + line + "\u001b[0m"
|
||||
elif line.startswith("@"):
|
||||
yield "\u001b[36m" + line + "\u001b[0m"
|
||||
else:
|
||||
yield line
|
||||
|
||||
|
||||
def _v2_get_diff(cfg: config.Config, new_version: str) -> str:
|
||||
old_vinfo = v2version.parse_version_info(cfg.current_version, cfg.version_pattern)
|
||||
new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern)
|
||||
return v2rewrite.diff(old_vinfo, new_vinfo, cfg.file_patterns)
|
||||
|
||||
|
||||
def _v1_get_diff(cfg: config.Config, new_version: str) -> str:
|
||||
old_vinfo = v1version.parse_version_info(cfg.current_version, cfg.version_pattern)
|
||||
new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern)
|
||||
return v1rewrite.diff(old_vinfo, new_vinfo, cfg.file_patterns)
|
||||
|
||||
|
||||
def get_diff(cfg, new_version) -> str:
|
||||
if cfg.is_new_pattern:
|
||||
return _v2_get_diff(cfg, new_version)
|
||||
else:
|
||||
return _v1_get_diff(cfg, new_version)
|
||||
|
||||
|
||||
def _print_diff_str(diff: str) -> None:
|
||||
colored_diff = "\n".join(_colored_diff_lines(diff))
|
||||
if sys.stdout.isatty():
|
||||
click.echo(colored_diff)
|
||||
else:
|
||||
click.echo(diff)
|
||||
|
||||
|
||||
def _print_diff(cfg: config.Config, new_version: str) -> None:
|
||||
try:
|
||||
diff = get_diff(cfg, new_version)
|
||||
_print_diff_str(diff)
|
||||
except OSError as err:
|
||||
logger.error(str(err))
|
||||
sys.exit(1)
|
||||
except rewrite.NoPatternMatch as ex:
|
||||
logger.error(str(ex))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def incr_dispatch(
|
||||
old_version: str,
|
||||
raw_pattern: str,
|
||||
*,
|
||||
major : bool = False,
|
||||
minor : bool = False,
|
||||
patch : bool = False,
|
||||
tag : str = None,
|
||||
tag_num : bool = False,
|
||||
pin_date: bool = False,
|
||||
date : typ.Optional[dt.date] = None,
|
||||
) -> typ.Optional[str]:
|
||||
v1_parts = list(v1patterns.PART_PATTERNS) + list(v1patterns.FULL_PART_FORMATS)
|
||||
has_v1_part = any("{" + part + "}" in raw_pattern for part in v1_parts)
|
||||
|
||||
if _VERBOSE:
|
||||
if has_v1_part:
|
||||
pattern = v1patterns.compile_pattern(raw_pattern)
|
||||
else:
|
||||
pattern = v2patterns.compile_pattern(raw_pattern)
|
||||
|
||||
logger.info("Using pattern " + raw_pattern)
|
||||
logger.info("regex = " + regexfmt.pyexpr_regex(pattern.regexp.pattern))
|
||||
|
||||
if has_v1_part:
|
||||
new_version = v1version.incr(
|
||||
old_version,
|
||||
raw_pattern=raw_pattern,
|
||||
major=major,
|
||||
minor=minor,
|
||||
patch=patch,
|
||||
tag=tag,
|
||||
tag_num=tag_num,
|
||||
pin_date=pin_date,
|
||||
date=date,
|
||||
)
|
||||
else:
|
||||
new_version = v2version.incr(
|
||||
old_version,
|
||||
raw_pattern=raw_pattern,
|
||||
major=major,
|
||||
minor=minor,
|
||||
patch=patch,
|
||||
tag=tag,
|
||||
tag_num=tag_num,
|
||||
pin_date=pin_date,
|
||||
date=date,
|
||||
)
|
||||
|
||||
if new_version is None:
|
||||
return None
|
||||
elif pkg_resources.parse_version(new_version) <= pkg_resources.parse_version(old_version):
|
||||
logger.error("Invariant violated: New version must be greater than old version ")
|
||||
logger.error(f" Failed Invariant: '{new_version}' > '{old_version}'")
|
||||
return None
|
||||
else:
|
||||
return new_version
|
||||
|
||||
|
||||
def _update(
|
||||
cfg : config.Config,
|
||||
new_version : str,
|
||||
commit_message: str,
|
||||
allow_dirty : bool = False,
|
||||
) -> None:
|
||||
vcs_api: typ.Optional[vcs.VCSAPI] = None
|
||||
|
||||
if cfg.commit:
|
||||
try:
|
||||
vcs_api = vcs.get_vcs_api()
|
||||
except OSError:
|
||||
logger.warning("Version Control System not found, skipping commit.")
|
||||
|
||||
filepaths = set(cfg.file_patterns.keys())
|
||||
|
||||
if vcs_api:
|
||||
vcs.assert_not_dirty(vcs_api, filepaths, allow_dirty)
|
||||
|
||||
try:
|
||||
if cfg.is_new_pattern:
|
||||
new_v2_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern)
|
||||
v2rewrite.rewrite_files(cfg.file_patterns, new_v2_vinfo)
|
||||
else:
|
||||
new_v1_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern)
|
||||
v1rewrite.rewrite_files(cfg.file_patterns, new_v1_vinfo)
|
||||
except rewrite.NoPatternMatch as ex:
|
||||
logger.error(str(ex))
|
||||
sys.exit(1)
|
||||
|
||||
if vcs_api:
|
||||
vcs.commit(cfg, vcs_api, filepaths, new_version, commit_message)
|
||||
|
||||
|
||||
def _try_update(
|
||||
cfg : config.Config,
|
||||
new_version : str,
|
||||
commit_message: str,
|
||||
allow_dirty : bool = False,
|
||||
) -> None:
|
||||
try:
|
||||
_update(cfg, new_version, commit_message, allow_dirty)
|
||||
except sp.CalledProcessError as ex:
|
||||
logger.error(f"Error running subcommand: {ex.cmd}")
|
||||
if ex.stdout:
|
||||
sys.stdout.write(ex.stdout.decode('utf-8'))
|
||||
if ex.stderr:
|
||||
sys.stderr.write(ex.stderr.decode('utf-8'))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
|
||||
@click.option(
|
||||
'-d', "--dry", default=False, is_flag=True, help="Display diff of changes, don't rewrite files."
|
||||
)
|
||||
def init(verbose: int = 0, dry: bool = False) -> None:
|
||||
"""Initialize [calver] configuration."""
|
||||
_configure_logging(verbose=max(_VERBOSE, verbose))
|
||||
|
||||
ctx, cfg = config.init(project_path=".", cfg_missing_ok=True)
|
||||
|
||||
if cfg:
|
||||
logger.error(f"Configuration already initialized in {ctx.config_rel_path}")
|
||||
sys.exit(1)
|
||||
|
||||
if dry:
|
||||
click.echo(f"Exiting because of '-d/--dry'. Would have written to {ctx.config_rel_path}:")
|
||||
cfg_text: str = config.default_config(ctx)
|
||||
click.echo("\n " + "\n ".join(cfg_text.splitlines()))
|
||||
sys.exit(0)
|
||||
|
||||
config.write_content(ctx)
|
||||
|
||||
|
||||
def get_latest_vcs_version_tag(cfg: config.Config, fetch: bool) -> typ.Optional[str]:
|
||||
all_tags = vcs.get_tags(fetch=fetch)
|
||||
|
||||
if cfg.is_new_pattern:
|
||||
version_tags = [tag for tag in all_tags if v2version.is_valid(tag, cfg.version_pattern)]
|
||||
else:
|
||||
version_tags = [tag for tag in all_tags if v1version.is_valid(tag, cfg.version_pattern)]
|
||||
|
||||
if version_tags:
|
||||
version_tags.sort(key=pkg_resources.parse_version, reverse=True)
|
||||
_debug_tags = ", ".join(version_tags[:3])
|
||||
logger.debug(f"found tags: {_debug_tags} ... ({len(version_tags)} in total)")
|
||||
return version_tags[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config:
|
||||
latest_version_tag = get_latest_vcs_version_tag(cfg, fetch)
|
||||
if latest_version_tag is None:
|
||||
logger.debug("no vcs tags found")
|
||||
return cfg
|
||||
else:
|
||||
latest_version_pep440 = version.to_pep440(latest_version_tag)
|
||||
if latest_version_tag <= cfg.current_version:
|
||||
# current_version already newer/up-to-date
|
||||
return cfg
|
||||
else:
|
||||
logger.info(f"Working dir version : {cfg.current_version}")
|
||||
logger.info(f"Latest version from VCS tag: {latest_version_tag}")
|
||||
return cfg._replace(
|
||||
current_version=latest_version_tag,
|
||||
pep440_version=latest_version_pep440,
|
||||
)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"-v",
|
||||
"--verbose",
|
||||
count=True,
|
||||
help="Control log level. -vv for debug level.",
|
||||
)
|
||||
@click.option(
|
||||
"-f/-n",
|
||||
"--fetch/--no-fetch",
|
||||
is_flag=True,
|
||||
default=True,
|
||||
help="Sync tags from remote origin.",
|
||||
)
|
||||
@click.option(
|
||||
"-d",
|
||||
"--dry",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
help="Display diff of changes, don't rewrite files.",
|
||||
)
|
||||
@click.option(
|
||||
"--allow-dirty",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
help=(
|
||||
"Commit even when working directory is has uncomitted changes. "
|
||||
"(WARNING: The commit will still be aborted if there are uncomitted "
|
||||
"to files with version strings."
|
||||
),
|
||||
)
|
||||
@click.option("--major", is_flag=True, default=False, help="Increment major component.")
|
||||
@click.option("-m", "--minor", is_flag=True, default=False, help="Increment minor component.")
|
||||
@click.option("-p", "--patch", is_flag=True, default=False, help="Increment patch component.")
|
||||
@click.option(
|
||||
"-t",
|
||||
"--tag",
|
||||
default=None,
|
||||
metavar="<NAME>",
|
||||
help=(
|
||||
f"Override release tag of current_version. Valid options are: "
|
||||
f"{', '.join(VALID_RELEASE_TAG_VALUES)}."
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--tag-num",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Increment release tag number (rc1, rc2, rc3..).",
|
||||
)
|
||||
@click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.")
|
||||
@click.option(
|
||||
"--date",
|
||||
default=None,
|
||||
metavar="<ISODATE>",
|
||||
help=f"Set explicit date in format YYYY-0M-0D (e.g. {_current_date}).",
|
||||
)
|
||||
def update(
|
||||
verbose : int = 0,
|
||||
dry : bool = False,
|
||||
allow_dirty: bool = False,
|
||||
fetch : bool = True,
|
||||
major : bool = False,
|
||||
minor : bool = False,
|
||||
patch : bool = False,
|
||||
tag : typ.Optional[str] = None,
|
||||
tag_num : bool = False,
|
||||
pin_date : bool = False,
|
||||
date : typ.Optional[str] = None,
|
||||
) -> None:
|
||||
"""Update project files with the incremented version string."""
|
||||
verbose = max(_VERBOSE, verbose)
|
||||
_configure_logging(verbose)
|
||||
_validate_release_tag(tag)
|
||||
_date = _validate_date(date, pin_date)
|
||||
|
||||
_, cfg = config.init(project_path=".")
|
||||
|
||||
if cfg is None:
|
||||
logger.error("Could not parse configuration. Perhaps try 'bumpver init'.")
|
||||
sys.exit(1)
|
||||
|
||||
cfg = _update_cfg_from_vcs(cfg, fetch)
|
||||
|
||||
old_version = cfg.current_version
|
||||
new_version = incr_dispatch(
|
||||
old_version,
|
||||
raw_pattern=cfg.version_pattern,
|
||||
major=major,
|
||||
minor=minor,
|
||||
patch=patch,
|
||||
tag=tag,
|
||||
tag_num=tag_num,
|
||||
pin_date=pin_date,
|
||||
date=_date,
|
||||
)
|
||||
|
||||
if new_version is None:
|
||||
_log_no_change('update', cfg.version_pattern, old_version)
|
||||
sys.exit(1)
|
||||
|
||||
logger.info(f"Old Version: {old_version}")
|
||||
logger.info(f"New Version: {new_version}")
|
||||
|
||||
if dry or verbose >= 2:
|
||||
_print_diff(cfg, new_version)
|
||||
|
||||
if dry:
|
||||
return
|
||||
|
||||
commit_message_kwargs = {
|
||||
'new_version' : new_version,
|
||||
'old_version' : old_version,
|
||||
'new_version_pep440': version.to_pep440(new_version),
|
||||
'old_version_pep440': version.to_pep440(old_version),
|
||||
}
|
||||
commit_message = cfg.commit_message.format(**commit_message_kwargs)
|
||||
|
||||
_try_update(cfg, new_version, commit_message, allow_dirty)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
621
src/bumpver/config.py
Normal file
621
src/bumpver/config.py
Normal file
|
|
@ -0,0 +1,621 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://gitlab.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Parse bumpver.toml, setup.cfg or pyproject.toml files."""
|
||||
|
||||
import re
|
||||
import glob
|
||||
import typing as typ
|
||||
import logging
|
||||
import datetime as dt
|
||||
import configparser
|
||||
|
||||
import toml
|
||||
import pathlib2 as pl
|
||||
|
||||
from . import version
|
||||
from . import v1version
|
||||
from . import v2version
|
||||
from . import v1patterns
|
||||
from . import v2patterns
|
||||
from .patterns import Pattern
|
||||
|
||||
logger = logging.getLogger("bumpver.config")
|
||||
|
||||
RawPatterns = typ.List[str]
|
||||
RawPatternsByFile = typ.Dict[str, RawPatterns]
|
||||
FileRawPatternsItem = typ.Tuple[str, RawPatterns]
|
||||
|
||||
PatternsByFile = typ.Dict[str, typ.List[Pattern]]
|
||||
FilePatternsItem = typ.Tuple[str, typ.List[Pattern]]
|
||||
|
||||
|
||||
SUPPORTED_CONFIGS = ["setup.cfg", "pyproject.toml", "pycalver.toml", "bumpver.toml"]
|
||||
|
||||
DEFAULT_COMMIT_MESSAGE = "bump version to {new_version}"
|
||||
|
||||
|
||||
class ProjectContext(typ.NamedTuple):
|
||||
"""Container class for project info."""
|
||||
|
||||
path : pl.Path
|
||||
config_filepath: pl.Path
|
||||
config_rel_path: str
|
||||
config_format : str
|
||||
vcs_type : typ.Optional[str]
|
||||
|
||||
|
||||
def _parse_config_and_format(path: pl.Path) -> typ.Tuple[pl.Path, str, str]:
|
||||
if (path / "pycalver.toml").exists():
|
||||
config_filepath = path / "pycalver.toml"
|
||||
config_format = 'toml'
|
||||
elif (path / "bumpver.toml").exists():
|
||||
config_filepath = path / "bumpver.toml"
|
||||
config_format = 'toml'
|
||||
elif (path / "pyproject.toml").exists():
|
||||
config_filepath = path / "pyproject.toml"
|
||||
config_format = 'toml'
|
||||
elif (path / "setup.cfg").exists():
|
||||
config_filepath = path / "setup.cfg"
|
||||
config_format = 'cfg'
|
||||
else:
|
||||
# fallback to creating a new bumpver.toml
|
||||
config_filepath = path / "bumpver.toml"
|
||||
config_format = 'toml'
|
||||
|
||||
if config_filepath.is_absolute():
|
||||
config_rel_path = str(config_filepath.relative_to(path.absolute()))
|
||||
else:
|
||||
config_rel_path = str(config_filepath)
|
||||
config_filepath = pl.Path.cwd() / config_filepath
|
||||
|
||||
return (config_filepath, config_rel_path, config_format)
|
||||
|
||||
|
||||
def init_project_ctx(project_path: typ.Union[str, pl.Path, None] = ".") -> ProjectContext:
|
||||
"""Initialize ProjectContext from a path."""
|
||||
if isinstance(project_path, pl.Path):
|
||||
path = project_path
|
||||
elif project_path is None:
|
||||
path = pl.Path(".")
|
||||
else:
|
||||
# assume it's a str/unicode
|
||||
path = pl.Path(project_path)
|
||||
|
||||
config_filepath, config_rel_path, config_format = _parse_config_and_format(path)
|
||||
|
||||
vcs_type: typ.Optional[str]
|
||||
|
||||
if (path / ".git").exists():
|
||||
vcs_type = 'git'
|
||||
elif (path / ".hg").exists():
|
||||
vcs_type = 'hg'
|
||||
else:
|
||||
vcs_type = None
|
||||
|
||||
return ProjectContext(path, config_filepath, config_rel_path, config_format, vcs_type)
|
||||
|
||||
|
||||
RawConfig = typ.Dict[str, typ.Any]
|
||||
MaybeRawConfig = typ.Optional[RawConfig]
|
||||
|
||||
|
||||
class Config(typ.NamedTuple):
|
||||
"""Container for parameters parsed from a config file."""
|
||||
|
||||
current_version: str
|
||||
version_pattern: str
|
||||
pep440_version : str
|
||||
commit_message : str
|
||||
|
||||
commit : bool
|
||||
tag : bool
|
||||
push : bool
|
||||
is_new_pattern: bool
|
||||
|
||||
file_patterns: PatternsByFile
|
||||
|
||||
|
||||
MaybeConfig = typ.Optional[Config]
|
||||
|
||||
|
||||
def _debug_str(cfg: Config) -> str:
|
||||
cfg_str_parts = [
|
||||
"Config Parsed: Config(",
|
||||
f"\n current_version='{cfg.current_version}',",
|
||||
f"\n version_pattern='{cfg.version_pattern}',",
|
||||
f"\n pep440_version='{cfg.pep440_version}',",
|
||||
f"\n commit_message='{cfg.commit_message}',",
|
||||
f"\n commit={cfg.commit},",
|
||||
f"\n tag={cfg.tag},",
|
||||
f"\n push={cfg.push},",
|
||||
f"\n is_new_pattern={cfg.is_new_pattern},",
|
||||
"\n file_patterns={",
|
||||
]
|
||||
|
||||
for filepath, patterns in sorted(cfg.file_patterns.items()):
|
||||
for pattern in patterns:
|
||||
cfg_str_parts.append(f"\n '{filepath}': '{pattern.raw_pattern}',")
|
||||
|
||||
cfg_str_parts += ["\n }\n)"]
|
||||
return "".join(cfg_str_parts)
|
||||
|
||||
|
||||
def _parse_cfg_file_patterns(
|
||||
cfg_parser: configparser.RawConfigParser,
|
||||
) -> typ.Iterable[FileRawPatternsItem]:
|
||||
file_pattern_items: typ.List[typ.Tuple[str, str]]
|
||||
|
||||
if cfg_parser.has_section("pycalver:file_patterns"):
|
||||
file_pattern_items = cfg_parser.items("pycalver:file_patterns")
|
||||
elif cfg_parser.has_section("bumpver:file_patterns"):
|
||||
file_pattern_items = cfg_parser.items("bumpver:file_patterns")
|
||||
else:
|
||||
return
|
||||
|
||||
for filepath, patterns_str in file_pattern_items:
|
||||
maybe_patterns = (line.strip() for line in patterns_str.splitlines())
|
||||
patterns = [p for p in maybe_patterns if p]
|
||||
yield filepath, patterns
|
||||
|
||||
|
||||
class _ConfigParser(configparser.RawConfigParser):
|
||||
# pylint:disable=too-many-ancestors ; from our perspective, it's just one
|
||||
"""Custom parser, simply to override optionxform behaviour."""
|
||||
|
||||
def optionxform(self, optionstr: str) -> str:
|
||||
"""Non-xforming (ie. uppercase preserving) override.
|
||||
|
||||
This is important because our option names are actually
|
||||
filenames, so case sensitivity is relevant. The default
|
||||
behaviour is to do optionstr.lower()
|
||||
"""
|
||||
return optionstr
|
||||
|
||||
|
||||
OptionVal = typ.Union[str, bool, None]
|
||||
|
||||
BOOL_OPTIONS: typ.Mapping[str, OptionVal] = {'commit': False, 'tag': None, 'push': None}
|
||||
|
||||
|
||||
def _parse_cfg(cfg_buffer: typ.IO[str]) -> RawConfig:
|
||||
cfg_parser = _ConfigParser()
|
||||
|
||||
if hasattr(cfg_parser, 'read_file'):
|
||||
cfg_parser.read_file(cfg_buffer)
|
||||
else:
|
||||
cfg_parser.readfp(cfg_buffer) # python2 compat
|
||||
|
||||
raw_cfg: RawConfig
|
||||
if cfg_parser.has_section("pycalver"):
|
||||
raw_cfg = dict(cfg_parser.items("pycalver"))
|
||||
elif cfg_parser.has_section("bumpver"):
|
||||
raw_cfg = dict(cfg_parser.items("bumpver"))
|
||||
else:
|
||||
raise ValueError("Missing [bumpver] section.")
|
||||
|
||||
for option, default_val in BOOL_OPTIONS.items():
|
||||
val: OptionVal = raw_cfg.get(option, default_val)
|
||||
if isinstance(val, (bytes, str)):
|
||||
val = val.lower() in ("yes", "true", "1", "on")
|
||||
raw_cfg[option] = val
|
||||
|
||||
raw_cfg['file_patterns'] = dict(_parse_cfg_file_patterns(cfg_parser))
|
||||
|
||||
_set_raw_config_defaults(raw_cfg)
|
||||
|
||||
return raw_cfg
|
||||
|
||||
|
||||
def _parse_toml(cfg_buffer: typ.IO[str]) -> RawConfig:
|
||||
raw_full_cfg: typ.Any = toml.load(cfg_buffer)
|
||||
raw_cfg : RawConfig
|
||||
|
||||
if 'bumpver' in raw_full_cfg:
|
||||
raw_cfg = raw_full_cfg['bumpver']
|
||||
elif 'pycalver' in raw_full_cfg:
|
||||
raw_cfg = raw_full_cfg['pycalver']
|
||||
else:
|
||||
raw_cfg = {}
|
||||
|
||||
for option, default_val in BOOL_OPTIONS.items():
|
||||
raw_cfg[option] = raw_cfg.get(option, default_val)
|
||||
|
||||
_set_raw_config_defaults(raw_cfg)
|
||||
|
||||
return raw_cfg
|
||||
|
||||
|
||||
def _iter_glob_expanded_file_patterns(
|
||||
raw_patterns_by_file: RawPatternsByFile,
|
||||
) -> typ.Iterable[FileRawPatternsItem]:
|
||||
for filepath_glob, raw_patterns in raw_patterns_by_file.items():
|
||||
filepaths = glob.glob(filepath_glob)
|
||||
if filepaths:
|
||||
for filepath in filepaths:
|
||||
yield filepath, raw_patterns
|
||||
else:
|
||||
logger.warning(f"Invalid config, no such file: {filepath_glob}")
|
||||
# fallback to treating it as a simple path
|
||||
yield filepath_glob, raw_patterns
|
||||
|
||||
|
||||
def _compile_v1_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsItem]:
|
||||
"""Create inernal/compiled representation of the file_patterns config field.
|
||||
|
||||
The result the same, regardless of the config format.
|
||||
"""
|
||||
# current_version: str = raw_cfg['current_version']
|
||||
# current_pep440_version = version.pep440_version(current_version)
|
||||
|
||||
version_pattern : str = raw_cfg['version_pattern']
|
||||
raw_patterns_by_file: RawPatternsByFile = raw_cfg['file_patterns']
|
||||
|
||||
for filepath, raw_patterns in _iter_glob_expanded_file_patterns(raw_patterns_by_file):
|
||||
compiled_patterns = v1patterns.compile_patterns(version_pattern, raw_patterns)
|
||||
yield filepath, compiled_patterns
|
||||
|
||||
|
||||
def _compile_v2_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsItem]:
|
||||
"""Create inernal/compiled representation of the file_patterns config field.
|
||||
|
||||
The result the same, regardless of the config format.
|
||||
"""
|
||||
version_pattern : str = raw_cfg['version_pattern']
|
||||
raw_patterns_by_file: RawPatternsByFile = raw_cfg['file_patterns']
|
||||
|
||||
for filepath, raw_patterns in _iter_glob_expanded_file_patterns(raw_patterns_by_file):
|
||||
compiled_patterns = v2patterns.compile_patterns(version_pattern, raw_patterns)
|
||||
yield filepath, compiled_patterns
|
||||
|
||||
|
||||
def _compile_file_patterns(raw_cfg: RawConfig, is_new_pattern: bool) -> PatternsByFile:
|
||||
if is_new_pattern:
|
||||
_file_pattern_items = _compile_v2_file_patterns(raw_cfg)
|
||||
else:
|
||||
_file_pattern_items = _compile_v1_file_patterns(raw_cfg)
|
||||
|
||||
# NOTE (mb 2020-10-03): There can be multiple items for the same
|
||||
# path, so this is not an option:
|
||||
#
|
||||
# return dict(_file_pattern_items)
|
||||
|
||||
file_patterns: PatternsByFile = {}
|
||||
for path, patterns in _file_pattern_items:
|
||||
if path in file_patterns:
|
||||
file_patterns[path].extend(patterns)
|
||||
else:
|
||||
file_patterns[path] = patterns
|
||||
return file_patterns
|
||||
|
||||
|
||||
def _validate_version_with_pattern(
|
||||
current_version: str,
|
||||
version_pattern: str,
|
||||
is_new_pattern : bool,
|
||||
) -> None:
|
||||
"""Provoke ValueError if version_pattern and current_version are not compatible."""
|
||||
try:
|
||||
if is_new_pattern:
|
||||
v2version.parse_version_info(current_version, version_pattern)
|
||||
else:
|
||||
v1version.parse_version_info(current_version, version_pattern)
|
||||
except version.PatternError:
|
||||
errmsg = (
|
||||
"Invalid configuration. "
|
||||
f"current_version='{current_version}' is invalid for "
|
||||
f"version_pattern='{version_pattern}'"
|
||||
)
|
||||
raise ValueError(errmsg)
|
||||
|
||||
if is_new_pattern:
|
||||
invalid_chars = re.search(r"([\s]+)", version_pattern)
|
||||
if invalid_chars:
|
||||
errmsg = (
|
||||
f"Invalid character(s) '{invalid_chars.group(1)}'"
|
||||
f' in version_pattern = "{version_pattern}"'
|
||||
)
|
||||
raise ValueError(errmsg)
|
||||
if not v2version.is_valid_week_pattern(version_pattern):
|
||||
errmsg = f"Invalid week number pattern: {version_pattern}"
|
||||
raise ValueError(errmsg)
|
||||
|
||||
|
||||
def _parse_config(raw_cfg: RawConfig) -> Config:
|
||||
"""Parse configuration which was loaded from an .ini/.cfg or .toml file."""
|
||||
|
||||
commit_message: str = raw_cfg.get('commit_message', DEFAULT_COMMIT_MESSAGE)
|
||||
commit_message = raw_cfg['commit_message'] = commit_message.strip("'\" ")
|
||||
|
||||
current_version: str = raw_cfg['current_version']
|
||||
current_version = raw_cfg['current_version'] = current_version.strip("'\" ")
|
||||
|
||||
version_pattern: str = raw_cfg['version_pattern']
|
||||
version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ")
|
||||
|
||||
is_new_pattern = "{" not in version_pattern and "}" not in version_pattern
|
||||
|
||||
_validate_version_with_pattern(current_version, version_pattern, is_new_pattern)
|
||||
|
||||
pep440_version = version.to_pep440(current_version)
|
||||
|
||||
file_patterns = _compile_file_patterns(raw_cfg, is_new_pattern)
|
||||
|
||||
commit = raw_cfg['commit']
|
||||
tag = raw_cfg['tag']
|
||||
push = raw_cfg['push']
|
||||
|
||||
if tag is None:
|
||||
tag = raw_cfg['tag'] = False
|
||||
if push is None:
|
||||
push = raw_cfg['push'] = False
|
||||
|
||||
if tag and not commit:
|
||||
raise ValueError("commit=True required if tag=True")
|
||||
|
||||
if push and not commit:
|
||||
raise ValueError("commit=True required if push=True")
|
||||
|
||||
cfg = Config(
|
||||
current_version=current_version,
|
||||
version_pattern=version_pattern,
|
||||
pep440_version=pep440_version,
|
||||
commit_message=commit_message,
|
||||
commit=commit,
|
||||
tag=tag,
|
||||
push=push,
|
||||
is_new_pattern=is_new_pattern,
|
||||
file_patterns=file_patterns,
|
||||
)
|
||||
logger.debug(_debug_str(cfg))
|
||||
return cfg
|
||||
|
||||
|
||||
def _parse_current_version_default_pattern(raw_cfg: RawConfig, raw_cfg_text: str) -> str:
|
||||
is_config_section = False
|
||||
for line in raw_cfg_text.splitlines():
|
||||
if is_config_section and line.startswith("current_version"):
|
||||
current_version: str = raw_cfg['current_version']
|
||||
version_pattern: str = raw_cfg['version_pattern']
|
||||
return line.replace(current_version, version_pattern)
|
||||
|
||||
if line.strip() == "[pycalver]":
|
||||
is_config_section = True
|
||||
elif line.strip() == "[bumpver]":
|
||||
is_config_section = True
|
||||
elif line and line[0] == "[" and line[-1] == "]":
|
||||
is_config_section = False
|
||||
|
||||
raise ValueError("Could not parse 'current_version'")
|
||||
|
||||
|
||||
def _set_raw_config_defaults(raw_cfg: RawConfig) -> None:
|
||||
if 'version_pattern' not in raw_cfg:
|
||||
raise TypeError("Missing version_pattern")
|
||||
elif not isinstance(raw_cfg['version_pattern'], str):
|
||||
err = f"Invalid type for version_pattern = {raw_cfg['version_pattern']}"
|
||||
raise TypeError(err)
|
||||
|
||||
if 'current_version' not in raw_cfg:
|
||||
raise ValueError("Missing 'current_version' configuration")
|
||||
elif not isinstance(raw_cfg['current_version'], str):
|
||||
err = f"Invalid type for current_version = {raw_cfg['current_version']}"
|
||||
raise TypeError(err)
|
||||
|
||||
if 'file_patterns' not in raw_cfg:
|
||||
raw_cfg['file_patterns'] = {}
|
||||
|
||||
|
||||
def _parse_raw_config(ctx: ProjectContext) -> RawConfig:
|
||||
with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj:
|
||||
if ctx.config_format == 'toml':
|
||||
raw_cfg = _parse_toml(fobj)
|
||||
elif ctx.config_format == 'cfg':
|
||||
raw_cfg = _parse_cfg(fobj)
|
||||
else:
|
||||
err_msg = (
|
||||
f"Invalid config_format='{ctx.config_format}'."
|
||||
"Supported formats are 'setup.cfg' and 'pyproject.toml'"
|
||||
)
|
||||
raise RuntimeError(err_msg)
|
||||
|
||||
if ctx.config_rel_path not in raw_cfg['file_patterns']:
|
||||
with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj:
|
||||
raw_cfg_text = fobj.read()
|
||||
|
||||
# NOTE (mb 2020-09-19): By default we always add
|
||||
# a pattern for the config section itself.
|
||||
raw_version_pattern = _parse_current_version_default_pattern(raw_cfg, raw_cfg_text)
|
||||
raw_cfg['file_patterns'][ctx.config_rel_path] = [raw_version_pattern]
|
||||
|
||||
return raw_cfg
|
||||
|
||||
|
||||
def parse(ctx: ProjectContext, cfg_missing_ok: bool = False) -> MaybeConfig:
|
||||
"""Parse config file if available."""
|
||||
if ctx.config_filepath.exists():
|
||||
try:
|
||||
raw_cfg = _parse_raw_config(ctx)
|
||||
return _parse_config(raw_cfg)
|
||||
except (TypeError, ValueError) as ex:
|
||||
logger.warning(f"Couldn't parse {ctx.config_rel_path}: {str(ex)}")
|
||||
return None
|
||||
else:
|
||||
if not cfg_missing_ok:
|
||||
logger.warning(f"File not found: {ctx.config_rel_path}")
|
||||
return None
|
||||
|
||||
|
||||
def init(
|
||||
project_path : typ.Union[str, pl.Path, None] = ".",
|
||||
cfg_missing_ok: bool = False,
|
||||
) -> typ.Tuple[ProjectContext, MaybeConfig]:
|
||||
ctx = init_project_ctx(project_path)
|
||||
cfg = parse(ctx, cfg_missing_ok)
|
||||
return (ctx, cfg)
|
||||
|
||||
|
||||
DEFAULT_CONFIGPARSER_BASE_TMPL = """
|
||||
[bumpver]
|
||||
current_version = "{initial_version}"
|
||||
version_pattern = "YYYY.BUILD[-TAG]"
|
||||
commit_message = "bump version {{old_version}} -> {{new_version}}"
|
||||
commit = True
|
||||
tag = True
|
||||
push = True
|
||||
|
||||
[bumpver:file_patterns]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_CONFIGPARSER_SETUP_CFG_STR = """
|
||||
setup.cfg =
|
||||
current_version = "{version}"
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_CONFIGPARSER_SETUP_PY_STR = """
|
||||
setup.py =
|
||||
"{version}"
|
||||
"{pep440_version}"
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_CONFIGPARSER_README_RST_STR = """
|
||||
README.rst =
|
||||
{version}
|
||||
{pep440_version}
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_CONFIGPARSER_README_MD_STR = """
|
||||
README.md =
|
||||
{version}
|
||||
{pep440_version}
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_BASE_TMPL = """
|
||||
[bumpver]
|
||||
current_version = "{initial_version}"
|
||||
version_pattern = "YYYY.BUILD[-TAG]"
|
||||
commit_message = "bump version {{old_version}} -> {{new_version}}"
|
||||
commit = true
|
||||
tag = true
|
||||
push = true
|
||||
|
||||
[bumpver.file_patterns]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_PYCALVER_STR = """
|
||||
"pycalver.toml" = [
|
||||
'current_version = "{version}"',
|
||||
]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_BUMPVER_STR = """
|
||||
"bumpver.toml" = [
|
||||
'current_version = "{version}"',
|
||||
]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_PYPROJECT_STR = """
|
||||
"pyproject.toml" = [
|
||||
'current_version = "{version}"',
|
||||
]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_SETUP_PY_STR = """
|
||||
"setup.py" = [
|
||||
"{version}",
|
||||
"{pep440_version}",
|
||||
]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_README_RST_STR = """
|
||||
"README.rst" = [
|
||||
"{version}",
|
||||
"{pep440_version}",
|
||||
]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_README_MD_STR = """
|
||||
"README.md" = [
|
||||
"{version}",
|
||||
"{pep440_version}",
|
||||
]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
def _initial_version() -> str:
|
||||
return dt.datetime.utcnow().strftime("%Y.1001-alpha")
|
||||
|
||||
|
||||
def _initial_version_pep440() -> str:
|
||||
return dt.datetime.utcnow().strftime("%Y.1001a0")
|
||||
|
||||
|
||||
def default_config(ctx: ProjectContext) -> str:
|
||||
"""Generate initial default config."""
|
||||
fmt = ctx.config_format
|
||||
if fmt == 'cfg':
|
||||
base_tmpl = DEFAULT_CONFIGPARSER_BASE_TMPL
|
||||
|
||||
default_pattern_strs_by_filename = {
|
||||
"setup.cfg" : DEFAULT_CONFIGPARSER_SETUP_CFG_STR,
|
||||
"setup.py" : DEFAULT_CONFIGPARSER_SETUP_PY_STR,
|
||||
"README.rst": DEFAULT_CONFIGPARSER_README_RST_STR,
|
||||
"README.md" : DEFAULT_CONFIGPARSER_README_MD_STR,
|
||||
}
|
||||
elif fmt == 'toml':
|
||||
base_tmpl = DEFAULT_TOML_BASE_TMPL
|
||||
|
||||
default_pattern_strs_by_filename = {
|
||||
"pyproject.toml": DEFAULT_TOML_PYPROJECT_STR,
|
||||
"pycalver.toml" : DEFAULT_TOML_PYCALVER_STR,
|
||||
"bumpver.toml" : DEFAULT_TOML_BUMPVER_STR,
|
||||
"setup.py" : DEFAULT_TOML_SETUP_PY_STR,
|
||||
"README.rst" : DEFAULT_TOML_README_RST_STR,
|
||||
"README.md" : DEFAULT_TOML_README_MD_STR,
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Invalid config_format='{fmt}', must be either 'toml' or 'cfg'.")
|
||||
|
||||
cfg_str = base_tmpl.format(initial_version=_initial_version())
|
||||
|
||||
for filename, default_str in default_pattern_strs_by_filename.items():
|
||||
if (ctx.path / filename).exists():
|
||||
cfg_str += default_str
|
||||
|
||||
has_config_file = any((ctx.path / fn).exists() for fn in SUPPORTED_CONFIGS)
|
||||
|
||||
if not has_config_file:
|
||||
if ctx.config_format == 'cfg':
|
||||
cfg_str += DEFAULT_CONFIGPARSER_SETUP_CFG_STR
|
||||
if ctx.config_format == 'toml':
|
||||
cfg_str += DEFAULT_TOML_BUMPVER_STR
|
||||
|
||||
cfg_str += "\n"
|
||||
|
||||
return cfg_str
|
||||
|
||||
|
||||
def write_content(ctx: ProjectContext) -> None:
|
||||
"""Update project config file with initial default config."""
|
||||
fobj: typ.IO[str]
|
||||
|
||||
cfg_content = default_config(ctx)
|
||||
if ctx.config_filepath.exists():
|
||||
cfg_content = "\n" + cfg_content
|
||||
|
||||
with ctx.config_filepath.open(mode="at", encoding="utf-8") as fobj:
|
||||
fobj.write(cfg_content)
|
||||
print(f"Updated {ctx.config_rel_path}")
|
||||
85
src/bumpver/parse.py
Normal file
85
src/bumpver/parse.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Parse PyCalVer strings from files."""
|
||||
|
||||
import typing as typ
|
||||
|
||||
from .patterns import Pattern
|
||||
|
||||
LineNo = int
|
||||
Start = int
|
||||
End = int
|
||||
|
||||
|
||||
class LineSpan(typ.NamedTuple):
|
||||
lineno: LineNo
|
||||
start : Start
|
||||
end : End
|
||||
|
||||
|
||||
LineSpans = typ.List[LineSpan]
|
||||
|
||||
|
||||
def _has_overlap(needle: LineSpan, haystack: LineSpans) -> bool:
|
||||
for span in haystack:
|
||||
# assume needle is in the center
|
||||
has_overlap = (
|
||||
span.lineno == needle.lineno
|
||||
# needle starts before (or at) span end
|
||||
and needle.start <= span.end
|
||||
# needle ends after (or at) span start
|
||||
and needle.end >= span.start
|
||||
)
|
||||
if has_overlap:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class PatternMatch(typ.NamedTuple):
|
||||
"""Container to mark a version string in a file."""
|
||||
|
||||
lineno : LineNo # zero based
|
||||
line : str
|
||||
pattern: Pattern
|
||||
span : typ.Tuple[Start, End]
|
||||
match : str
|
||||
|
||||
|
||||
PatternMatches = typ.Iterable[PatternMatch]
|
||||
|
||||
|
||||
def _iter_for_pattern(lines: typ.List[str], pattern: Pattern) -> PatternMatches:
|
||||
for lineno, line in enumerate(lines):
|
||||
match = pattern.regexp.search(line)
|
||||
if match:
|
||||
yield PatternMatch(lineno, line, pattern, match.span(), match.group(0))
|
||||
|
||||
|
||||
def iter_matches(lines: typ.List[str], patterns: typ.List[Pattern]) -> PatternMatches:
|
||||
"""Iterate over all matches of any pattern on any line.
|
||||
|
||||
>>> from . import v1patterns
|
||||
>>> lines = ["__version__ = 'v201712.0002-alpha'"]
|
||||
>>> version_pattern = "{pycalver}"
|
||||
>>> raw_patterns = ["{pycalver}", "{pep440_pycalver}"]
|
||||
>>> patterns = [v1patterns.compile_pattern(version_pattern, p) for p in raw_patterns]
|
||||
>>> matches = list(iter_matches(lines, patterns))
|
||||
>>> assert matches[0] == PatternMatch(
|
||||
... lineno = 0,
|
||||
... line = "__version__ = 'v201712.0002-alpha'",
|
||||
... pattern= v1patterns.compile_pattern(version_pattern),
|
||||
... span = (15, 33),
|
||||
... match = "v201712.0002-alpha",
|
||||
... )
|
||||
"""
|
||||
matched_spans: LineSpans = []
|
||||
for pattern in patterns:
|
||||
for match in _iter_for_pattern(lines, pattern):
|
||||
needle_span = LineSpan(match.lineno, *match.span)
|
||||
if not _has_overlap(needle_span, matched_spans):
|
||||
yield match
|
||||
matched_spans.append(needle_span)
|
||||
29
src/bumpver/patterns.py
Normal file
29
src/bumpver/patterns.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
import typing as typ
|
||||
|
||||
|
||||
class Pattern(typ.NamedTuple):
|
||||
|
||||
version_pattern: str # "{pycalver}", "{year}.{month}", "vYYYY0M.BUILD"
|
||||
raw_pattern : str # '__version__ = "{version}"', "Copyright (c) YYYY"
|
||||
regexp : typ.Pattern[str]
|
||||
|
||||
|
||||
RE_PATTERN_ESCAPES = [
|
||||
("\u005c", "\u005c\u005c"),
|
||||
("-" , "\u005c-"),
|
||||
("." , "\u005c."),
|
||||
("+" , "\u005c+"),
|
||||
("*" , "\u005c*"),
|
||||
("?" , "\u005c?"),
|
||||
("{" , "\u005c{"),
|
||||
("}" , "\u005c}"),
|
||||
("[" , "\u005c["),
|
||||
("]" , "\u005c]"),
|
||||
("(" , "\u005c("),
|
||||
(")" , "\u005c)"),
|
||||
]
|
||||
47
src/bumpver/pysix.py
Normal file
47
src/bumpver/pysix.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
import sys
|
||||
import typing as typ
|
||||
|
||||
PY2 = sys.version_info.major < 3
|
||||
|
||||
|
||||
try:
|
||||
from urllib.parse import quote as py3_stdlib_quote
|
||||
except ImportError:
|
||||
from urllib import quote as py2_stdlib_quote # type: ignore
|
||||
|
||||
|
||||
# NOTE (mb 2016-05-23): quote in python2 expects bytes argument.
|
||||
|
||||
|
||||
def quote(
|
||||
string : str,
|
||||
safe : str = "/",
|
||||
encoding: typ.Optional[str] = None,
|
||||
errors : typ.Optional[str] = None,
|
||||
) -> str:
|
||||
if not isinstance(string, str):
|
||||
errmsg = f"Expected str/unicode but got {type(string)}" # type: ignore
|
||||
raise TypeError(errmsg)
|
||||
|
||||
if encoding is None:
|
||||
_encoding = "utf-8"
|
||||
else:
|
||||
_encoding = encoding
|
||||
|
||||
if errors is None:
|
||||
_errors = "strict"
|
||||
else:
|
||||
_errors = errors
|
||||
|
||||
if PY2:
|
||||
data = string.encode(_encoding)
|
||||
|
||||
res = py2_stdlib_quote(data, safe=safe.encode(_encoding))
|
||||
return res.decode(_encoding, errors=_errors)
|
||||
else:
|
||||
return py3_stdlib_quote(string, safe=safe, encoding=_encoding, errors=_errors)
|
||||
76
src/bumpver/regexfmt.py
Normal file
76
src/bumpver/regexfmt.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
import re
|
||||
import logging
|
||||
import textwrap
|
||||
|
||||
from . import pysix
|
||||
|
||||
logger = logging.getLogger("bumpver.regexfmt")
|
||||
|
||||
|
||||
def format_regex(regex: str) -> str:
|
||||
r"""Format a regex pattern suitible for flags=re.VERBOSE.
|
||||
|
||||
>>> regex = r"\[CalVer v(?P<year_y>[1-9][0-9]{3})(?P<month>(?:1[0-2]|0[1-9]))"
|
||||
>>> print(format_regex(regex))
|
||||
\[CalVer[ ]v
|
||||
(?P<year_y>[1-9][0-9]{3})
|
||||
(?P<month>
|
||||
(?:1[0-2]|0[1-9])
|
||||
)
|
||||
"""
|
||||
# provoke error for invalid regex
|
||||
re.compile(regex)
|
||||
|
||||
tmp_regex = regex.replace(" ", r"[ ]")
|
||||
tmp_regex = tmp_regex.replace('"', r'\"')
|
||||
tmp_regex, _ = re.subn(r"([^\\])?\)(\?)?", "\\1)\\2\n", tmp_regex)
|
||||
tmp_regex, _ = re.subn(r"([^\\])\(" , "\\1\n(" , tmp_regex)
|
||||
tmp_regex, _ = re.subn(r"^\)\)" , ")\n)" , tmp_regex, flags=re.MULTILINE)
|
||||
lines = tmp_regex.splitlines()
|
||||
indented_lines = []
|
||||
level = 0
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
increment = line.count("(") - line.count(")")
|
||||
if increment >= 0:
|
||||
line = " " * level + line
|
||||
level += increment
|
||||
else:
|
||||
level += increment
|
||||
line = " " * level + line
|
||||
indented_lines.append(line)
|
||||
|
||||
formatted_regex = "\n".join(indented_lines)
|
||||
|
||||
# provoke error if there is a bug in the formatting code
|
||||
re.compile(formatted_regex)
|
||||
return formatted_regex
|
||||
|
||||
|
||||
def pyexpr_regex(regex: str) -> str:
|
||||
try:
|
||||
formatted_regex = format_regex(regex)
|
||||
formatted_regex = textwrap.indent(formatted_regex.rstrip(), " ")
|
||||
return 're.compile(r"""\n' + formatted_regex + '\n""", flags=re.VERBOSE)'
|
||||
except re.error:
|
||||
return f"re.compile({repr(regex)})"
|
||||
|
||||
|
||||
def regex101_url(regex_pattern: str) -> str:
|
||||
try:
|
||||
regex_pattern = format_regex(regex_pattern)
|
||||
except re.error:
|
||||
logger.warning(f"Error formatting regex '{repr(regex_pattern)}'")
|
||||
|
||||
return "".join(
|
||||
(
|
||||
"https://regex101.com/",
|
||||
"?flavor=python",
|
||||
"&flags=gmx" "®ex=" + pysix.quote(regex_pattern),
|
||||
)
|
||||
)
|
||||
90
src/bumpver/rewrite.py
Normal file
90
src/bumpver/rewrite.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
import typing as typ
|
||||
import difflib
|
||||
|
||||
import pathlib2 as pl
|
||||
|
||||
from . import config
|
||||
from .patterns import Pattern
|
||||
|
||||
|
||||
class NoPatternMatch(Exception):
|
||||
"""Pattern not found in content.
|
||||
|
||||
logger.error is used to show error info about the patterns so
|
||||
that users can debug what is wrong with them. The class
|
||||
itself doesn't capture that info. This approach is used so
|
||||
that all patter issues can be shown, rather than bubbling
|
||||
all the way up the stack on the very first pattern with no
|
||||
matches.
|
||||
"""
|
||||
|
||||
|
||||
def detect_line_sep(content: str) -> str:
|
||||
r"""Parse line separator from content.
|
||||
|
||||
>>> detect_line_sep('\r\n')
|
||||
'\r\n'
|
||||
>>> detect_line_sep('\r')
|
||||
'\r'
|
||||
>>> detect_line_sep('\n')
|
||||
'\n'
|
||||
>>> detect_line_sep('')
|
||||
'\n'
|
||||
"""
|
||||
if "\r\n" in content:
|
||||
return "\r\n"
|
||||
elif "\r" in content:
|
||||
return "\r"
|
||||
else:
|
||||
return "\n"
|
||||
|
||||
|
||||
class RewrittenFileData(typ.NamedTuple):
|
||||
"""Container for line-wise content of rewritten files."""
|
||||
|
||||
path : str
|
||||
line_sep : str
|
||||
old_lines: typ.List[str]
|
||||
new_lines: typ.List[str]
|
||||
|
||||
|
||||
PathPatternsItem = typ.Tuple[pl.Path, typ.List[Pattern]]
|
||||
|
||||
|
||||
def iter_path_patterns_items(
|
||||
file_patterns: config.PatternsByFile,
|
||||
) -> typ.Iterable[PathPatternsItem]:
|
||||
for filepath_str, patterns in file_patterns.items():
|
||||
filepath_obj = pl.Path(filepath_str)
|
||||
if filepath_obj.exists():
|
||||
yield (filepath_obj, patterns)
|
||||
else:
|
||||
errmsg = f"File does not exist: '{filepath_str}'"
|
||||
raise IOError(errmsg)
|
||||
|
||||
|
||||
def diff_lines(rfd: RewrittenFileData) -> typ.List[str]:
|
||||
r"""Generate unified diff.
|
||||
|
||||
>>> rfd = RewrittenFileData(
|
||||
... path = "<path>",
|
||||
... line_sep = "\n",
|
||||
... old_lines = ["foo"],
|
||||
... new_lines = ["bar"],
|
||||
... )
|
||||
>>> diff_lines(rfd)
|
||||
['--- <path>', '+++ <path>', '@@ -1 +1 @@', '-foo', '+bar']
|
||||
"""
|
||||
lines = difflib.unified_diff(
|
||||
a=rfd.old_lines,
|
||||
b=rfd.new_lines,
|
||||
lineterm="",
|
||||
fromfile=rfd.path,
|
||||
tofile=rfd.path,
|
||||
)
|
||||
return list(lines)
|
||||
24
src/bumpver/utils.py
Normal file
24
src/bumpver/utils.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
import typing as typ
|
||||
import functools
|
||||
|
||||
# NOTE (mb 2020-09-24): The main use of the memo function is
|
||||
# not as a performance optimization, but to reduce logging
|
||||
# spam.
|
||||
|
||||
|
||||
def memo(func: typ.Callable) -> typ.Callable:
|
||||
cache = {}
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args):
|
||||
key = str(args)
|
||||
if key not in cache:
|
||||
cache[key] = func(*args)
|
||||
return cache[key]
|
||||
|
||||
return wrapper
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://gitlab.com/mbarkhau/pycalver
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Compose Regular Expressions from Patterns.
|
||||
|
||||
|
|
@ -32,6 +32,13 @@
|
|||
|
||||
import re
|
||||
import typing as typ
|
||||
import logging
|
||||
|
||||
from . import utils
|
||||
from .patterns import RE_PATTERN_ESCAPES
|
||||
from .patterns import Pattern
|
||||
|
||||
logger = logging.getLogger("bumpver.v1patterns")
|
||||
|
||||
# https://regex101.com/r/fnj60p/10
|
||||
PYCALVER_PATTERN = r"""
|
||||
|
|
@ -56,21 +63,6 @@ PYCALVER_PATTERN = r"""
|
|||
PYCALVER_RE: typ.Pattern[str] = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE)
|
||||
|
||||
|
||||
PATTERN_ESCAPES = [
|
||||
("\u005c", "\u005c\u005c"),
|
||||
("-" , "\u005c-"),
|
||||
("." , "\u005c."),
|
||||
("+" , "\u005c+"),
|
||||
("*" , "\u005c*"),
|
||||
("?" , "\u005c?"),
|
||||
("{" , "\u005c{"),
|
||||
("}" , "\u005c}"),
|
||||
("[" , "\u005c["),
|
||||
("]" , "\u005c]"),
|
||||
("(" , "\u005c("),
|
||||
(")" , "\u005c)"),
|
||||
]
|
||||
|
||||
COMPOSITE_PART_PATTERNS = {
|
||||
'pep440_pycalver': r"{year}{month}\.{BID}(?:{pep440_tag})?",
|
||||
'pycalver' : r"v{year}{month}\.{bid}(?:-{tag})?",
|
||||
|
|
@ -122,6 +114,44 @@ PART_PATTERNS = {
|
|||
}
|
||||
|
||||
|
||||
PATTERN_PART_FIELDS = {
|
||||
'year' : 'year',
|
||||
'month' : 'month',
|
||||
'month_short': 'month',
|
||||
'pep440_tag' : 'tag',
|
||||
'tag' : 'tag',
|
||||
'yy' : 'year',
|
||||
'yyyy' : 'year',
|
||||
'quarter' : 'quarter',
|
||||
'iso_week' : 'iso_week',
|
||||
'us_week' : 'us_week',
|
||||
'dom' : 'dom',
|
||||
'doy' : 'doy',
|
||||
'dom_short' : 'dom',
|
||||
'doy_short' : 'doy',
|
||||
'MAJOR' : 'major',
|
||||
'MINOR' : 'minor',
|
||||
'MM' : 'minor',
|
||||
'MMM' : 'minor',
|
||||
'MMMM' : 'minor',
|
||||
'MMMMM' : 'minor',
|
||||
'PP' : 'patch',
|
||||
'PPP' : 'patch',
|
||||
'PPPP' : 'patch',
|
||||
'PPPPP' : 'patch',
|
||||
'PATCH' : 'patch',
|
||||
'build_no' : 'bid',
|
||||
'bid' : 'bid',
|
||||
'BID' : 'bid',
|
||||
'BB' : 'bid',
|
||||
'BBB' : 'bid',
|
||||
'BBBB' : 'bid',
|
||||
'BBBBB' : 'bid',
|
||||
'BBBBBB' : 'bid',
|
||||
'BBBBBBB' : 'bid',
|
||||
}
|
||||
|
||||
|
||||
FULL_PART_FORMATS = {
|
||||
'pep440_pycalver': "{year}{month:02}.{BID}{pep440_tag}",
|
||||
'pycalver' : "v{year}{month:02}.{bid}{release}",
|
||||
|
|
@ -130,7 +160,7 @@ FULL_PART_FORMATS = {
|
|||
'release_tag' : "{tag}",
|
||||
'build' : ".{bid}",
|
||||
# NOTE (mb 2019-01-04): since release is optional, it
|
||||
# is treates specially in version.format
|
||||
# is treated specially in v1version.format_version
|
||||
# 'release' : "-{tag}",
|
||||
'month' : "{month:02}",
|
||||
'month_short': "{month}",
|
||||
|
|
@ -147,56 +177,17 @@ FULL_PART_FORMATS = {
|
|||
}
|
||||
|
||||
|
||||
PART_FORMATS = {
|
||||
'major' : "[0-9]+",
|
||||
'minor' : "[0-9]{3,}",
|
||||
'patch' : "[0-9]{3,}",
|
||||
'bid' : "[0-9]{4,}",
|
||||
'MAJOR' : "[0-9]+",
|
||||
'MINOR' : "[0-9]+",
|
||||
'MM' : "[0-9]{2,}",
|
||||
'MMM' : "[0-9]{3,}",
|
||||
'MMMM' : "[0-9]{4,}",
|
||||
'MMMMM' : "[0-9]{5,}",
|
||||
'MMMMMM' : "[0-9]{6,}",
|
||||
'MMMMMMM': "[0-9]{7,}",
|
||||
'PATCH' : "[0-9]+",
|
||||
'PP' : "[0-9]{2,}",
|
||||
'PPP' : "[0-9]{3,}",
|
||||
'PPPP' : "[0-9]{4,}",
|
||||
'PPPPP' : "[0-9]{5,}",
|
||||
'PPPPPP' : "[0-9]{6,}",
|
||||
'PPPPPPP': "[0-9]{7,}",
|
||||
'BID' : "[1-9][0-9]*",
|
||||
'BB' : "[1-9][0-9]{1,}",
|
||||
'BBB' : "[1-9][0-9]{2,}",
|
||||
'BBBB' : "[1-9][0-9]{3,}",
|
||||
'BBBBB' : "[1-9][0-9]{4,}",
|
||||
'BBBBBB' : "[1-9][0-9]{5,}",
|
||||
'BBBBBBB': "[1-9][0-9]{6,}",
|
||||
}
|
||||
|
||||
|
||||
def _replace_pattern_parts(pattern: str) -> str:
|
||||
# The pattern is escaped, so that everything besides the format
|
||||
# string variables is treated literally.
|
||||
for part_name, part_pattern in PART_PATTERNS.items():
|
||||
named_part_pattern = f"(?P<{part_name}>{part_pattern})"
|
||||
placeholder = "\u005c{" + part_name + "\u005c}"
|
||||
pattern = pattern.replace(placeholder, named_part_pattern)
|
||||
|
||||
return pattern
|
||||
|
||||
|
||||
def compile_pattern_str(pattern: str) -> str:
|
||||
for char, escaped in PATTERN_ESCAPES:
|
||||
pattern = pattern.replace(char, escaped)
|
||||
|
||||
return _replace_pattern_parts(pattern)
|
||||
|
||||
|
||||
def compile_pattern(pattern: str) -> typ.Pattern[str]:
|
||||
pattern_str = compile_pattern_str(pattern)
|
||||
return re.compile(pattern_str)
|
||||
|
||||
|
||||
def _init_composite_patterns() -> None:
|
||||
for part_name, part_pattern in COMPOSITE_PART_PATTERNS.items():
|
||||
part_pattern = part_pattern.replace("{", "\u005c{").replace("}", "\u005c}")
|
||||
|
|
@ -205,3 +196,44 @@ def _init_composite_patterns() -> None:
|
|||
|
||||
|
||||
_init_composite_patterns()
|
||||
|
||||
|
||||
def _compile_pattern_re(normalized_pattern: str) -> typ.Pattern[str]:
|
||||
escaped_pattern = normalized_pattern
|
||||
for char, escaped in RE_PATTERN_ESCAPES:
|
||||
escaped_pattern = escaped_pattern.replace(char, escaped)
|
||||
|
||||
pattern_str = _replace_pattern_parts(escaped_pattern)
|
||||
return re.compile(pattern_str)
|
||||
|
||||
|
||||
def _normalized_pattern(version_pattern: str, raw_pattern: str) -> str:
|
||||
res = raw_pattern.replace(r"{version}", version_pattern)
|
||||
if version_pattern == r"{pycalver}":
|
||||
res = res.replace(r"{pep440_version}", r"{pep440_pycalver}")
|
||||
elif version_pattern == r"{semver}":
|
||||
res = res.replace(r"{pep440_version}", r"{semver}")
|
||||
elif version_pattern == r"v{year}{month}{build}{release}":
|
||||
res = res.replace(r"{pep440_version}", r"{year}{month}.{BID}{pep440_tag}")
|
||||
elif version_pattern == r"{year}{month}{build}{release}":
|
||||
res = res.replace(r"{pep440_version}", r"{year}{month}.{BID}{pep440_tag}")
|
||||
elif version_pattern == r"v{year}{build}{release}":
|
||||
res = res.replace(r"{pep440_version}", r"{year}.{BID}{pep440_tag}")
|
||||
elif version_pattern == r"{year}{build}{release}":
|
||||
res = res.replace(r"{pep440_version}", r"{year}.{BID}{pep440_tag}")
|
||||
elif r"{pep440_version}" in raw_pattern:
|
||||
logger.warning(f"No mapping of '{version_pattern}' to '{{pep440_version}}'")
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@utils.memo
|
||||
def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern:
|
||||
_raw_pattern = version_pattern if raw_pattern is None else raw_pattern
|
||||
normalized_pattern = _normalized_pattern(version_pattern, _raw_pattern)
|
||||
regexp = _compile_pattern_re(normalized_pattern)
|
||||
return Pattern(version_pattern, normalized_pattern, regexp)
|
||||
|
||||
|
||||
def compile_patterns(version_pattern: str, raw_patterns: typ.List[str]) -> typ.List[Pattern]:
|
||||
return [compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns]
|
||||
154
src/bumpver/v1rewrite.py
Normal file
154
src/bumpver/v1rewrite.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Rewrite files, updating occurences of version strings."""
|
||||
|
||||
import io
|
||||
import typing as typ
|
||||
import logging
|
||||
|
||||
from . import parse
|
||||
from . import config
|
||||
from . import rewrite
|
||||
from . import version
|
||||
from . import regexfmt
|
||||
from . import v1version
|
||||
from .patterns import Pattern
|
||||
|
||||
logger = logging.getLogger("bumpver.v1rewrite")
|
||||
|
||||
|
||||
def rewrite_lines(
|
||||
patterns : typ.List[Pattern],
|
||||
new_vinfo: version.V1VersionInfo,
|
||||
old_lines: typ.List[str],
|
||||
) -> typ.List[str]:
|
||||
"""Replace occurances of patterns in old_lines with new_vinfo."""
|
||||
found_patterns: typ.Set[Pattern] = set()
|
||||
|
||||
new_lines = old_lines[:]
|
||||
for match in parse.iter_matches(old_lines, patterns):
|
||||
found_patterns.add(match.pattern)
|
||||
replacement = v1version.format_version(new_vinfo, match.pattern.raw_pattern)
|
||||
span_l, span_r = match.span
|
||||
new_line = match.line[:span_l] + replacement + match.line[span_r:]
|
||||
new_lines[match.lineno] = new_line
|
||||
|
||||
non_matched_patterns = set(patterns) - found_patterns
|
||||
if non_matched_patterns:
|
||||
for nmp in non_matched_patterns:
|
||||
logger.error(f"No match for pattern '{nmp.raw_pattern}'")
|
||||
msg = (
|
||||
"\n# "
|
||||
+ regexfmt.regex101_url(nmp.regexp.pattern)
|
||||
+ "\nregex = "
|
||||
+ regexfmt.pyexpr_regex(nmp.regexp.pattern)
|
||||
)
|
||||
logger.error(msg)
|
||||
raise rewrite.NoPatternMatch("Invalid pattern(s)")
|
||||
else:
|
||||
return new_lines
|
||||
|
||||
|
||||
def rfd_from_content(
|
||||
patterns : typ.List[Pattern],
|
||||
new_vinfo: version.V1VersionInfo,
|
||||
content : str,
|
||||
path : str = "<path>",
|
||||
) -> rewrite.RewrittenFileData:
|
||||
r"""Rewrite pattern occurrences with version string.
|
||||
|
||||
>>> version_pattern = "{pycalver}"
|
||||
>>> new_vinfo = v1version.parse_version_info("v201809.0123")
|
||||
|
||||
>>> from .v1patterns import compile_pattern
|
||||
>>> patterns = [compile_pattern(version_pattern, '__version__ = "{pycalver}"')]
|
||||
|
||||
>>> content = '__version__ = "v201809.0001-alpha"'
|
||||
>>> rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||
>>> rfd.new_lines
|
||||
['__version__ = "v201809.0123"']
|
||||
|
||||
>>> patterns = [compile_pattern('{semver}', '__version__ = "v{semver}"')]
|
||||
>>> new_vinfo = v1version.parse_version_info("v1.2.3", "v{semver}")
|
||||
|
||||
>>> content = '__version__ = "v1.2.2"'
|
||||
>>> rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||
>>> rfd.new_lines
|
||||
['__version__ = "v1.2.3"']
|
||||
"""
|
||||
line_sep = rewrite.detect_line_sep(content)
|
||||
old_lines = content.split(line_sep)
|
||||
new_lines = rewrite_lines(patterns, new_vinfo, old_lines)
|
||||
return rewrite.RewrittenFileData(path, line_sep, old_lines, new_lines)
|
||||
|
||||
|
||||
def iter_rewritten(
|
||||
file_patterns: config.PatternsByFile,
|
||||
new_vinfo : version.V1VersionInfo,
|
||||
) -> typ.Iterable[rewrite.RewrittenFileData]:
|
||||
"""Iterate over files with version string replaced."""
|
||||
|
||||
fobj: typ.IO[str]
|
||||
|
||||
for file_path, pattern_strs in rewrite.iter_path_patterns_items(file_patterns):
|
||||
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
||||
content = fobj.read()
|
||||
|
||||
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
||||
yield rfd._replace(path=str(file_path))
|
||||
|
||||
|
||||
def diff(
|
||||
old_vinfo : version.V1VersionInfo,
|
||||
new_vinfo : version.V1VersionInfo,
|
||||
file_patterns: config.PatternsByFile,
|
||||
) -> str:
|
||||
"""Generate diffs of rewritten files."""
|
||||
|
||||
full_diff = ""
|
||||
fobj: typ.IO[str]
|
||||
|
||||
for file_path, patterns in sorted(rewrite.iter_path_patterns_items(file_patterns)):
|
||||
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
||||
content = fobj.read()
|
||||
|
||||
has_updated_version = False
|
||||
for pattern in patterns:
|
||||
old_str = v1version.format_version(old_vinfo, pattern.raw_pattern)
|
||||
new_str = v1version.format_version(new_vinfo, pattern.raw_pattern)
|
||||
if old_str != new_str:
|
||||
has_updated_version = True
|
||||
|
||||
try:
|
||||
rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||
except rewrite.NoPatternMatch:
|
||||
# pylint:disable=raise-missing-from ; we support py2, so not an option
|
||||
errmsg = f"No patterns matched for file '{file_path}'"
|
||||
raise rewrite.NoPatternMatch(errmsg)
|
||||
|
||||
rfd = rfd._replace(path=str(file_path))
|
||||
lines = rewrite.diff_lines(rfd)
|
||||
if len(lines) == 0 and has_updated_version:
|
||||
errmsg = f"No patterns matched for file '{file_path}'"
|
||||
raise rewrite.NoPatternMatch(errmsg)
|
||||
|
||||
full_diff += "\n".join(lines) + "\n"
|
||||
|
||||
full_diff = full_diff.rstrip("\n")
|
||||
return full_diff
|
||||
|
||||
|
||||
def rewrite_files(
|
||||
file_patterns: config.PatternsByFile,
|
||||
new_vinfo : version.V1VersionInfo,
|
||||
) -> None:
|
||||
"""Rewrite project files, updating each with the new version."""
|
||||
fobj: typ.IO[str]
|
||||
|
||||
for file_data in iter_rewritten(file_patterns, new_vinfo):
|
||||
new_content = file_data.line_sep.join(file_data.new_lines)
|
||||
with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj:
|
||||
fobj.write(new_content)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://gitlab.com/mbarkhau/pycalver
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Functions related to version string manipulation."""
|
||||
|
||||
|
|
@ -9,94 +9,45 @@ import typing as typ
|
|||
import logging
|
||||
import datetime as dt
|
||||
|
||||
import pkg_resources
|
||||
import lexid
|
||||
|
||||
from . import lex_id
|
||||
from . import patterns
|
||||
from . import version
|
||||
from . import v1patterns
|
||||
|
||||
logger = logging.getLogger("pycalver.version")
|
||||
logger = logging.getLogger("bumpver.v1version")
|
||||
|
||||
|
||||
# The test suite may replace this.
|
||||
TODAY = dt.datetime.utcnow().date()
|
||||
CalInfo = typ.Union[version.V1CalendarInfo, version.V1VersionInfo]
|
||||
|
||||
|
||||
PATTERN_PART_FIELDS = {
|
||||
'year' : 'year',
|
||||
'month' : 'month',
|
||||
'month_short': 'month',
|
||||
'pep440_tag' : 'tag',
|
||||
'tag' : 'tag',
|
||||
'yy' : 'year',
|
||||
'yyyy' : 'year',
|
||||
'quarter' : 'quarter',
|
||||
'iso_week' : 'iso_week',
|
||||
'us_week' : 'us_week',
|
||||
'dom' : 'dom',
|
||||
'doy' : 'doy',
|
||||
'dom_short' : 'dom',
|
||||
'doy_short' : 'doy',
|
||||
'MAJOR' : 'major',
|
||||
'MINOR' : 'minor',
|
||||
'MM' : 'minor',
|
||||
'MMM' : 'minor',
|
||||
'MMMM' : 'minor',
|
||||
'MMMMM' : 'minor',
|
||||
'PP' : 'patch',
|
||||
'PPP' : 'patch',
|
||||
'PPPP' : 'patch',
|
||||
'PPPPP' : 'patch',
|
||||
'PATCH' : 'patch',
|
||||
'build_no' : 'bid',
|
||||
'bid' : 'bid',
|
||||
'BID' : 'bid',
|
||||
'BB' : 'bid',
|
||||
'BBB' : 'bid',
|
||||
'BBBB' : 'bid',
|
||||
'BBBBB' : 'bid',
|
||||
'BBBBBB' : 'bid',
|
||||
'BBBBBBB' : 'bid',
|
||||
}
|
||||
def _is_cal_gt(left: CalInfo, right: CalInfo) -> bool:
|
||||
"""Is left > right for non-None fields."""
|
||||
|
||||
lvals = []
|
||||
rvals = []
|
||||
for field in version.V1CalendarInfo._fields:
|
||||
lval = getattr(left , field)
|
||||
rval = getattr(right, field)
|
||||
if not (lval is None or rval is None):
|
||||
lvals.append(lval)
|
||||
rvals.append(rval)
|
||||
|
||||
return lvals > rvals
|
||||
|
||||
|
||||
class CalendarInfo(typ.NamedTuple):
|
||||
"""Container for calendar components of version strings."""
|
||||
|
||||
year : int
|
||||
quarter : int
|
||||
month : int
|
||||
dom : int
|
||||
doy : int
|
||||
iso_week: int
|
||||
us_week : int
|
||||
def _ver_to_cal_info(vnfo: version.V1VersionInfo) -> version.V1CalendarInfo:
|
||||
return version.V1CalendarInfo(
|
||||
vnfo.year,
|
||||
vnfo.quarter,
|
||||
vnfo.month,
|
||||
vnfo.dom,
|
||||
vnfo.doy,
|
||||
vnfo.iso_week,
|
||||
vnfo.us_week,
|
||||
)
|
||||
|
||||
|
||||
def _date_from_doy(year: int, doy: int) -> dt.date:
|
||||
"""Parse date from year and day of year (1 indexed).
|
||||
|
||||
>>> cases = [
|
||||
... (2016, 1), (2016, 31), (2016, 31 + 1), (2016, 31 + 29), (2016, 31 + 30),
|
||||
... (2017, 1), (2017, 31), (2017, 31 + 1), (2017, 31 + 28), (2017, 31 + 29),
|
||||
... ]
|
||||
>>> dates = [_date_from_doy(year, month) for year, month in cases]
|
||||
>>> assert [(d.month, d.day) for d in dates] == [
|
||||
... (1, 1), (1, 31), (2, 1), (2, 29), (3, 1),
|
||||
... (1, 1), (1, 31), (2, 1), (2, 28), (3, 1),
|
||||
... ]
|
||||
"""
|
||||
return dt.date(year, 1, 1) + dt.timedelta(days=doy - 1)
|
||||
|
||||
|
||||
def _quarter_from_month(month: int) -> int:
|
||||
"""Calculate quarter (1 indexed) from month (1 indexed).
|
||||
|
||||
>>> [_quarter_from_month(month) for month in range(1, 13)]
|
||||
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]
|
||||
"""
|
||||
return ((month - 1) // 3) + 1
|
||||
|
||||
|
||||
def cal_info(date: dt.date = None) -> CalendarInfo:
|
||||
def cal_info(date: dt.date = None) -> version.V1CalendarInfo:
|
||||
"""Generate calendar components for current date.
|
||||
|
||||
>>> from datetime import date
|
||||
|
|
@ -118,11 +69,11 @@ def cal_info(date: dt.date = None) -> CalendarInfo:
|
|||
(2019, 2, 4, 7, 97, 13, 14)
|
||||
"""
|
||||
if date is None:
|
||||
date = TODAY
|
||||
date = version.TODAY
|
||||
|
||||
kwargs = {
|
||||
'year' : date.year,
|
||||
'quarter' : _quarter_from_month(date.month),
|
||||
'quarter' : version.quarter_from_month(date.month),
|
||||
'month' : date.month,
|
||||
'dom' : date.day,
|
||||
'doy' : int(date.strftime("%j"), base=10),
|
||||
|
|
@ -130,24 +81,7 @@ def cal_info(date: dt.date = None) -> CalendarInfo:
|
|||
'us_week' : int(date.strftime("%U"), base=10),
|
||||
}
|
||||
|
||||
return CalendarInfo(**kwargs)
|
||||
|
||||
|
||||
class VersionInfo(typ.NamedTuple):
|
||||
"""Container for parsed version string."""
|
||||
|
||||
year : typ.Optional[int]
|
||||
quarter : typ.Optional[int]
|
||||
month : typ.Optional[int]
|
||||
dom : typ.Optional[int]
|
||||
doy : typ.Optional[int]
|
||||
iso_week: typ.Optional[int]
|
||||
us_week : typ.Optional[int]
|
||||
major : int
|
||||
minor : int
|
||||
patch : int
|
||||
bid : str
|
||||
tag : str
|
||||
return version.V1CalendarInfo(**kwargs)
|
||||
|
||||
|
||||
FieldKey = str
|
||||
|
|
@ -158,24 +92,27 @@ PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr]
|
|||
FieldValues = typ.Dict[FieldKey , MatchGroupStr]
|
||||
|
||||
|
||||
def _parse_field_values(field_values: FieldValues) -> VersionInfo:
|
||||
def _parse_field_values(field_values: FieldValues) -> version.V1VersionInfo:
|
||||
fvals = field_values
|
||||
tag = fvals.get('tag')
|
||||
if tag is None:
|
||||
tag = "final"
|
||||
tag = TAG_ALIASES.get(tag, tag)
|
||||
tag = version.TAG_BY_PEP440_TAG.get(tag, tag)
|
||||
assert tag is not None
|
||||
|
||||
bid = fvals['bid'] if 'bid' in fvals else "0001"
|
||||
|
||||
year = int(fvals['year']) if 'year' in fvals else None
|
||||
if year is not None and year < 100:
|
||||
year += 2000
|
||||
|
||||
doy = int(fvals['doy']) if 'doy' in fvals else None
|
||||
|
||||
month: typ.Optional[int]
|
||||
dom : typ.Optional[int]
|
||||
|
||||
if year and doy:
|
||||
date = _date_from_doy(year, doy)
|
||||
date = version.date_from_doy(year, doy)
|
||||
month = date.month
|
||||
dom = date.day
|
||||
else:
|
||||
|
|
@ -196,13 +133,13 @@ def _parse_field_values(field_values: FieldValues) -> VersionInfo:
|
|||
|
||||
quarter = int(fvals['quarter']) if 'quarter' in fvals else None
|
||||
if quarter is None and month:
|
||||
quarter = _quarter_from_month(month)
|
||||
quarter = version.quarter_from_month(month)
|
||||
|
||||
major = int(fvals['major']) if 'major' in fvals else 0
|
||||
minor = int(fvals['minor']) if 'minor' in fvals else 0
|
||||
patch = int(fvals['patch']) if 'patch' in fvals else 0
|
||||
|
||||
return VersionInfo(
|
||||
return version.V1VersionInfo(
|
||||
year=year,
|
||||
quarter=quarter,
|
||||
month=month,
|
||||
|
|
@ -218,7 +155,7 @@ def _parse_field_values(field_values: FieldValues) -> VersionInfo:
|
|||
)
|
||||
|
||||
|
||||
def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool:
|
||||
def _is_calver(cinfo: CalInfo) -> bool:
|
||||
"""Check pattern for any calendar based parts.
|
||||
|
||||
>>> _is_calver(cal_info())
|
||||
|
|
@ -232,46 +169,30 @@ def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool:
|
|||
>>> _is_calver(vnfo)
|
||||
False
|
||||
"""
|
||||
for field in CalendarInfo._fields:
|
||||
maybe_val: typ.Any = getattr(nfo, field, None)
|
||||
for field in version.V1CalendarInfo._fields:
|
||||
maybe_val: typ.Any = getattr(cinfo, field, None)
|
||||
if isinstance(maybe_val, int):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
TAG_ALIASES: typ.Dict[str, str] = {'a': "alpha", 'b': "beta", 'pre': "rc"}
|
||||
|
||||
|
||||
PEP440_TAGS: typ.Dict[str, str] = {
|
||||
'alpha': "a",
|
||||
'beta' : "b",
|
||||
'final': "",
|
||||
'rc' : "rc",
|
||||
'dev' : "dev",
|
||||
'post' : "post",
|
||||
}
|
||||
|
||||
|
||||
VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]
|
||||
|
||||
|
||||
class PatternError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues:
|
||||
for part_name in pattern_groups.keys():
|
||||
is_valid_part_name = (
|
||||
part_name in patterns.COMPOSITE_PART_PATTERNS or part_name in PATTERN_PART_FIELDS
|
||||
part_name in v1patterns.COMPOSITE_PART_PATTERNS
|
||||
or part_name in v1patterns.PATTERN_PART_FIELDS
|
||||
)
|
||||
if not is_valid_part_name:
|
||||
err_msg = f"Invalid part '{part_name}'"
|
||||
raise PatternError(err_msg)
|
||||
raise version.PatternError(err_msg)
|
||||
|
||||
field_value_items = [
|
||||
(field_name, pattern_groups[part_name])
|
||||
for part_name, field_name in PATTERN_PART_FIELDS.items()
|
||||
for part_name, field_name in v1patterns.PATTERN_PART_FIELDS.items()
|
||||
if part_name in pattern_groups.keys()
|
||||
]
|
||||
|
||||
|
|
@ -281,18 +202,22 @@ def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues:
|
|||
|
||||
if any(duplicate_fields):
|
||||
err_msg = f"Multiple parts for same field {duplicate_fields}."
|
||||
raise PatternError(err_msg)
|
||||
|
||||
raise version.PatternError(err_msg)
|
||||
else:
|
||||
return dict(field_value_items)
|
||||
|
||||
|
||||
def _parse_version_info(pattern_groups: PatternGroups) -> VersionInfo:
|
||||
"""Parse normalized VersionInfo from groups of a matched pattern.
|
||||
def _parse_version_info(pattern_groups: PatternGroups) -> version.V1VersionInfo:
|
||||
"""Parse normalized V1VersionInfo from groups of a matched pattern.
|
||||
|
||||
>>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"})
|
||||
>>> (vnfo.year, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag)
|
||||
(2018, 11, 4, '0099', 'final')
|
||||
|
||||
>>> vnfo = _parse_version_info({'year': "18", 'month': "11"})
|
||||
>>> (vnfo.year, vnfo.month, vnfo.quarter)
|
||||
(2018, 11, 4)
|
||||
|
||||
>>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"})
|
||||
>>> (vnfo.year, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag)
|
||||
(2018, 1, 11, '099', 'beta')
|
||||
|
|
@ -309,42 +234,43 @@ def _parse_version_info(pattern_groups: PatternGroups) -> VersionInfo:
|
|||
return _parse_field_values(field_values)
|
||||
|
||||
|
||||
def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> VersionInfo:
|
||||
"""Parse normalized VersionInfo.
|
||||
def parse_version_info(version_str: str, raw_pattern: str = "{pycalver}") -> version.V1VersionInfo:
|
||||
"""Parse normalized V1VersionInfo.
|
||||
|
||||
>>> vnfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}")
|
||||
>>> vnfo = parse_version_info("v201712.0033-beta", raw_pattern="{pycalver}")
|
||||
>>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"})
|
||||
|
||||
>>> vnfo = parse_version_info("1.23.456", pattern="{semver}")
|
||||
>>> vnfo = parse_version_info("1.23.456", raw_pattern="{semver}")
|
||||
>>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"})
|
||||
"""
|
||||
regex = patterns.compile_pattern(pattern)
|
||||
match = regex.match(version_str)
|
||||
pattern = v1patterns.compile_pattern(raw_pattern)
|
||||
match = pattern.regexp.match(version_str)
|
||||
if match is None:
|
||||
err_msg = (
|
||||
f"Invalid version string '{version_str}' for pattern '{pattern}'/'{regex.pattern}'"
|
||||
f"Invalid version string '{version_str}' "
|
||||
f"for pattern '{raw_pattern}'/'{pattern.regexp.pattern}'"
|
||||
)
|
||||
raise PatternError(err_msg)
|
||||
|
||||
raise version.PatternError(err_msg)
|
||||
else:
|
||||
return _parse_version_info(match.groupdict())
|
||||
|
||||
|
||||
def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool:
|
||||
def is_valid(version_str: str, raw_pattern: str = "{pycalver}") -> bool:
|
||||
"""Check if a version matches a pattern.
|
||||
|
||||
>>> is_valid("v201712.0033-beta", pattern="{pycalver}")
|
||||
>>> is_valid("v201712.0033-beta", raw_pattern="{pycalver}")
|
||||
True
|
||||
>>> is_valid("v201712.0033-beta", pattern="{semver}")
|
||||
>>> is_valid("v201712.0033-beta", raw_pattern="{semver}")
|
||||
False
|
||||
>>> is_valid("1.2.3", pattern="{semver}")
|
||||
>>> is_valid("1.2.3", raw_pattern="{semver}")
|
||||
True
|
||||
>>> is_valid("v201712.0033-beta", pattern="{semver}")
|
||||
>>> is_valid("v201712.0033-beta", raw_pattern="{semver}")
|
||||
False
|
||||
"""
|
||||
try:
|
||||
parse_version_info(version_str, pattern)
|
||||
parse_version_info(version_str, raw_pattern)
|
||||
return True
|
||||
except PatternError:
|
||||
except version.PatternError:
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -374,60 +300,60 @@ ID_FIELDS_BY_PART = {
|
|||
}
|
||||
|
||||
|
||||
def format_version(vinfo: VersionInfo, pattern: str) -> str:
|
||||
def format_version(vinfo: version.V1VersionInfo, raw_pattern: str) -> str:
|
||||
"""Generate version string.
|
||||
|
||||
>>> import datetime as dt
|
||||
>>> vinfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}")
|
||||
>>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="{pycalver}")
|
||||
>>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict())
|
||||
>>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict())
|
||||
>>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final')
|
||||
|
||||
>>> format_version(vinfo_a, pattern="v{yy}.{BID}{release}")
|
||||
>>> format_version(vinfo_a, raw_pattern="v{yy}.{BID}{release}")
|
||||
'v17.33-beta'
|
||||
>>> format_version(vinfo_a, pattern="{pep440_version}")
|
||||
>>> format_version(vinfo_a, raw_pattern="{pep440_version}")
|
||||
'201701.33b0'
|
||||
|
||||
>>> format_version(vinfo_a, pattern="{pycalver}")
|
||||
>>> format_version(vinfo_a, raw_pattern="{pycalver}")
|
||||
'v201701.0033-beta'
|
||||
>>> format_version(vinfo_b, pattern="{pycalver}")
|
||||
>>> format_version(vinfo_b, raw_pattern="{pycalver}")
|
||||
'v201712.0033-beta'
|
||||
|
||||
>>> format_version(vinfo_a, pattern="v{year}w{iso_week}.{BID}{release}")
|
||||
>>> format_version(vinfo_a, raw_pattern="v{year}w{iso_week}.{BID}{release}")
|
||||
'v2017w00.33-beta'
|
||||
>>> format_version(vinfo_b, pattern="v{year}w{iso_week}.{BID}{release}")
|
||||
>>> format_version(vinfo_b, raw_pattern="v{year}w{iso_week}.{BID}{release}")
|
||||
'v2017w52.33-beta'
|
||||
|
||||
>>> format_version(vinfo_a, pattern="v{year}d{doy}.{bid}{release}")
|
||||
>>> format_version(vinfo_a, raw_pattern="v{year}d{doy}.{bid}{release}")
|
||||
'v2017d001.0033-beta'
|
||||
>>> format_version(vinfo_b, pattern="v{year}d{doy}.{bid}{release}")
|
||||
>>> format_version(vinfo_b, raw_pattern="v{year}d{doy}.{bid}{release}")
|
||||
'v2017d365.0033-beta'
|
||||
|
||||
>>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}-{tag}")
|
||||
>>> format_version(vinfo_c, raw_pattern="v{year}w{iso_week}.{BID}-{tag}")
|
||||
'v2017w52.33-final'
|
||||
>>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}{release}")
|
||||
>>> format_version(vinfo_c, raw_pattern="v{year}w{iso_week}.{BID}{release}")
|
||||
'v2017w52.33'
|
||||
|
||||
>>> format_version(vinfo_c, pattern="v{MAJOR}.{MINOR}.{PATCH}")
|
||||
>>> format_version(vinfo_c, raw_pattern="v{MAJOR}.{MINOR}.{PATCH}")
|
||||
'v1.2.34'
|
||||
>>> format_version(vinfo_c, pattern="v{MAJOR}.{MM}.{PPP}")
|
||||
>>> format_version(vinfo_c, raw_pattern="v{MAJOR}.{MM}.{PPP}")
|
||||
'v1.02.034'
|
||||
"""
|
||||
full_pattern = pattern
|
||||
for part_name, full_part_format in patterns.FULL_PART_FORMATS.items():
|
||||
full_pattern = raw_pattern
|
||||
for part_name, full_part_format in v1patterns.FULL_PART_FORMATS.items():
|
||||
full_pattern = full_pattern.replace("{" + part_name + "}", full_part_format)
|
||||
|
||||
kwargs: typ.Dict[str, typ.Union[str, int, None]] = vinfo._asdict()
|
||||
|
||||
tag = vinfo.tag
|
||||
if tag == 'final':
|
||||
release_tag = vinfo.tag
|
||||
if release_tag == 'final':
|
||||
kwargs['release' ] = ""
|
||||
kwargs['pep440_tag'] = ""
|
||||
else:
|
||||
kwargs['release' ] = "-" + tag
|
||||
kwargs['pep440_tag'] = PEP440_TAGS[tag] + "0"
|
||||
kwargs['release' ] = "-" + release_tag
|
||||
kwargs['pep440_tag'] = version.PEP440_TAG_BY_TAG[release_tag] + "0"
|
||||
|
||||
kwargs['release_tag'] = tag
|
||||
kwargs['release_tag'] = release_tag
|
||||
|
||||
year = vinfo.year
|
||||
if year:
|
||||
|
|
@ -453,36 +379,35 @@ def format_version(vinfo: VersionInfo, pattern: str) -> str:
|
|||
|
||||
def incr(
|
||||
old_version: str,
|
||||
pattern : str = "{pycalver}",
|
||||
raw_pattern: str = "{pycalver}",
|
||||
*,
|
||||
release: str = None,
|
||||
major : bool = False,
|
||||
minor : bool = False,
|
||||
patch : bool = False,
|
||||
tag : typ.Optional[str] = None,
|
||||
tag_num : bool = False,
|
||||
pin_date: bool = False,
|
||||
date : typ.Optional[dt.date] = None,
|
||||
) -> typ.Optional[str]:
|
||||
"""Increment version string.
|
||||
|
||||
'old_version' is assumed to be a string that matches 'pattern'
|
||||
"""
|
||||
try:
|
||||
old_vinfo = parse_version_info(old_version, pattern)
|
||||
except PatternError as ex:
|
||||
old_vinfo = parse_version_info(old_version, raw_pattern)
|
||||
except version.PatternError as ex:
|
||||
logger.error(str(ex))
|
||||
return None
|
||||
|
||||
cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info(date)
|
||||
|
||||
if _is_cal_gt(old_vinfo, cur_cinfo):
|
||||
logger.warning(f"Old version appears to be from the future '{old_version}'")
|
||||
cur_vinfo = old_vinfo
|
||||
|
||||
cur_cal_nfo = cal_info()
|
||||
|
||||
old_date = (old_vinfo.year or 0, old_vinfo.month or 0, old_vinfo.dom or 0)
|
||||
cur_date = (cur_cal_nfo.year , cur_cal_nfo.month , cur_cal_nfo.dom)
|
||||
|
||||
if old_date <= cur_date:
|
||||
cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict())
|
||||
else:
|
||||
logger.warning(f"Version appears to be from the future '{old_version}'")
|
||||
cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict())
|
||||
|
||||
cur_vinfo = cur_vinfo._replace(bid=lex_id.next_id(cur_vinfo.bid))
|
||||
cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid))
|
||||
|
||||
if major:
|
||||
cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0)
|
||||
|
|
@ -490,22 +415,14 @@ def incr(
|
|||
cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0)
|
||||
if patch:
|
||||
cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1)
|
||||
if tag_num:
|
||||
raise NotImplementedError("--tag-num not supported for old style patterns")
|
||||
if tag:
|
||||
cur_vinfo = cur_vinfo._replace(tag=tag)
|
||||
|
||||
if release:
|
||||
cur_vinfo = cur_vinfo._replace(tag=release)
|
||||
|
||||
new_version = format_version(cur_vinfo, pattern)
|
||||
new_version = format_version(cur_vinfo, raw_pattern)
|
||||
if new_version == old_version:
|
||||
logger.error("Invalid arguments or pattern, version did not change.")
|
||||
return None
|
||||
else:
|
||||
return new_version
|
||||
|
||||
|
||||
def to_pep440(version: str) -> str:
|
||||
"""Derive pep440 compliant version string from PyCalVer version string.
|
||||
|
||||
>>> to_pep440("v201811.0007-beta")
|
||||
'201811.7b0'
|
||||
"""
|
||||
return str(pkg_resources.parse_version(version))
|
||||
346
src/bumpver/v2patterns.py
Normal file
346
src/bumpver/v2patterns.py
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Compose Regular Expressions from Patterns.
|
||||
|
||||
>>> pattern = compile_pattern("vYYYY0M.BUILD[-TAG]")
|
||||
>>> version_info = pattern.regexp.match("v201712.0123-alpha")
|
||||
>>> assert version_info.groupdict() == {
|
||||
... "year_y" : "2017",
|
||||
... "month" : "12",
|
||||
... "bid" : "0123",
|
||||
... "tag" : "alpha",
|
||||
... }
|
||||
>>>
|
||||
>>> version_info = pattern.regexp.match("201712.1234")
|
||||
>>> assert version_info is None
|
||||
|
||||
>>> version_info = pattern.regexp.match("v201713.1234")
|
||||
>>> assert version_info is None
|
||||
|
||||
>>> version_info = pattern.regexp.match("v201712.1234")
|
||||
>>> assert version_info.groupdict() == {
|
||||
... "year_y" : "2017",
|
||||
... "month" : "12",
|
||||
... "bid" : "1234",
|
||||
... "tag" : None,
|
||||
... }
|
||||
"""
|
||||
|
||||
import re
|
||||
import typing as typ
|
||||
import logging
|
||||
|
||||
from . import utils
|
||||
from .patterns import RE_PATTERN_ESCAPES
|
||||
from .patterns import Pattern
|
||||
|
||||
logger = logging.getLogger("bumpver.v2patterns")
|
||||
|
||||
# NOTE (mb 2020-09-17): For patterns with different options '(AAA|BB|C)', the
|
||||
# patterns with more digits should be first/left of those with fewer digits:
|
||||
#
|
||||
# good: (?:1[0-2]|[1-9])
|
||||
# bad: (?:[1-9]|1[0-2])
|
||||
#
|
||||
# This ensures that the longest match is done for a pattern.
|
||||
#
|
||||
# This implies that patterns for smaller numbers sometimes must be right of
|
||||
# those for larger numbers. To be consistent we use this ordering not
|
||||
# sometimes but always (even though in theory it wouldn't matter):
|
||||
#
|
||||
# good: (?:3[0-1]|[1-2][0-9]|[1-9])
|
||||
# bad: (?:[1-2][0-9]|3[0-1]|[1-9])
|
||||
|
||||
|
||||
PART_PATTERNS = {
|
||||
# Based on calver.org
|
||||
'YYYY': r"[1-9][0-9]{3}",
|
||||
'YY' : r"[1-9][0-9]?",
|
||||
'0Y' : r"[0-9]{2}",
|
||||
'GGGG': r"[1-9][0-9]{3}",
|
||||
'GG' : r"[1-9][0-9]?",
|
||||
'0G' : r"[0-9]{2}",
|
||||
'Q' : r"[1-4]",
|
||||
'MM' : r"1[0-2]|[1-9]",
|
||||
'0M' : r"1[0-2]|0[1-9]",
|
||||
'DD' : r"3[0-1]|[1-2][0-9]|[1-9]",
|
||||
'0D' : r"3[0-1]|[1-2][0-9]|0[1-9]",
|
||||
'JJJ' : r"36[0-6]|3[0-5][0-9]|[1-2][0-9][0-9]|[1-9][0-9]|[1-9]",
|
||||
'00J' : r"36[0-6]|3[0-5][0-9]|[1-2][0-9][0-9]|0[1-9][0-9]|00[1-9]",
|
||||
# week numbering parts
|
||||
'WW': r"5[0-2]|[1-4][0-9]|[0-9]",
|
||||
'0W': r"5[0-2]|[0-4][0-9]",
|
||||
'UU': r"5[0-2]|[1-4][0-9]|[0-9]",
|
||||
'0U': r"5[0-2]|[0-4][0-9]",
|
||||
'VV': r"5[0-3]|[1-4][0-9]|[1-9]",
|
||||
'0V': r"5[0-3]|[1-4][0-9]|0[1-9]",
|
||||
# non calver parts
|
||||
'MAJOR': r"[0-9]+",
|
||||
'MINOR': r"[0-9]+",
|
||||
'PATCH': r"[0-9]+",
|
||||
'BUILD': r"[0-9]+",
|
||||
'BLD' : r"[1-9][0-9]*",
|
||||
'TAG' : r"preview|final|alpha|beta|post|rc",
|
||||
'PYTAG': r"post|rc|a|b",
|
||||
'NUM' : r"[0-9]+",
|
||||
'INC0' : r"[0-9]+",
|
||||
'INC1' : r"[1-9][0-9]*",
|
||||
}
|
||||
|
||||
|
||||
PATTERN_PART_FIELDS = {
|
||||
'YYYY' : 'year_y',
|
||||
'YY' : 'year_y',
|
||||
'0Y' : 'year_y',
|
||||
'GGGG' : 'year_g',
|
||||
'GG' : 'year_g',
|
||||
'0G' : 'year_g',
|
||||
'Q' : 'quarter',
|
||||
'MM' : 'month',
|
||||
'0M' : 'month',
|
||||
'DD' : 'dom',
|
||||
'0D' : 'dom',
|
||||
'JJJ' : 'doy',
|
||||
'00J' : 'doy',
|
||||
'MAJOR': 'major',
|
||||
'MINOR': 'minor',
|
||||
'PATCH': 'patch',
|
||||
'BUILD': 'bid',
|
||||
'BLD' : 'bid',
|
||||
'TAG' : 'tag',
|
||||
'PYTAG': 'pytag',
|
||||
'NUM' : 'num',
|
||||
'INC0' : 'inc0',
|
||||
'INC1' : 'inc1',
|
||||
'WW' : 'week_w',
|
||||
'0W' : 'week_w',
|
||||
'UU' : 'week_u',
|
||||
'0U' : 'week_u',
|
||||
'VV' : 'week_v',
|
||||
'0V' : 'week_v',
|
||||
}
|
||||
|
||||
|
||||
PEP440_PART_SUBSTITUTIONS = {
|
||||
'0W' : "WW",
|
||||
'0U' : "UU",
|
||||
'0V' : "VV",
|
||||
'0M' : "MM",
|
||||
'0D' : "DD",
|
||||
'00J' : "JJJ",
|
||||
'BUILD': "BLD",
|
||||
'TAG' : "PYTAG",
|
||||
}
|
||||
|
||||
|
||||
FieldValue = typ.Union[str, int]
|
||||
|
||||
|
||||
def _fmt_num(val: FieldValue) -> str:
|
||||
return str(val)
|
||||
|
||||
|
||||
def _fmt_bld(val: FieldValue) -> str:
|
||||
return str(int(val))
|
||||
|
||||
|
||||
def _fmt_yy(year_y: FieldValue) -> str:
|
||||
return str(int(str(year_y)[-2:]))
|
||||
|
||||
|
||||
def _fmt_0y(year_y: FieldValue) -> str:
|
||||
return "{0:02}".format(int(str(year_y)[-2:]))
|
||||
|
||||
|
||||
def _fmt_gg(year_g: FieldValue) -> str:
|
||||
return str(int(str(year_g)[-2:]))
|
||||
|
||||
|
||||
def _fmt_0g(year_g: FieldValue) -> str:
|
||||
return "{0:02}".format(int(str(year_g)[-2:]))
|
||||
|
||||
|
||||
def _fmt_0m(month: FieldValue) -> str:
|
||||
return "{0:02}".format(int(month))
|
||||
|
||||
|
||||
def _fmt_0d(dom: FieldValue) -> str:
|
||||
return "{0:02}".format(int(dom))
|
||||
|
||||
|
||||
def _fmt_00j(doy: FieldValue) -> str:
|
||||
return "{0:03}".format(int(doy))
|
||||
|
||||
|
||||
def _fmt_0w(week_w: FieldValue) -> str:
|
||||
return "{0:02}".format(int(week_w))
|
||||
|
||||
|
||||
def _fmt_0u(week_u: FieldValue) -> str:
|
||||
return "{0:02}".format(int(week_u))
|
||||
|
||||
|
||||
def _fmt_0v(week_v: FieldValue) -> str:
|
||||
return "{0:02}".format(int(week_v))
|
||||
|
||||
|
||||
FormatterFunc = typ.Callable[[FieldValue], str]
|
||||
|
||||
|
||||
PART_FORMATS: typ.Dict[str, FormatterFunc] = {
|
||||
'YYYY' : _fmt_num,
|
||||
'YY' : _fmt_yy,
|
||||
'0Y' : _fmt_0y,
|
||||
'GGGG' : _fmt_num,
|
||||
'GG' : _fmt_gg,
|
||||
'0G' : _fmt_0g,
|
||||
'Q' : _fmt_num,
|
||||
'MM' : _fmt_num,
|
||||
'0M' : _fmt_0m,
|
||||
'DD' : _fmt_num,
|
||||
'0D' : _fmt_0d,
|
||||
'JJJ' : _fmt_num,
|
||||
'00J' : _fmt_00j,
|
||||
'MAJOR': _fmt_num,
|
||||
'MINOR': _fmt_num,
|
||||
'PATCH': _fmt_num,
|
||||
'BUILD': _fmt_num,
|
||||
'BLD' : _fmt_bld,
|
||||
'TAG' : _fmt_num,
|
||||
'PYTAG': _fmt_num,
|
||||
'NUM' : _fmt_num,
|
||||
'INC0' : _fmt_num,
|
||||
'INC1' : _fmt_num,
|
||||
'WW' : _fmt_num,
|
||||
'0W' : _fmt_0w,
|
||||
'UU' : _fmt_num,
|
||||
'0U' : _fmt_0u,
|
||||
'VV' : _fmt_num,
|
||||
'0V' : _fmt_0v,
|
||||
}
|
||||
|
||||
|
||||
def _convert_to_pep440(version_pattern: str) -> str:
|
||||
# NOTE (mb 2020-09-20): This does not support some
|
||||
# corner cases as specified in PEP440, in particular
|
||||
# related to post and dev releases.
|
||||
|
||||
pep440_pattern = version_pattern
|
||||
|
||||
if pep440_pattern.startswith("v"):
|
||||
pep440_pattern = pep440_pattern[1:]
|
||||
|
||||
pep440_pattern = pep440_pattern.replace(r"\[", "")
|
||||
pep440_pattern = pep440_pattern.replace(r"\]", "")
|
||||
|
||||
pep440_pattern, _ = re.subn(r"[^a-zA-Z0-9\.\[\]]", "", pep440_pattern)
|
||||
|
||||
part_names = list(PATTERN_PART_FIELDS.keys())
|
||||
part_names.sort(key=len, reverse=True)
|
||||
|
||||
for part_name in part_names:
|
||||
if part_name not in version_pattern:
|
||||
continue
|
||||
if part_name not in PEP440_PART_SUBSTITUTIONS:
|
||||
continue
|
||||
|
||||
substitution = PEP440_PART_SUBSTITUTIONS[part_name]
|
||||
if substitution in pep440_pattern:
|
||||
continue
|
||||
|
||||
is_numerical_part = part_name not in ('TAG', 'PYTAG')
|
||||
if is_numerical_part:
|
||||
part_index = pep440_pattern.find(part_name)
|
||||
is_zero_truncation_part = part_index == 0 or pep440_pattern[part_index - 1] == "."
|
||||
if is_zero_truncation_part:
|
||||
pep440_pattern = pep440_pattern.replace(part_name, substitution)
|
||||
else:
|
||||
pep440_pattern = pep440_pattern.replace(part_name, substitution)
|
||||
|
||||
# PYTAG and NUM must be adjacent and also be the last (optional) part
|
||||
if 'PYTAGNUM' not in pep440_pattern:
|
||||
pep440_pattern = pep440_pattern.replace("PYTAG", "")
|
||||
pep440_pattern = pep440_pattern.replace("NUM" , "")
|
||||
pep440_pattern = pep440_pattern.replace("[]" , "")
|
||||
pep440_pattern += "[PYTAGNUM]"
|
||||
|
||||
return pep440_pattern
|
||||
|
||||
|
||||
def normalize_pattern(version_pattern: str, raw_pattern: str) -> str:
|
||||
normalized_pattern = raw_pattern
|
||||
if "{version}" in raw_pattern:
|
||||
normalized_pattern = normalized_pattern.replace("{version}", version_pattern)
|
||||
|
||||
if "{pep440_version}" in normalized_pattern:
|
||||
pep440_version_pattern = _convert_to_pep440(version_pattern)
|
||||
normalized_pattern = normalized_pattern.replace("{pep440_version}", pep440_version_pattern)
|
||||
|
||||
return normalized_pattern
|
||||
|
||||
|
||||
def _replace_pattern_parts(pattern: str) -> str:
|
||||
# The pattern is escaped, so that everything besides the format
|
||||
# string variables is treated literally.
|
||||
while True:
|
||||
new_pattern, _n = re.subn(r"([^\\]|^)\[", r"\1(?:", pattern)
|
||||
new_pattern, _m = re.subn(r"([^\\]|^)\]", r"\1)?" , new_pattern)
|
||||
pattern = new_pattern
|
||||
if _n + _m == 0:
|
||||
break
|
||||
|
||||
SortKey = typ.Tuple[int, int]
|
||||
PostitionedPart = typ.Tuple[int, int, str]
|
||||
part_patterns_by_index: typ.Dict[SortKey, PostitionedPart] = {}
|
||||
|
||||
for part_name, part_pattern in PART_PATTERNS.items():
|
||||
start_idx = pattern.find(part_name)
|
||||
if start_idx >= 0:
|
||||
field = PATTERN_PART_FIELDS[part_name]
|
||||
named_part_pattern = f"(?P<{field}>{part_pattern})"
|
||||
end_idx = start_idx + len(part_name)
|
||||
sort_key = (-end_idx, -len(part_name))
|
||||
part_patterns_by_index[sort_key] = (start_idx, end_idx, named_part_pattern)
|
||||
|
||||
# NOTE (mb 2020-09-17): The sorting is done so that we process items:
|
||||
# - right before left
|
||||
# - longer before shorter
|
||||
last_start_idx = len(pattern) + 1
|
||||
result_pattern = pattern
|
||||
for _, (start_idx, end_idx, named_part_pattern) in sorted(part_patterns_by_index.items()):
|
||||
if end_idx <= last_start_idx:
|
||||
result_pattern = (
|
||||
result_pattern[:start_idx] + named_part_pattern + result_pattern[end_idx:]
|
||||
)
|
||||
last_start_idx = start_idx
|
||||
|
||||
return result_pattern
|
||||
|
||||
|
||||
def _compile_pattern_re(normalized_pattern: str) -> typ.Pattern[str]:
|
||||
escaped_pattern = normalized_pattern
|
||||
for char, escaped in RE_PATTERN_ESCAPES:
|
||||
# [] braces are used for optional parts, such as [-TAG]/[-beta]
|
||||
# and need to be escaped manually.
|
||||
is_semantic_char = char in "[]\\"
|
||||
if not is_semantic_char:
|
||||
# escape it so it is a literal in the re pattern
|
||||
escaped_pattern = escaped_pattern.replace(char, escaped)
|
||||
|
||||
pattern_str = _replace_pattern_parts(escaped_pattern)
|
||||
return re.compile(pattern_str)
|
||||
|
||||
|
||||
@utils.memo
|
||||
def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern:
|
||||
_raw_pattern = version_pattern if raw_pattern is None else raw_pattern
|
||||
normalized_pattern = normalize_pattern(version_pattern, _raw_pattern)
|
||||
regexp = _compile_pattern_re(normalized_pattern)
|
||||
return Pattern(version_pattern, normalized_pattern, regexp)
|
||||
|
||||
|
||||
def compile_patterns(version_pattern: str, raw_patterns: typ.List[str]) -> typ.List[Pattern]:
|
||||
return [compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns]
|
||||
163
src/bumpver/v2rewrite.py
Normal file
163
src/bumpver/v2rewrite.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Rewrite files, updating occurences of version strings."""
|
||||
|
||||
import io
|
||||
import typing as typ
|
||||
import logging
|
||||
|
||||
from . import parse
|
||||
from . import config
|
||||
from . import rewrite
|
||||
from . import version
|
||||
from . import regexfmt
|
||||
from . import v2version
|
||||
from . import v2patterns
|
||||
from .patterns import Pattern
|
||||
|
||||
logger = logging.getLogger("bumpver.v2rewrite")
|
||||
|
||||
|
||||
def rewrite_lines(
|
||||
patterns : typ.List[Pattern],
|
||||
new_vinfo: version.V2VersionInfo,
|
||||
old_lines: typ.List[str],
|
||||
) -> typ.List[str]:
|
||||
"""Replace occurances of patterns in old_lines with new_vinfo."""
|
||||
found_patterns: typ.Set[Pattern] = set()
|
||||
|
||||
new_lines = old_lines[:]
|
||||
for match in parse.iter_matches(old_lines, patterns):
|
||||
found_patterns.add(match.pattern)
|
||||
normalized_pattern = v2patterns.normalize_pattern(
|
||||
match.pattern.version_pattern, match.pattern.raw_pattern
|
||||
)
|
||||
replacement = v2version.format_version(new_vinfo, normalized_pattern)
|
||||
span_l, span_r = match.span
|
||||
new_line = match.line[:span_l] + replacement + match.line[span_r:]
|
||||
new_lines[match.lineno] = new_line
|
||||
|
||||
non_matched_patterns = set(patterns) - found_patterns
|
||||
if non_matched_patterns:
|
||||
for nmp in non_matched_patterns:
|
||||
logger.error(f"No match for pattern '{nmp.raw_pattern}'")
|
||||
msg = (
|
||||
"\n# "
|
||||
+ regexfmt.regex101_url(nmp.regexp.pattern)
|
||||
+ "\nregex = "
|
||||
+ regexfmt.pyexpr_regex(nmp.regexp.pattern)
|
||||
)
|
||||
logger.error(msg)
|
||||
raise rewrite.NoPatternMatch("Invalid pattern(s)")
|
||||
else:
|
||||
return new_lines
|
||||
|
||||
|
||||
def rfd_from_content(
|
||||
patterns : typ.List[Pattern],
|
||||
new_vinfo: version.V2VersionInfo,
|
||||
content : str,
|
||||
path : str = "<path>",
|
||||
) -> rewrite.RewrittenFileData:
|
||||
r"""Rewrite pattern occurrences with version string.
|
||||
|
||||
>>> from .v2patterns import compile_pattern
|
||||
>>> version_pattern = "vYYYY0M.BUILD[-TAG]"
|
||||
>>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern)
|
||||
>>> patterns = [compile_pattern(version_pattern, '__version__ = "vYYYY0M.BUILD[-TAG]"')]
|
||||
>>> content = '__version__ = "v201809.0001-alpha"'
|
||||
>>> rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||
>>> rfd.new_lines
|
||||
['__version__ = "v201809.0123"']
|
||||
|
||||
>>> version_pattern = "vMAJOR.MINOR.PATCH"
|
||||
>>> new_vinfo = v2version.parse_version_info("v1.2.3", version_pattern)
|
||||
>>> patterns = [compile_pattern(version_pattern, '__version__ = "vMAJOR.MINOR.PATCH"')]
|
||||
>>> content = '__version__ = "v1.2.2"'
|
||||
>>> rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||
>>> rfd.new_lines
|
||||
['__version__ = "v1.2.3"']
|
||||
"""
|
||||
line_sep = rewrite.detect_line_sep(content)
|
||||
old_lines = content.split(line_sep)
|
||||
new_lines = rewrite_lines(patterns, new_vinfo, old_lines)
|
||||
return rewrite.RewrittenFileData(path, line_sep, old_lines, new_lines)
|
||||
|
||||
|
||||
def _patterns_with_change(
|
||||
old_vinfo: version.V2VersionInfo, new_vinfo: version.V2VersionInfo, patterns: typ.List[Pattern]
|
||||
) -> int:
|
||||
patterns_with_change = 0
|
||||
for pattern in patterns:
|
||||
old_str = v2version.format_version(old_vinfo, pattern.raw_pattern)
|
||||
new_str = v2version.format_version(new_vinfo, pattern.raw_pattern)
|
||||
if old_str != new_str:
|
||||
patterns_with_change += 1
|
||||
return patterns_with_change
|
||||
|
||||
|
||||
def iter_rewritten(
|
||||
file_patterns: config.PatternsByFile,
|
||||
new_vinfo : version.V2VersionInfo,
|
||||
) -> typ.Iterable[rewrite.RewrittenFileData]:
|
||||
"""Iterate over files with version string replaced."""
|
||||
|
||||
fobj: typ.IO[str]
|
||||
|
||||
for file_path, patterns in rewrite.iter_path_patterns_items(file_patterns):
|
||||
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
||||
content = fobj.read()
|
||||
|
||||
rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||
yield rfd._replace(path=str(file_path))
|
||||
|
||||
|
||||
def diff(
|
||||
old_vinfo : version.V2VersionInfo,
|
||||
new_vinfo : version.V2VersionInfo,
|
||||
file_patterns: config.PatternsByFile,
|
||||
) -> str:
|
||||
r"""Generate diffs of rewritten files."""
|
||||
|
||||
full_diff = ""
|
||||
fobj: typ.IO[str]
|
||||
|
||||
for file_path, patterns in sorted(rewrite.iter_path_patterns_items(file_patterns)):
|
||||
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
||||
content = fobj.read()
|
||||
|
||||
try:
|
||||
rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||
except rewrite.NoPatternMatch:
|
||||
# pylint:disable=raise-missing-from ; we support py2, so not an option
|
||||
errmsg = f"No patterns matched for file '{file_path}'"
|
||||
raise rewrite.NoPatternMatch(errmsg)
|
||||
|
||||
rfd = rfd._replace(path=str(file_path))
|
||||
lines = rewrite.diff_lines(rfd)
|
||||
|
||||
patterns_with_change = _patterns_with_change(old_vinfo, new_vinfo, patterns)
|
||||
if len(lines) == 0 and patterns_with_change > 0:
|
||||
errmsg = f"No patterns matched for file '{file_path}'"
|
||||
raise rewrite.NoPatternMatch(errmsg)
|
||||
|
||||
full_diff += "\n".join(lines) + "\n"
|
||||
|
||||
full_diff = full_diff.rstrip("\n")
|
||||
return full_diff
|
||||
|
||||
|
||||
def rewrite_files(
|
||||
file_patterns: config.PatternsByFile,
|
||||
new_vinfo : version.V2VersionInfo,
|
||||
) -> None:
|
||||
"""Rewrite project files, updating each with the new version."""
|
||||
fobj: typ.IO[str]
|
||||
|
||||
for file_data in iter_rewritten(file_patterns, new_vinfo):
|
||||
new_content = file_data.line_sep.join(file_data.new_lines)
|
||||
with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj:
|
||||
fobj.write(new_content)
|
||||
753
src/bumpver/v2version.py
Normal file
753
src/bumpver/v2version.py
Normal file
|
|
@ -0,0 +1,753 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Functions related to version string manipulation."""
|
||||
|
||||
import typing as typ
|
||||
import logging
|
||||
import datetime as dt
|
||||
|
||||
import lexid
|
||||
|
||||
from . import version
|
||||
from . import v2patterns
|
||||
|
||||
logger = logging.getLogger("bumpver.v2version")
|
||||
|
||||
|
||||
CalInfo = typ.Union[version.V2CalendarInfo, version.V2VersionInfo]
|
||||
|
||||
|
||||
def _is_cal_gt(left: CalInfo, right: CalInfo) -> bool:
|
||||
"""Is left > right for non-None fields."""
|
||||
|
||||
lvals = []
|
||||
rvals = []
|
||||
for field in version.V2CalendarInfo._fields:
|
||||
lval = getattr(left , field)
|
||||
rval = getattr(right, field)
|
||||
if not (lval is None or rval is None):
|
||||
lvals.append(lval)
|
||||
rvals.append(rval)
|
||||
|
||||
return lvals > rvals
|
||||
|
||||
|
||||
def _ver_to_cal_info(vinfo: version.V2VersionInfo) -> version.V2CalendarInfo:
|
||||
return version.V2CalendarInfo(
|
||||
vinfo.year_y,
|
||||
vinfo.year_g,
|
||||
vinfo.quarter,
|
||||
vinfo.month,
|
||||
vinfo.dom,
|
||||
vinfo.doy,
|
||||
vinfo.week_w,
|
||||
vinfo.week_u,
|
||||
vinfo.week_v,
|
||||
)
|
||||
|
||||
|
||||
def cal_info(date: dt.date = None) -> version.V2CalendarInfo:
|
||||
"""Generate calendar components for current date.
|
||||
|
||||
>>> import datetime as dt
|
||||
|
||||
>>> c = cal_info(dt.date(2019, 1, 5))
|
||||
>>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.week_w, c.week_u, c.week_v)
|
||||
(2019, 1, 1, 5, 5, 0, 0, 1)
|
||||
|
||||
>>> c = cal_info(dt.date(2019, 1, 6))
|
||||
>>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.week_w, c.week_u, c.week_v)
|
||||
(2019, 1, 1, 6, 6, 0, 1, 1)
|
||||
|
||||
>>> c = cal_info(dt.date(2019, 1, 7))
|
||||
>>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.week_w, c.week_u, c.week_v)
|
||||
(2019, 1, 1, 7, 7, 1, 1, 2)
|
||||
|
||||
>>> c = cal_info(dt.date(2019, 4, 7))
|
||||
>>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.week_w, c.week_u, c.week_v)
|
||||
(2019, 2, 4, 7, 97, 13, 14, 14)
|
||||
"""
|
||||
if date is None:
|
||||
date = version.TODAY
|
||||
|
||||
kwargs = {
|
||||
'year_y' : date.year,
|
||||
'year_g' : int(date.strftime("%G"), base=10),
|
||||
'quarter': version.quarter_from_month(date.month),
|
||||
'month' : date.month,
|
||||
'dom' : date.day,
|
||||
'doy' : int(date.strftime("%j"), base=10),
|
||||
'week_w' : int(date.strftime("%W"), base=10),
|
||||
'week_u' : int(date.strftime("%U"), base=10),
|
||||
'week_v' : int(date.strftime("%V"), base=10),
|
||||
}
|
||||
|
||||
return version.V2CalendarInfo(**kwargs)
|
||||
|
||||
|
||||
VALID_FIELD_KEYS = set(version.V2VersionInfo._fields) | {'version'}
|
||||
|
||||
MaybeInt = typ.Optional[int]
|
||||
|
||||
FieldKey = str
|
||||
MatchGroupKey = str
|
||||
MatchGroupStr = str
|
||||
|
||||
PatternGroups = typ.Dict[FieldKey, MatchGroupStr]
|
||||
FieldValues = typ.Dict[FieldKey, MatchGroupStr]
|
||||
|
||||
VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]
|
||||
|
||||
|
||||
def parse_field_values_to_cinfo(field_values: FieldValues) -> version.V2CalendarInfo:
|
||||
"""Parse normalized V2CalendarInfo from groups of a matched pattern.
|
||||
|
||||
>>> cinfo = parse_field_values_to_cinfo({'year_y': "2021", 'week_w': "02"})
|
||||
>>> (cinfo.year_y, cinfo.week_w)
|
||||
(2021, 2)
|
||||
>>> cinfo = parse_field_values_to_cinfo({'year_y': "2021", 'week_u': "02"})
|
||||
>>> (cinfo.year_y, cinfo.week_u)
|
||||
(2021, 2)
|
||||
>>> cinfo = parse_field_values_to_cinfo({'year_g': "2021", 'week_v': "02"})
|
||||
>>> (cinfo.year_g, cinfo.week_v)
|
||||
(2021, 2)
|
||||
|
||||
>>> cinfo = parse_field_values_to_cinfo({'year_y': "2021", 'month': "01", 'dom': "03"})
|
||||
>>> (cinfo.year_y, cinfo.month, cinfo.dom)
|
||||
(2021, 1, 3)
|
||||
>>> (cinfo.year_y, cinfo.week_w, cinfo.year_y, cinfo.week_u,cinfo.year_g, cinfo.week_v)
|
||||
(2021, 0, 2021, 1, 2020, 53)
|
||||
"""
|
||||
fvals = field_values
|
||||
date: typ.Optional[dt.date] = None
|
||||
|
||||
year_y: MaybeInt = int(fvals['year_y']) if 'year_y' in fvals else None
|
||||
year_g: MaybeInt = int(fvals['year_g']) if 'year_g' in fvals else None
|
||||
|
||||
if year_y is not None and year_y < 1000:
|
||||
year_y += 2000
|
||||
if year_g is not None and year_g < 1000:
|
||||
year_g += 2000
|
||||
|
||||
month: MaybeInt = int(fvals['month']) if 'month' in fvals else None
|
||||
doy : MaybeInt = int(fvals['doy' ]) if 'doy' in fvals else None
|
||||
dom : MaybeInt = int(fvals['dom' ]) if 'dom' in fvals else None
|
||||
|
||||
week_w: MaybeInt = int(fvals['week_w']) if 'week_w' in fvals else None
|
||||
week_u: MaybeInt = int(fvals['week_u']) if 'week_u' in fvals else None
|
||||
week_v: MaybeInt = int(fvals['week_v']) if 'week_v' in fvals else None
|
||||
|
||||
if year_y and doy:
|
||||
date = version.date_from_doy(year_y, doy)
|
||||
month = date.month
|
||||
dom = date.day
|
||||
else:
|
||||
month = int(fvals['month']) if 'month' in fvals else None
|
||||
dom = int(fvals['dom' ]) if 'dom' in fvals else None
|
||||
|
||||
if year_y and month and dom:
|
||||
date = dt.date(year_y, month, dom)
|
||||
|
||||
if date:
|
||||
# derive all fields from other previous values
|
||||
year_y = int(date.strftime("%Y"), base=10)
|
||||
year_g = int(date.strftime("%G"), base=10)
|
||||
month = int(date.strftime("%m"), base=10)
|
||||
dom = int(date.strftime("%d"), base=10)
|
||||
doy = int(date.strftime("%j"), base=10)
|
||||
week_w = int(date.strftime("%W"), base=10)
|
||||
week_u = int(date.strftime("%U"), base=10)
|
||||
week_v = int(date.strftime("%V"), base=10)
|
||||
|
||||
quarter = int(fvals['quarter']) if 'quarter' in fvals else None
|
||||
if quarter is None and month:
|
||||
quarter = version.quarter_from_month(month)
|
||||
|
||||
return version.V2CalendarInfo(
|
||||
year_y=year_y,
|
||||
year_g=year_g,
|
||||
quarter=quarter,
|
||||
month=month,
|
||||
dom=dom,
|
||||
doy=doy,
|
||||
week_w=week_w,
|
||||
week_u=week_u,
|
||||
week_v=week_v,
|
||||
)
|
||||
|
||||
|
||||
def parse_field_values_to_vinfo(field_values: FieldValues) -> version.V2VersionInfo:
|
||||
"""Parse normalized V2VersionInfo from groups of a matched pattern.
|
||||
|
||||
>>> vinfo = parse_field_values_to_vinfo({'year_y': "2018", 'month': "11", 'bid': "0099"})
|
||||
>>> (vinfo.year_y, vinfo.month, vinfo.quarter, vinfo.bid, vinfo.tag)
|
||||
(2018, 11, 4, '0099', 'final')
|
||||
|
||||
>>> vinfo = parse_field_values_to_vinfo({'year_y': "18", 'month': "11"})
|
||||
>>> (vinfo.year_y, vinfo.month, vinfo.quarter)
|
||||
(2018, 11, 4)
|
||||
|
||||
>>> vinfo = parse_field_values_to_vinfo({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"})
|
||||
>>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy, vinfo.bid, vinfo.tag)
|
||||
(2018, 1, 11, 11, '099', 'beta')
|
||||
|
||||
>>> vinfo = parse_field_values_to_vinfo({'year_y': "2018", 'month': "6", 'dom': "15"})
|
||||
>>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy)
|
||||
(2018, 6, 15, 166)
|
||||
|
||||
>>> vinfo = parse_field_values_to_vinfo({'major': "1", 'minor': "23", 'patch': "45"})
|
||||
>>> (vinfo.major, vinfo.minor, vinfo.patch)
|
||||
(1, 23, 45)
|
||||
|
||||
>>> vinfo = parse_field_values_to_vinfo({'major': "1", 'minor': "023", 'patch': "0045"})
|
||||
>>> (vinfo.major, vinfo.minor, vinfo.patch, vinfo.tag)
|
||||
(1, 23, 45, 'final')
|
||||
"""
|
||||
# pylint:disable=dangerous-default-value; We don't mutate args, mypy would fail if we did.
|
||||
for key in field_values:
|
||||
assert key in VALID_FIELD_KEYS, key
|
||||
|
||||
cinfo = parse_field_values_to_cinfo(field_values)
|
||||
|
||||
fvals = field_values
|
||||
|
||||
tag = fvals.get('tag' ) or ""
|
||||
pytag = fvals.get('pytag') or ""
|
||||
|
||||
if tag and not pytag:
|
||||
pytag = version.PEP440_TAG_BY_TAG[tag]
|
||||
elif pytag and not tag:
|
||||
tag = version.TAG_BY_PEP440_TAG[pytag]
|
||||
|
||||
if not tag:
|
||||
tag = "final"
|
||||
|
||||
# NOTE (mb 2020-09-18): If a part is optional, fvals[<field>] may be None
|
||||
major = int(fvals.get('major') or 0)
|
||||
minor = int(fvals.get('minor') or 0)
|
||||
patch = int(fvals.get('patch') or 0)
|
||||
num = int(fvals.get('num' ) or 0)
|
||||
bid = fvals['bid'] if 'bid' in fvals else "1000"
|
||||
inc0 = int(fvals.get('inc0') or 0)
|
||||
inc1 = int(fvals.get('inc1') or 1)
|
||||
|
||||
return version.V2VersionInfo(
|
||||
year_y=cinfo.year_y,
|
||||
year_g=cinfo.year_g,
|
||||
quarter=cinfo.quarter,
|
||||
month=cinfo.month,
|
||||
dom=cinfo.dom,
|
||||
doy=cinfo.doy,
|
||||
week_w=cinfo.week_w,
|
||||
week_u=cinfo.week_u,
|
||||
week_v=cinfo.week_v,
|
||||
major=major,
|
||||
minor=minor,
|
||||
patch=patch,
|
||||
num=num,
|
||||
bid=bid,
|
||||
tag=tag,
|
||||
pytag=pytag,
|
||||
inc0=inc0,
|
||||
inc1=inc1,
|
||||
)
|
||||
|
||||
|
||||
def parse_version_info(
|
||||
version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG]"
|
||||
) -> version.V2VersionInfo:
|
||||
"""Parse normalized V2VersionInfo.
|
||||
|
||||
>>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG]")
|
||||
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}
|
||||
>>> assert vinfo == parse_field_values_to_vinfo(fvals)
|
||||
|
||||
>>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-TAG]")
|
||||
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"}
|
||||
>>> assert vinfo == parse_field_values_to_vinfo(fvals)
|
||||
|
||||
>>> vinfo = parse_version_info("201712.33b0", raw_pattern="YYYY0M.BLD[PYTAGNUM]")
|
||||
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "33", 'tag': "beta", 'num': 0}
|
||||
>>> assert vinfo == parse_field_values_to_vinfo(fvals)
|
||||
|
||||
>>> vinfo = parse_version_info("1.23.456", raw_pattern="MAJOR.MINOR.PATCH")
|
||||
>>> fvals = {'major': "1", 'minor': "23", 'patch': "456"}
|
||||
>>> assert vinfo == parse_field_values_to_vinfo(fvals)
|
||||
"""
|
||||
pattern = v2patterns.compile_pattern(raw_pattern)
|
||||
match = pattern.regexp.match(version_str)
|
||||
if match is None:
|
||||
err_msg = (
|
||||
f"Invalid version string '{version_str}' "
|
||||
f"for pattern '{raw_pattern}'/'{pattern.regexp.pattern}'"
|
||||
)
|
||||
raise version.PatternError(err_msg)
|
||||
elif len(match.group()) < len(version_str):
|
||||
err_msg = (
|
||||
f"Incomplete match '{match.group()}' for version string '{version_str}' "
|
||||
f"with pattern '{raw_pattern}'/'{pattern.regexp.pattern}'"
|
||||
)
|
||||
raise version.PatternError(err_msg)
|
||||
else:
|
||||
field_values = match.groupdict()
|
||||
return parse_field_values_to_vinfo(field_values)
|
||||
|
||||
|
||||
def is_valid(version_str: str, raw_pattern: str = "vYYYY.BUILD[-TAG]") -> bool:
|
||||
"""Check if a version matches a pattern.
|
||||
|
||||
>>> is_valid("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG]")
|
||||
True
|
||||
>>> is_valid("v201712.0033-beta", raw_pattern="MAJOR.MINOR.PATCH")
|
||||
False
|
||||
>>> is_valid("1.2.3", raw_pattern="MAJOR.MINOR.PATCH")
|
||||
True
|
||||
>>> is_valid("v201712.0033-beta", raw_pattern="MAJOR.MINOR.PATCH")
|
||||
False
|
||||
"""
|
||||
try:
|
||||
parse_version_info(version_str, raw_pattern)
|
||||
return True
|
||||
except version.PatternError:
|
||||
return False
|
||||
|
||||
|
||||
TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]]
|
||||
PartValues = typ.List[typ.Tuple[str, str]]
|
||||
|
||||
|
||||
def _format_part_values(vinfo: version.V2VersionInfo) -> PartValues:
|
||||
"""Generate kwargs for template from minimal V2VersionInfo.
|
||||
|
||||
The V2VersionInfo Tuple only has the minimal representation
|
||||
of a parsed version, not the values suitable for formatting.
|
||||
It may for example have month=9, but not the formatted
|
||||
representation '09' for '0M'.
|
||||
|
||||
>>> vinfo = parse_version_info("v200709.1033-beta", raw_pattern="vYYYY0M.BUILD[-TAG]")
|
||||
>>> kwargs = dict(_format_part_values(vinfo))
|
||||
>>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['TAG'])
|
||||
('2007', '09', '1033', 'beta')
|
||||
>>> (kwargs['YY'], kwargs['0Y'], kwargs['MM'], kwargs['PYTAG'])
|
||||
('7', '07', '9', 'b')
|
||||
|
||||
>>> vinfo = parse_version_info("200709.1033b1", raw_pattern="YYYY0M.BLD[PYTAGNUM]")
|
||||
>>> kwargs = dict(_format_part_values(vinfo))
|
||||
>>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['PYTAG'], kwargs['NUM'])
|
||||
('2007', '09', '1033', 'b', '1')
|
||||
"""
|
||||
vnfo_kwargs: TemplateKwargs = vinfo._asdict()
|
||||
kwargs : typ.Dict[str, str] = {}
|
||||
|
||||
for part, field in v2patterns.PATTERN_PART_FIELDS.items():
|
||||
field_val = vnfo_kwargs[field]
|
||||
if field_val is not None:
|
||||
format_fn = v2patterns.PART_FORMATS[part]
|
||||
kwargs[part] = format_fn(field_val)
|
||||
|
||||
return sorted(kwargs.items(), key=lambda item: -len(item[0]))
|
||||
|
||||
|
||||
Segment = str
|
||||
# mypy limitation wrt. cyclic definition
|
||||
# SegmentTree = typ.List[typ.Union[Segment, "SegmentTree"]]
|
||||
SegmentTree = typ.Any
|
||||
|
||||
|
||||
def _parse_segtree(raw_pattern: str) -> SegmentTree:
|
||||
"""Generate segment tree from pattern string.
|
||||
|
||||
>>> _parse_segtree('aa[bb[cc]]')
|
||||
['aa', ['bb', ['cc']]]
|
||||
>>> _parse_segtree('aa[bb[cc]dd[ee]ff]gg')
|
||||
['aa', ['bb', ['cc'], 'dd', ['ee'], 'ff'], 'gg']
|
||||
"""
|
||||
|
||||
internal_root: SegmentTree = []
|
||||
branch_stack : typ.List[SegmentTree] = [internal_root]
|
||||
segment_start_index = -1
|
||||
|
||||
raw_pattern = "[" + raw_pattern + "]"
|
||||
|
||||
for i, char in enumerate(raw_pattern):
|
||||
is_escaped = i > 0 and raw_pattern[i - 1] == "\\"
|
||||
if char in "[]" and not is_escaped:
|
||||
start = segment_start_index + 1
|
||||
end = i
|
||||
if start < end:
|
||||
branch_stack[-1].append(raw_pattern[start:end])
|
||||
|
||||
if char == "[":
|
||||
new_branch: SegmentTree = []
|
||||
branch_stack[-1].append(new_branch)
|
||||
branch_stack.append(new_branch)
|
||||
segment_start_index = i
|
||||
elif char == "]":
|
||||
if len(branch_stack) == 1:
|
||||
err = f"Unbalanced brace(s) in '{raw_pattern}'"
|
||||
raise ValueError(err)
|
||||
|
||||
branch_stack.pop()
|
||||
segment_start_index = i
|
||||
else:
|
||||
raise NotImplementedError("Unreachable")
|
||||
|
||||
if len(branch_stack) > 1:
|
||||
err = f"Unclosed brace in '{raw_pattern}'"
|
||||
raise ValueError(err)
|
||||
|
||||
return internal_root[0]
|
||||
|
||||
|
||||
FormattedSegmentParts = typ.List[str]
|
||||
|
||||
|
||||
class FormatedSeg(typ.NamedTuple):
|
||||
is_literal: bool
|
||||
is_zero : bool
|
||||
result : str
|
||||
|
||||
|
||||
def _format_segment(seg: Segment, part_values: PartValues) -> FormatedSeg:
|
||||
zero_part_count = 0
|
||||
|
||||
# find all parts, regardless of zero value
|
||||
used_parts: typ.List[typ.Tuple[str, str]] = []
|
||||
|
||||
for part, part_value in part_values:
|
||||
if part in seg:
|
||||
used_parts.append((part, part_value))
|
||||
if version.is_zero_val(part, part_value):
|
||||
zero_part_count += 1
|
||||
|
||||
result = seg
|
||||
# unescape braces
|
||||
result = result.replace(r"\[", r"[")
|
||||
result = result.replace(r"\]", r"]")
|
||||
|
||||
for part, part_value in used_parts:
|
||||
result = result.replace(part, part_value)
|
||||
|
||||
# If a segment has no parts at all, it is a literal string
|
||||
# (typically a prefix or sufix) and should be output as is.
|
||||
is_literal_seg = len(used_parts) == 0
|
||||
if is_literal_seg:
|
||||
return FormatedSeg(True, False, result)
|
||||
elif zero_part_count > 0 and zero_part_count == len(used_parts):
|
||||
# all zero, omit segment completely
|
||||
return FormatedSeg(False, True, result)
|
||||
else:
|
||||
return FormatedSeg(False, False, result)
|
||||
|
||||
|
||||
def _format_segment_tree(
|
||||
segtree : SegmentTree,
|
||||
part_values: PartValues,
|
||||
) -> FormatedSeg:
|
||||
# NOTE (mb 2020-10-02): starting from the right, if there is any non-zero
|
||||
# part, all further parts going left will be used. In other words, a part
|
||||
# is only omitted, if all parts to the right of it were also omitted.
|
||||
result_parts: typ.List[str] = []
|
||||
is_zero = True
|
||||
for seg in segtree:
|
||||
if isinstance(seg, list):
|
||||
formatted_seg = _format_segment_tree(seg, part_values)
|
||||
else:
|
||||
formatted_seg = _format_segment(seg, part_values)
|
||||
|
||||
if formatted_seg.is_literal:
|
||||
result_parts.append(formatted_seg.result)
|
||||
else:
|
||||
is_zero = is_zero and formatted_seg.is_zero
|
||||
result_parts.append(formatted_seg.result)
|
||||
|
||||
result = "" if is_zero else "".join(result_parts)
|
||||
return FormatedSeg(False, is_zero, result)
|
||||
|
||||
|
||||
def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str:
|
||||
"""Generate version string.
|
||||
|
||||
>>> import datetime as dt
|
||||
>>> vinfo = parse_version_info("v200712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG]")
|
||||
>>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2007, 1, 1))._asdict())
|
||||
>>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2007, 12, 31))._asdict())
|
||||
|
||||
>>> format_version(vinfo_a, raw_pattern="vYY.BLD[-PYTAGNUM]")
|
||||
'v7.33-b0'
|
||||
|
||||
>>> format_version(vinfo_a, raw_pattern="vYY.BLD[-PYTAGNUM]")
|
||||
'v7.33-b0'
|
||||
>>> format_version(vinfo_a, raw_pattern="YYYY0M.BUILD[PYTAG[NUM]]")
|
||||
'200701.0033b'
|
||||
>>> format_version(vinfo_a, raw_pattern="v0Y.BLD[-TAG]")
|
||||
'v07.33-beta'
|
||||
|
||||
>>> format_version(vinfo_a, raw_pattern="vYYYY0M.BUILD[-TAG]")
|
||||
'v200701.0033-beta'
|
||||
>>> format_version(vinfo_b, raw_pattern="vYYYY0M.BUILD[-TAG]")
|
||||
'v200712.0033-beta'
|
||||
|
||||
>>> format_version(vinfo_a, raw_pattern="vYYYYw0W.BUILD[-TAG]")
|
||||
'v2007w01.0033-beta'
|
||||
>>> format_version(vinfo_a, raw_pattern="vYYYYwWW.BLD[-TAG]")
|
||||
'v2007w1.33-beta'
|
||||
>>> format_version(vinfo_b, raw_pattern="vYYYYw0W.BUILD[-TAG]")
|
||||
'v2007w53.0033-beta'
|
||||
|
||||
>>> format_version(vinfo_a, raw_pattern="vYYYYd00J.BUILD[-TAG]")
|
||||
'v2007d001.0033-beta'
|
||||
>>> format_version(vinfo_a, raw_pattern="vYYYYdJJJ.BUILD[-TAG]")
|
||||
'v2007d1.0033-beta'
|
||||
>>> format_version(vinfo_b, raw_pattern="vYYYYd00J.BUILD[-TAG]")
|
||||
'v2007d365.0033-beta'
|
||||
|
||||
>>> format_version(vinfo_a, raw_pattern="vGGGGwVV.BLD[PYTAGNUM]")
|
||||
'v2007w1.33b0'
|
||||
>>> format_version(vinfo_a, raw_pattern="vGGGGw0V.BUILD[-TAG]")
|
||||
'v2007w01.0033-beta'
|
||||
>>> format_version(vinfo_b, raw_pattern="vGGGGw0V.BUILD[-TAG]")
|
||||
'v2008w01.0033-beta'
|
||||
|
||||
>>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final')
|
||||
|
||||
>>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD-TAG")
|
||||
'v2007w53.0033-final'
|
||||
>>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD[-TAG]")
|
||||
'v2007w53.0033'
|
||||
|
||||
>>> format_version(vinfo_c, raw_pattern="vMAJOR.MINOR.PATCH")
|
||||
'v1.2.34'
|
||||
|
||||
>>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final')
|
||||
>>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-TAGNUM")
|
||||
'v1.0.0-final0'
|
||||
>>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-TAG")
|
||||
'v1.0.0-final'
|
||||
>>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-TAG")
|
||||
'v1.0.0-final'
|
||||
>>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH[-TAG]")
|
||||
'v1.0.0'
|
||||
>>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR[.PATCH[-TAG]]")
|
||||
'v1.0'
|
||||
>>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]")
|
||||
'v1'
|
||||
|
||||
>>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=2, tag='rc', pytag='rc', num=0)
|
||||
>>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH]]")
|
||||
'v1.0.2'
|
||||
>>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]")
|
||||
'v1.0.2-rc'
|
||||
>>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[PYTAGNUM]]]")
|
||||
'v1.0.2rc0'
|
||||
>>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH]]")
|
||||
'v1.0.2'
|
||||
|
||||
>>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2)
|
||||
>>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-TAGNUM]]]")
|
||||
'v1.0.0-rc2'
|
||||
|
||||
>>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2)
|
||||
>>> format_version(vinfo_d, raw_pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-TAGNUM]]]"')
|
||||
'__version__ = "v1.0.0-rc2"'
|
||||
"""
|
||||
part_values = _format_part_values(vinfo)
|
||||
segtree = _parse_segtree(raw_pattern)
|
||||
formatted_seg = _format_segment_tree(segtree, part_values)
|
||||
return formatted_seg.result
|
||||
|
||||
|
||||
def _iter_flat_segtree(segtree: SegmentTree) -> typ.Iterable[Segment]:
|
||||
"""Flatten a SegmentTree (mixed nested list of lists or str).
|
||||
|
||||
>>> list(_iter_flat_segtree(['aa', ['bb', ['cc'], 'dd', ['ee'], 'ff'], 'gg']))
|
||||
['aa', 'bb', 'cc', 'dd', 'ee', 'ff', 'gg']
|
||||
"""
|
||||
for subtree in segtree:
|
||||
if isinstance(subtree, list):
|
||||
for seg in _iter_flat_segtree(subtree):
|
||||
yield seg
|
||||
else:
|
||||
yield subtree
|
||||
|
||||
|
||||
def _parse_pattern_fields(raw_pattern: str) -> typ.List[str]:
|
||||
parts = list(v2patterns.PATTERN_PART_FIELDS.keys())
|
||||
parts.sort(key=len, reverse=True)
|
||||
|
||||
segtree = _parse_segtree(raw_pattern)
|
||||
segments = _iter_flat_segtree(segtree)
|
||||
|
||||
fields_by_index = {}
|
||||
for segment_index, segment in enumerate(segments):
|
||||
for part in parts:
|
||||
part_index = segment.find(part)
|
||||
if part_index >= 0:
|
||||
field = v2patterns.PATTERN_PART_FIELDS[part]
|
||||
fields_by_index[segment_index, part_index] = field
|
||||
|
||||
return [field for _, field in sorted(fields_by_index.items())]
|
||||
|
||||
|
||||
def _iter_reset_field_items(
|
||||
fields : typ.List[str],
|
||||
old_vinfo: version.V2VersionInfo,
|
||||
cur_vinfo: version.V2VersionInfo,
|
||||
) -> typ.Iterable[typ.Tuple[str, str]]:
|
||||
# Any field to the left of another can reset all to the right
|
||||
has_reset = False
|
||||
for field in fields:
|
||||
initial_val = version.V2_FIELD_INITIAL_VALUES.get(field)
|
||||
if has_reset and initial_val is not None:
|
||||
yield field, initial_val
|
||||
elif getattr(old_vinfo, field) != getattr(cur_vinfo, field):
|
||||
has_reset = True
|
||||
|
||||
|
||||
def _incr_numeric(
|
||||
raw_pattern: str,
|
||||
old_vinfo : version.V2VersionInfo,
|
||||
cur_vinfo : version.V2VersionInfo,
|
||||
major : bool,
|
||||
minor : bool,
|
||||
patch : bool,
|
||||
tag : typ.Optional[str],
|
||||
tag_num : bool,
|
||||
) -> version.V2VersionInfo:
|
||||
"""Increment (and reset to zero) non CalVer parts.
|
||||
|
||||
>>> raw_pattern = 'MAJOR.MINOR.PATCH[PYTAGNUM]'
|
||||
>>> old_vinfo = parse_field_values_to_vinfo({'major': "1", 'minor': "2", 'patch': "3"})
|
||||
>>> cur_vinfo = old_vinfo
|
||||
>>> new_vinfo = _incr_numeric(
|
||||
... raw_pattern,
|
||||
... cur_vinfo,
|
||||
... old_vinfo,
|
||||
... major=False,
|
||||
... minor=False,
|
||||
... patch=True,
|
||||
... tag='beta',
|
||||
... tag_num=False,
|
||||
... )
|
||||
>>> (new_vinfo.major, new_vinfo.minor, new_vinfo.patch, new_vinfo.tag, new_vinfo.pytag, new_vinfo.num)
|
||||
(1, 2, 4, 'beta', 'b', 0)
|
||||
"""
|
||||
# Reset major/minor/patch/num/inc to zero if any part to the left of it is incremented
|
||||
fields = _parse_pattern_fields(raw_pattern)
|
||||
reset_fields = dict(_iter_reset_field_items(fields, old_vinfo, cur_vinfo))
|
||||
|
||||
cur_kwargs = cur_vinfo._asdict()
|
||||
cur_kwargs.update(reset_fields)
|
||||
cur_vinfo = version.V2VersionInfo(**cur_kwargs)
|
||||
|
||||
# prevent truncation of leading zeros
|
||||
if int(cur_vinfo.bid) < 1000:
|
||||
cur_vinfo = cur_vinfo._replace(bid=str(int(cur_vinfo.bid) + 1000))
|
||||
|
||||
cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid))
|
||||
|
||||
if 'inc0' in reset_fields:
|
||||
cur_vinfo = cur_vinfo._replace(inc0=0)
|
||||
else:
|
||||
cur_vinfo = cur_vinfo._replace(inc0=cur_vinfo.inc0 + 1)
|
||||
|
||||
if 'inc1' in reset_fields:
|
||||
cur_vinfo = cur_vinfo._replace(inc1=1)
|
||||
else:
|
||||
cur_vinfo = cur_vinfo._replace(inc1=cur_vinfo.inc1 + 1)
|
||||
|
||||
if major and 'major' not in reset_fields:
|
||||
cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0)
|
||||
if minor and 'minor' not in reset_fields:
|
||||
cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0)
|
||||
if patch and 'patch' not in reset_fields:
|
||||
cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1)
|
||||
if tag_num and 'tag_num' not in reset_fields:
|
||||
cur_vinfo = cur_vinfo._replace(num=cur_vinfo.num + 1)
|
||||
if tag and 'tag' not in reset_fields:
|
||||
if tag != cur_vinfo.tag:
|
||||
cur_vinfo = cur_vinfo._replace(num=0)
|
||||
cur_vinfo = cur_vinfo._replace(tag=tag)
|
||||
|
||||
if cur_vinfo.tag and not cur_vinfo.pytag:
|
||||
pytag = version.PEP440_TAG_BY_TAG[cur_vinfo.tag]
|
||||
cur_vinfo = cur_vinfo._replace(pytag=pytag)
|
||||
elif cur_vinfo.pytag and not cur_vinfo.tag:
|
||||
tag = version.TAG_BY_PEP440_TAG[cur_vinfo.pytag]
|
||||
cur_vinfo = cur_vinfo._replace(tag=tag)
|
||||
|
||||
return cur_vinfo
|
||||
|
||||
|
||||
def is_valid_week_pattern(raw_pattern: str) -> bool:
|
||||
has_yy_part = any(part in raw_pattern for part in ["YYYY", "YY", "0Y"])
|
||||
has_ww_part = any(part in raw_pattern for part in ["WW" , "0W", "UU", "0U"])
|
||||
has_gg_part = any(part in raw_pattern for part in ["GGGG", "GG", "0G"])
|
||||
has_vv_part = any(part in raw_pattern for part in ["VV" , "0V"])
|
||||
if has_yy_part and has_vv_part:
|
||||
alt1 = raw_pattern.replace("V", "W")
|
||||
alt2 = raw_pattern.replace("Y", "G")
|
||||
logger.error(f"Invalid pattern: '{raw_pattern}'. Maybe try {alt1} or {alt2}")
|
||||
return False
|
||||
elif has_gg_part and has_ww_part:
|
||||
alt1 = raw_pattern.replace("W", "V").replace("U", "V")
|
||||
alt2 = raw_pattern.replace("G", "Y")
|
||||
logger.error(f"Invalid pattern: '{raw_pattern}'. Maybe try {alt1} or {alt2}")
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def incr(
|
||||
old_version: str,
|
||||
raw_pattern: str = "vYYYY0M.BUILD[-TAG]",
|
||||
*,
|
||||
major : bool = False,
|
||||
minor : bool = False,
|
||||
patch : bool = False,
|
||||
tag : typ.Optional[str] = None,
|
||||
tag_num : bool = False,
|
||||
pin_date: bool = False,
|
||||
date : typ.Optional[dt.date] = None,
|
||||
) -> typ.Optional[str]:
|
||||
"""Increment version string.
|
||||
|
||||
'old_version' is assumed to be a string that matches 'raw_pattern'
|
||||
"""
|
||||
if not is_valid_week_pattern(raw_pattern):
|
||||
return None
|
||||
|
||||
try:
|
||||
old_vinfo = parse_version_info(old_version, raw_pattern)
|
||||
except version.PatternError as ex:
|
||||
logger.error(str(ex))
|
||||
return None
|
||||
|
||||
cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info(date)
|
||||
|
||||
if _is_cal_gt(old_vinfo, cur_cinfo):
|
||||
logger.warning(f"Old version appears to be from the future '{old_version}'")
|
||||
cur_vinfo = old_vinfo
|
||||
else:
|
||||
cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict())
|
||||
|
||||
cur_vinfo = _incr_numeric(
|
||||
raw_pattern,
|
||||
old_vinfo,
|
||||
cur_vinfo,
|
||||
major=major,
|
||||
minor=minor,
|
||||
patch=patch,
|
||||
tag=tag,
|
||||
tag_num=tag_num,
|
||||
)
|
||||
|
||||
new_version = format_version(cur_vinfo, raw_pattern)
|
||||
if new_version == old_version:
|
||||
logger.error("Invalid arguments or pattern, version did not change.")
|
||||
return None
|
||||
else:
|
||||
return new_version
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://gitlab.com/mbarkhau/pycalver
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# pycalver/vcs.py (this file) is based on code from the
|
||||
# bumpver/vcs.py (this file) is based on code from the
|
||||
# bumpversion project: https://github.com/peritus/bumpversion
|
||||
# Copyright (c) 2013-2014 Filip Noetzel - MIT License
|
||||
|
||||
"""Minimal Git and Mercirial API.
|
||||
|
||||
If terminology for similar concepts differs between git and
|
||||
|
|
@ -15,12 +16,16 @@ mercurial, then the git terms are used. For example "fetch"
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shlex
|
||||
import typing as typ
|
||||
import logging
|
||||
import tempfile
|
||||
import subprocess as sp
|
||||
|
||||
logger = logging.getLogger("pycalver.vcs")
|
||||
from . import config
|
||||
|
||||
logger = logging.getLogger("bumpver.vcs")
|
||||
|
||||
|
||||
VCS_SUBCOMMANDS_BY_NAME = {
|
||||
|
|
@ -30,9 +35,9 @@ VCS_SUBCOMMANDS_BY_NAME = {
|
|||
'ls_tags' : "git tag --list",
|
||||
'status' : "git status --porcelain",
|
||||
'add_path' : "git add --update {path}",
|
||||
'commit' : "git commit --file {path}",
|
||||
'commit' : "git commit --message '{message}'",
|
||||
'tag' : "git tag --annotate {tag} --message {tag}",
|
||||
'push_tag' : "git push origin --follow-tags {tag}",
|
||||
'push_tag' : "git push origin --follow-tags {tag} HEAD",
|
||||
'show_remotes': "git config --get remote.origin.url",
|
||||
},
|
||||
'hg': {
|
||||
|
|
@ -70,11 +75,10 @@ class VCSAPI:
|
|||
logger.info(cmd_str)
|
||||
else:
|
||||
logger.debug(cmd_str)
|
||||
output_data: bytes = sp.check_output(cmd_str.split(), env=env, stderr=sp.STDOUT)
|
||||
cmd_parts = shlex.split(cmd_str)
|
||||
output_data: bytes = sp.check_output(cmd_parts, env=env, stderr=sp.STDOUT)
|
||||
|
||||
# TODO (mb 2018-11-15): Detect encoding of output?
|
||||
_encoding = "utf-8"
|
||||
return output_data.decode(_encoding)
|
||||
return output_data.decode("utf-8")
|
||||
|
||||
@property
|
||||
def is_usable(self) -> bool:
|
||||
|
|
@ -96,10 +100,12 @@ class VCSAPI:
|
|||
|
||||
@property
|
||||
def has_remote(self) -> bool:
|
||||
# pylint:disable=broad-except; Not sure how to anticipate all cases.
|
||||
try:
|
||||
output = self('show_remotes')
|
||||
if output.strip() == "":
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
|
@ -139,9 +145,14 @@ class VCSAPI:
|
|||
|
||||
def commit(self, message: str) -> None:
|
||||
"""Commit added files."""
|
||||
message_data = message.encode("utf-8")
|
||||
env: Env = os.environ.copy()
|
||||
|
||||
if self.name == 'git':
|
||||
self('commit', env=env, message=message)
|
||||
else:
|
||||
message_data = message.encode("utf-8")
|
||||
tmp_file = tempfile.NamedTemporaryFile("wb", delete=False)
|
||||
try:
|
||||
assert " " not in tmp_file.name
|
||||
|
||||
fobj: typ.IO[bytes]
|
||||
|
|
@ -149,9 +160,9 @@ class VCSAPI:
|
|||
with tmp_file as fobj:
|
||||
fobj.write(message_data)
|
||||
|
||||
env: Env = os.environ.copy()
|
||||
env['HGENCODING'] = "utf-8"
|
||||
self('commit', env=env, path=tmp_file.name)
|
||||
finally:
|
||||
os.unlink(tmp_file.name)
|
||||
|
||||
def tag(self, tag_name: str) -> None:
|
||||
|
|
@ -179,3 +190,57 @@ def get_vcs_api() -> VCSAPI:
|
|||
return vcs_api
|
||||
|
||||
raise OSError("No such directory .git/ or .hg/ ")
|
||||
|
||||
|
||||
# cli helper methods
|
||||
|
||||
|
||||
def assert_not_dirty(vcs_api: VCSAPI, filepaths: typ.Set[str], allow_dirty: bool) -> None:
|
||||
dirty_files = vcs_api.status(required_files=filepaths)
|
||||
|
||||
if dirty_files:
|
||||
logger.warning(f"{vcs_api.name} working directory is not clean. Uncomitted file(s):")
|
||||
for dirty_file in dirty_files:
|
||||
logger.warning(" " + dirty_file)
|
||||
|
||||
if not allow_dirty and dirty_files:
|
||||
sys.exit(1)
|
||||
|
||||
dirty_pattern_files = set(dirty_files) & filepaths
|
||||
if dirty_pattern_files:
|
||||
logger.error("Not commiting when pattern files are dirty:")
|
||||
for dirty_file in dirty_pattern_files:
|
||||
logger.warning(" " + dirty_file)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def commit(
|
||||
cfg : config.Config,
|
||||
vcs_api : VCSAPI,
|
||||
filepaths : typ.Set[str],
|
||||
new_version : str,
|
||||
commit_message: str,
|
||||
) -> None:
|
||||
for filepath in filepaths:
|
||||
vcs_api.add(filepath)
|
||||
|
||||
vcs_api.commit(commit_message)
|
||||
|
||||
if cfg.commit and cfg.tag:
|
||||
vcs_api.tag(new_version)
|
||||
|
||||
if cfg.commit and cfg.tag and cfg.push:
|
||||
vcs_api.push(new_version)
|
||||
|
||||
|
||||
def get_tags(fetch: bool) -> typ.List[str]:
|
||||
try:
|
||||
vcs_api = get_vcs_api()
|
||||
logger.debug(f"vcs found: {vcs_api.name}")
|
||||
if fetch:
|
||||
logger.info("fetching tags from remote (to turn off use: -n / --no-fetch)")
|
||||
vcs_api.fetch()
|
||||
return vcs_api.ls_tags()
|
||||
except OSError:
|
||||
logger.debug("No vcs found")
|
||||
return []
|
||||
174
src/bumpver/version.py
Normal file
174
src/bumpver/version.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://github.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
import typing as typ
|
||||
import datetime as dt
|
||||
|
||||
import pkg_resources
|
||||
|
||||
MaybeInt = typ.Optional[int]
|
||||
|
||||
|
||||
class V1CalendarInfo(typ.NamedTuple):
|
||||
"""Container for calendar components of version strings."""
|
||||
|
||||
year : MaybeInt
|
||||
quarter : MaybeInt
|
||||
month : MaybeInt
|
||||
dom : MaybeInt
|
||||
doy : MaybeInt
|
||||
iso_week: MaybeInt
|
||||
us_week : MaybeInt
|
||||
|
||||
|
||||
class V1VersionInfo(typ.NamedTuple):
|
||||
"""Container for parsed version string."""
|
||||
|
||||
year : MaybeInt
|
||||
quarter : MaybeInt
|
||||
month : MaybeInt
|
||||
dom : MaybeInt
|
||||
doy : MaybeInt
|
||||
iso_week: MaybeInt
|
||||
us_week : MaybeInt
|
||||
major : int
|
||||
minor : int
|
||||
patch : int
|
||||
bid : str
|
||||
tag : str
|
||||
|
||||
|
||||
class V2CalendarInfo(typ.NamedTuple):
|
||||
"""Container for calendar components of version strings."""
|
||||
|
||||
year_y : MaybeInt
|
||||
year_g : MaybeInt
|
||||
quarter: MaybeInt
|
||||
month : MaybeInt
|
||||
dom : MaybeInt
|
||||
doy : MaybeInt
|
||||
week_w : MaybeInt
|
||||
week_u : MaybeInt
|
||||
week_v : MaybeInt
|
||||
|
||||
|
||||
class V2VersionInfo(typ.NamedTuple):
|
||||
"""Container for parsed version string."""
|
||||
|
||||
year_y : MaybeInt
|
||||
year_g : MaybeInt
|
||||
quarter: MaybeInt
|
||||
month : MaybeInt
|
||||
dom : MaybeInt
|
||||
doy : MaybeInt
|
||||
week_w : MaybeInt
|
||||
week_u : MaybeInt
|
||||
week_v : MaybeInt
|
||||
major : int
|
||||
minor : int
|
||||
patch : int
|
||||
num : int
|
||||
bid : str
|
||||
tag : str
|
||||
pytag : str
|
||||
inc0 : int
|
||||
inc1 : int
|
||||
|
||||
|
||||
# The test suite may replace this.
|
||||
TODAY = dt.datetime.utcnow().date()
|
||||
|
||||
|
||||
TAG_BY_PEP440_TAG = {
|
||||
'a' : 'alpha',
|
||||
'b' : 'beta',
|
||||
'' : 'final',
|
||||
'rc' : 'rc',
|
||||
'dev' : 'dev',
|
||||
'post': 'post',
|
||||
}
|
||||
|
||||
|
||||
PEP440_TAG_BY_TAG = {
|
||||
'a' : 'a',
|
||||
'b' : 'b',
|
||||
'dev' : 'dev',
|
||||
'alpha' : 'a',
|
||||
'beta' : 'b',
|
||||
'preview': 'rc',
|
||||
'pre' : 'rc',
|
||||
'rc' : 'rc',
|
||||
'c' : 'rc',
|
||||
'final' : '',
|
||||
'post' : 'post',
|
||||
'r' : 'post',
|
||||
'rev' : 'post',
|
||||
}
|
||||
|
||||
assert set(TAG_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_TAG.values())
|
||||
assert set(TAG_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_TAG.keys())
|
||||
|
||||
|
||||
PART_ZERO_VALUES = {
|
||||
'MAJOR': "0",
|
||||
'MINOR': "0",
|
||||
'PATCH': "0",
|
||||
'TAG' : "final",
|
||||
'PYTAG': "",
|
||||
'NUM' : "0",
|
||||
'INC0' : "0",
|
||||
}
|
||||
|
||||
|
||||
V2_FIELD_INITIAL_VALUES = {
|
||||
'major': "0",
|
||||
'minor': "0",
|
||||
'patch': "0",
|
||||
'num' : "0",
|
||||
'inc0' : "0",
|
||||
'inc1' : "1",
|
||||
}
|
||||
|
||||
|
||||
def is_zero_val(part: str, part_value: str) -> bool:
|
||||
return part in PART_ZERO_VALUES and part_value == PART_ZERO_VALUES[part]
|
||||
|
||||
|
||||
class PatternError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def date_from_doy(year: int, doy: int) -> dt.date:
|
||||
"""Parse date from year and day of year (1 indexed).
|
||||
|
||||
>>> cases = [
|
||||
... (2016, 1), (2016, 31), (2016, 31 + 1), (2016, 31 + 29), (2016, 31 + 30),
|
||||
... (2017, 1), (2017, 31), (2017, 31 + 1), (2017, 31 + 28), (2017, 31 + 29),
|
||||
... ]
|
||||
>>> dates = [date_from_doy(year, month) for year, month in cases]
|
||||
>>> assert [(d.month, d.day) for d in dates] == [
|
||||
... (1, 1), (1, 31), (2, 1), (2, 29), (3, 1),
|
||||
... (1, 1), (1, 31), (2, 1), (2, 28), (3, 1),
|
||||
... ]
|
||||
"""
|
||||
return dt.date(year, 1, 1) + dt.timedelta(days=doy - 1)
|
||||
|
||||
|
||||
def quarter_from_month(month: int) -> int:
|
||||
"""Calculate quarter (1 indexed) from month (1 indexed).
|
||||
|
||||
>>> [quarter_from_month(month) for month in range(1, 13)]
|
||||
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]
|
||||
"""
|
||||
return ((month - 1) // 3) + 1
|
||||
|
||||
|
||||
def to_pep440(version: str) -> str:
|
||||
"""Derive pep440 compliant version string from PyCalVer version string.
|
||||
|
||||
>>> to_pep440("v201811.0007-beta")
|
||||
'201811.7b0'
|
||||
"""
|
||||
return str(pkg_resources.parse_version(version))
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://gitlab.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""PyCalVer: CalVer for Python Packages."""
|
||||
|
||||
__version__ = "v202007.0036"
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# This file is part of the pycalver project
|
||||
# https://gitlab.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
__main__ module for PyCalVer.
|
||||
|
||||
Enables use as module: $ python -m pycalver --version
|
||||
"""
|
||||
|
||||
if __name__ == '__main__':
|
||||
from . import cli
|
||||
|
||||
cli.cli()
|
||||
|
|
@ -1,380 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# This file is part of the pycalver project
|
||||
# https://gitlab.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
CLI module for PyCalVer.
|
||||
|
||||
Provided subcommands: show, test, init, bump
|
||||
"""
|
||||
import sys
|
||||
import typing as typ
|
||||
import logging
|
||||
import subprocess as sp
|
||||
|
||||
import click
|
||||
|
||||
from . import vcs
|
||||
from . import config
|
||||
from . import rewrite
|
||||
from . import version
|
||||
|
||||
_VERBOSE = 0
|
||||
|
||||
|
||||
try:
|
||||
import pretty_traceback
|
||||
|
||||
pretty_traceback.install()
|
||||
except ImportError:
|
||||
pass # no need to fail because of missing dev dependency
|
||||
|
||||
|
||||
click.disable_unicode_literals_warning = True
|
||||
|
||||
|
||||
VALID_RELEASE_VALUES = ("alpha", "beta", "dev", "rc", "post", "final")
|
||||
|
||||
|
||||
logger = logging.getLogger("pycalver.cli")
|
||||
|
||||
|
||||
def _configure_logging(verbose: int = 0) -> None:
|
||||
if verbose >= 2:
|
||||
log_format = "%(asctime)s.%(msecs)03d %(levelname)-7s %(name)-17s - %(message)s"
|
||||
log_level = logging.DEBUG
|
||||
elif verbose == 1:
|
||||
log_format = "%(levelname)-7s - %(message)s"
|
||||
log_level = logging.INFO
|
||||
else:
|
||||
log_format = "%(levelname)-7s - %(message)s"
|
||||
log_level = logging.INFO
|
||||
|
||||
logging.basicConfig(level=log_level, format=log_format, datefmt="%Y-%m-%dT%H:%M:%S")
|
||||
logger.debug("Logging configured.")
|
||||
|
||||
|
||||
def _validate_release_tag(release: str) -> None:
|
||||
if release in VALID_RELEASE_VALUES:
|
||||
return
|
||||
|
||||
logger.error(f"Invalid argument --release={release}")
|
||||
logger.error(f"Valid arguments are: {', '.join(VALID_RELEASE_VALUES)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version="v202007.0036")
|
||||
@click.help_option()
|
||||
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
|
||||
def cli(verbose: int = 0) -> None:
|
||||
"""Automatically update PyCalVer version strings on python projects."""
|
||||
global _VERBOSE
|
||||
_VERBOSE = verbose
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("old_version")
|
||||
@click.argument("pattern", default="{pycalver}")
|
||||
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
|
||||
@click.option(
|
||||
"--release", default=None, metavar="<name>", help="Override release name of current_version"
|
||||
)
|
||||
@click.option("--major", is_flag=True, default=False, help="Increment major component.")
|
||||
@click.option("--minor", is_flag=True, default=False, help="Increment minor component.")
|
||||
@click.option("--patch", is_flag=True, default=False, help="Increment patch component.")
|
||||
def test(
|
||||
old_version: str,
|
||||
pattern : str = "{pycalver}",
|
||||
verbose : int = 0,
|
||||
release : str = None,
|
||||
major : bool = False,
|
||||
minor : bool = False,
|
||||
patch : bool = False,
|
||||
) -> None:
|
||||
"""Increment a version number for demo purposes."""
|
||||
_configure_logging(verbose=max(_VERBOSE, verbose))
|
||||
|
||||
if release:
|
||||
_validate_release_tag(release)
|
||||
|
||||
new_version = version.incr(
|
||||
old_version, pattern=pattern, release=release, major=major, minor=minor, patch=patch
|
||||
)
|
||||
if new_version is None:
|
||||
logger.error(f"Invalid version '{old_version}' and/or pattern '{pattern}'.")
|
||||
sys.exit(1)
|
||||
|
||||
pep440_version = version.to_pep440(new_version)
|
||||
|
||||
click.echo(f"New Version: {new_version}")
|
||||
click.echo(f"PEP440 : {pep440_version}")
|
||||
|
||||
|
||||
def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config:
|
||||
try:
|
||||
vcs_api = vcs.get_vcs_api()
|
||||
logger.debug(f"vcs found: {vcs_api.name}")
|
||||
if fetch:
|
||||
logger.info("fetching tags from remote (to turn off use: -n / --no-fetch)")
|
||||
vcs_api.fetch()
|
||||
|
||||
version_tags = [
|
||||
tag for tag in vcs_api.ls_tags() if version.is_valid(tag, cfg.version_pattern)
|
||||
]
|
||||
if version_tags:
|
||||
version_tags.sort(reverse=True)
|
||||
logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}")
|
||||
latest_version_tag = version_tags[0]
|
||||
latest_version_pep440 = version.to_pep440(latest_version_tag)
|
||||
if latest_version_tag > cfg.current_version:
|
||||
logger.info(f"Working dir version : {cfg.current_version}")
|
||||
logger.info(f"Latest version from {vcs_api.name:>3} tag: {latest_version_tag}")
|
||||
cfg = cfg._replace(
|
||||
current_version=latest_version_tag, pep440_version=latest_version_pep440
|
||||
)
|
||||
|
||||
else:
|
||||
logger.debug("no vcs tags found")
|
||||
except OSError:
|
||||
logger.debug("No vcs found")
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
|
||||
@click.option(
|
||||
"-f/-n", "--fetch/--no-fetch", is_flag=True, default=True, help="Sync tags from remote origin."
|
||||
)
|
||||
def show(verbose: int = 0, fetch: bool = True) -> None:
|
||||
"""Show current version."""
|
||||
_configure_logging(verbose=max(_VERBOSE, verbose))
|
||||
|
||||
ctx: config.ProjectContext = config.init_project_ctx(project_path=".")
|
||||
cfg: config.MaybeConfig = config.parse(ctx)
|
||||
|
||||
if cfg is None:
|
||||
logger.error("Could not parse configuration. Perhaps try 'pycalver init'.")
|
||||
sys.exit(1)
|
||||
|
||||
cfg = _update_cfg_from_vcs(cfg, fetch=fetch)
|
||||
|
||||
click.echo(f"Current Version: {cfg.current_version}")
|
||||
click.echo(f"PEP440 : {cfg.pep440_version}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
|
||||
@click.option(
|
||||
"--dry", default=False, is_flag=True, help="Display diff of changes, don't rewrite files."
|
||||
)
|
||||
def init(verbose: int = 0, dry: bool = False) -> None:
|
||||
"""Initialize [pycalver] configuration."""
|
||||
_configure_logging(verbose=max(_VERBOSE, verbose))
|
||||
|
||||
ctx: config.ProjectContext = config.init_project_ctx(project_path=".")
|
||||
cfg: config.MaybeConfig = config.parse(ctx)
|
||||
|
||||
if cfg:
|
||||
logger.error(f"Configuration already initialized in {ctx.config_filepath}")
|
||||
sys.exit(1)
|
||||
|
||||
if dry:
|
||||
click.echo(f"Exiting because of '--dry'. Would have written to {ctx.config_filepath}:")
|
||||
cfg_text: str = config.default_config(ctx)
|
||||
click.echo("\n " + "\n ".join(cfg_text.splitlines()))
|
||||
sys.exit(0)
|
||||
|
||||
config.write_content(ctx)
|
||||
|
||||
|
||||
def _assert_not_dirty(vcs_api: vcs.VCSAPI, filepaths: typ.Set[str], allow_dirty: bool) -> None:
|
||||
dirty_files = vcs_api.status(required_files=filepaths)
|
||||
|
||||
if dirty_files:
|
||||
logger.warning(f"{vcs_api.name} working directory is not clean. Uncomitted file(s):")
|
||||
for dirty_file in dirty_files:
|
||||
logger.warning(" " + dirty_file)
|
||||
|
||||
if not allow_dirty and dirty_files:
|
||||
sys.exit(1)
|
||||
|
||||
dirty_pattern_files = set(dirty_files) & filepaths
|
||||
if dirty_pattern_files:
|
||||
logger.error("Not commiting when pattern files are dirty:")
|
||||
for dirty_file in dirty_pattern_files:
|
||||
logger.warning(" " + dirty_file)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _commit(
|
||||
cfg: config.Config, new_version: str, vcs_api: vcs.VCSAPI, filepaths: typ.Set[str]
|
||||
) -> None:
|
||||
for filepath in filepaths:
|
||||
vcs_api.add(filepath)
|
||||
|
||||
vcs_api.commit(f"bump version to {new_version}")
|
||||
|
||||
if cfg.commit and cfg.tag:
|
||||
vcs_api.tag(new_version)
|
||||
|
||||
if cfg.commit and cfg.tag and cfg.push:
|
||||
vcs_api.push(new_version)
|
||||
|
||||
|
||||
def _bump(cfg: config.Config, new_version: str, allow_dirty: bool = False) -> None:
|
||||
vcs_api: typ.Optional[vcs.VCSAPI] = None
|
||||
|
||||
if cfg.commit:
|
||||
try:
|
||||
vcs_api = vcs.get_vcs_api()
|
||||
except OSError:
|
||||
logger.warning("Version Control System not found, aborting commit.")
|
||||
|
||||
filepaths = set(cfg.file_patterns.keys())
|
||||
|
||||
if vcs_api:
|
||||
_assert_not_dirty(vcs_api, filepaths, allow_dirty)
|
||||
|
||||
try:
|
||||
new_vinfo = version.parse_version_info(new_version, cfg.version_pattern)
|
||||
rewrite.rewrite(cfg.file_patterns, new_vinfo)
|
||||
except Exception as ex:
|
||||
logger.error(str(ex))
|
||||
sys.exit(1)
|
||||
|
||||
if vcs_api:
|
||||
_commit(cfg, new_version, vcs_api, filepaths)
|
||||
|
||||
|
||||
def _try_bump(cfg: config.Config, new_version: str, allow_dirty: bool = False) -> None:
|
||||
try:
|
||||
_bump(cfg, new_version, allow_dirty)
|
||||
except sp.CalledProcessError as ex:
|
||||
logger.error(f"Error running subcommand: {ex.cmd}")
|
||||
if ex.stdout:
|
||||
sys.stdout.write(ex.stdout.decode('utf-8'))
|
||||
if ex.stderr:
|
||||
sys.stderr.write(ex.stderr.decode('utf-8'))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _print_diff(cfg: config.Config, new_version: str) -> None:
|
||||
new_vinfo = version.parse_version_info(new_version, cfg.version_pattern)
|
||||
diff: str = rewrite.diff(new_vinfo, cfg.file_patterns)
|
||||
|
||||
if sys.stdout.isatty():
|
||||
for line in diff.splitlines():
|
||||
if line.startswith("+++") or line.startswith("---"):
|
||||
click.echo(line)
|
||||
elif line.startswith("+"):
|
||||
click.echo("\u001b[32m" + line + "\u001b[0m")
|
||||
elif line.startswith("-"):
|
||||
click.echo("\u001b[31m" + line + "\u001b[0m")
|
||||
elif line.startswith("@"):
|
||||
click.echo("\u001b[36m" + line + "\u001b[0m")
|
||||
else:
|
||||
click.echo(line)
|
||||
else:
|
||||
click.echo(diff)
|
||||
|
||||
|
||||
def _try_print_diff(cfg: config.Config, new_version: str) -> None:
|
||||
try:
|
||||
_print_diff(cfg, new_version)
|
||||
except Exception as ex:
|
||||
logger.error(str(ex))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("-v", "--verbose", count=True, help="Control log level. -vv for debug level.")
|
||||
@click.option(
|
||||
"-f/-n", "--fetch/--no-fetch", is_flag=True, default=True, help="Sync tags from remote origin."
|
||||
)
|
||||
@click.option(
|
||||
"--dry", default=False, is_flag=True, help="Display diff of changes, don't rewrite files."
|
||||
)
|
||||
@click.option(
|
||||
"--release",
|
||||
default=None,
|
||||
metavar="<name>",
|
||||
help=(
|
||||
f"Override release name of current_version. Valid options are: "
|
||||
f"{', '.join(VALID_RELEASE_VALUES)}."
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--allow-dirty",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
help=(
|
||||
"Commit even when working directory is has uncomitted changes. "
|
||||
"(WARNING: The commit will still be aborted if there are uncomitted "
|
||||
"to files with version strings."
|
||||
),
|
||||
)
|
||||
@click.option("--major", is_flag=True, default=False, help="Increment major component.")
|
||||
@click.option("--minor", is_flag=True, default=False, help="Increment minor component.")
|
||||
@click.option("--patch", is_flag=True, default=False, help="Increment patch component.")
|
||||
def bump(
|
||||
release : typ.Optional[str] = None,
|
||||
verbose : int = 0,
|
||||
dry : bool = False,
|
||||
allow_dirty: bool = False,
|
||||
fetch : bool = True,
|
||||
major : bool = False,
|
||||
minor : bool = False,
|
||||
patch : bool = False,
|
||||
) -> None:
|
||||
"""Increment the current version string and update project files."""
|
||||
verbose = max(_VERBOSE, verbose)
|
||||
_configure_logging(verbose)
|
||||
|
||||
if release:
|
||||
_validate_release_tag(release)
|
||||
|
||||
ctx: config.ProjectContext = config.init_project_ctx(project_path=".")
|
||||
cfg: config.MaybeConfig = config.parse(ctx)
|
||||
|
||||
if cfg is None:
|
||||
logger.error("Could not parse configuration. Perhaps try 'pycalver init'.")
|
||||
sys.exit(1)
|
||||
|
||||
cfg = _update_cfg_from_vcs(cfg, fetch=fetch)
|
||||
|
||||
old_version = cfg.current_version
|
||||
new_version = version.incr(
|
||||
old_version,
|
||||
pattern=cfg.version_pattern,
|
||||
release=release,
|
||||
major=major,
|
||||
minor=minor,
|
||||
patch=patch,
|
||||
)
|
||||
if new_version is None:
|
||||
is_semver = "{semver}" in cfg.version_pattern
|
||||
has_semver_inc = major or minor or patch
|
||||
if is_semver and not has_semver_inc:
|
||||
logger.warning("bump --major/--minor/--patch required when using semver.")
|
||||
else:
|
||||
logger.error(f"Invalid version '{old_version}' and/or pattern '{cfg.version_pattern}'.")
|
||||
sys.exit(1)
|
||||
|
||||
logger.info(f"Old Version: {old_version}")
|
||||
logger.info(f"New Version: {new_version}")
|
||||
|
||||
if dry or verbose >= 2:
|
||||
_try_print_diff(cfg, new_version)
|
||||
|
||||
if dry:
|
||||
return
|
||||
|
||||
_try_bump(cfg, new_version, allow_dirty)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
|
|
@ -1,482 +0,0 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://gitlab.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Parse setup.cfg or pycalver.cfg files."""
|
||||
|
||||
import os
|
||||
import typing as typ
|
||||
import logging
|
||||
import datetime as dt
|
||||
import configparser
|
||||
|
||||
import six
|
||||
import toml
|
||||
import pathlib2 as pl
|
||||
|
||||
from . import version
|
||||
|
||||
logger = logging.getLogger("pycalver.config")
|
||||
|
||||
Patterns = typ.List[str]
|
||||
PatternsByGlob = typ.Dict[str, Patterns]
|
||||
|
||||
SUPPORTED_CONFIGS = ["setup.cfg", "pyproject.toml", "pycalver.toml"]
|
||||
|
||||
|
||||
class ProjectContext(typ.NamedTuple):
|
||||
"""Container class for project info."""
|
||||
|
||||
path : pl.Path
|
||||
config_filepath: pl.Path
|
||||
config_format : str
|
||||
vcs_type : typ.Optional[str]
|
||||
|
||||
|
||||
def init_project_ctx(project_path: typ.Union[str, pl.Path, None] = ".") -> ProjectContext:
|
||||
"""Initialize ProjectContext from a path."""
|
||||
if isinstance(project_path, pl.Path):
|
||||
path = project_path
|
||||
elif project_path is None:
|
||||
path = pl.Path(".")
|
||||
else:
|
||||
# assume it's a str/unicode
|
||||
path = pl.Path(project_path)
|
||||
|
||||
if (path / "pycalver.toml").exists():
|
||||
config_filepath = path / "pycalver.toml"
|
||||
config_format = 'toml'
|
||||
elif (path / "pyproject.toml").exists():
|
||||
config_filepath = path / "pyproject.toml"
|
||||
config_format = 'toml'
|
||||
elif (path / "setup.cfg").exists():
|
||||
config_filepath = path / "setup.cfg"
|
||||
config_format = 'cfg'
|
||||
else:
|
||||
# fallback to creating a new pycalver.toml
|
||||
config_filepath = path / "pycalver.toml"
|
||||
config_format = 'toml'
|
||||
|
||||
vcs_type: typ.Optional[str]
|
||||
|
||||
if (path / ".git").exists():
|
||||
vcs_type = 'git'
|
||||
elif (path / ".hg").exists():
|
||||
vcs_type = 'hg'
|
||||
else:
|
||||
vcs_type = None
|
||||
|
||||
return ProjectContext(path, config_filepath, config_format, vcs_type)
|
||||
|
||||
|
||||
RawConfig = typ.Dict[str, typ.Any]
|
||||
|
||||
|
||||
class Config(typ.NamedTuple):
|
||||
"""Container for parameters parsed from a config file."""
|
||||
|
||||
current_version: str
|
||||
version_pattern: str
|
||||
pep440_version : str
|
||||
|
||||
commit: bool
|
||||
tag : bool
|
||||
push : bool
|
||||
|
||||
file_patterns: PatternsByGlob
|
||||
|
||||
|
||||
def _debug_str(cfg: Config) -> str:
|
||||
cfg_str_parts = [
|
||||
"Config Parsed: Config(",
|
||||
f"current_version='{cfg.current_version}'",
|
||||
"version_pattern='{pycalver}'",
|
||||
f"pep440_version='{cfg.pep440_version}'",
|
||||
f"commit={cfg.commit}",
|
||||
f"tag={cfg.tag}",
|
||||
f"push={cfg.push}",
|
||||
"file_patterns={",
|
||||
]
|
||||
|
||||
for filepath, patterns in cfg.file_patterns.items():
|
||||
for pattern in patterns:
|
||||
cfg_str_parts.append(f"\n '{filepath}': '{pattern}'")
|
||||
|
||||
cfg_str_parts += ["\n})"]
|
||||
return ", ".join(cfg_str_parts)
|
||||
|
||||
|
||||
MaybeConfig = typ.Optional[Config]
|
||||
MaybeRawConfig = typ.Optional[RawConfig]
|
||||
|
||||
FilePatterns = typ.Dict[str, typ.List[str]]
|
||||
|
||||
|
||||
def _parse_cfg_file_patterns(cfg_parser: configparser.RawConfigParser) -> FilePatterns:
|
||||
file_patterns: FilePatterns = {}
|
||||
|
||||
file_pattern_items: typ.List[typ.Tuple[str, str]]
|
||||
if cfg_parser.has_section("pycalver:file_patterns"):
|
||||
file_pattern_items = cfg_parser.items("pycalver:file_patterns")
|
||||
else:
|
||||
file_pattern_items = []
|
||||
|
||||
for filepath, patterns_str in file_pattern_items:
|
||||
patterns: typ.List[str] = []
|
||||
for line in patterns_str.splitlines():
|
||||
pattern = line.strip()
|
||||
if pattern:
|
||||
patterns.append(pattern)
|
||||
|
||||
file_patterns[filepath] = patterns
|
||||
|
||||
return file_patterns
|
||||
|
||||
|
||||
class _ConfigParser(configparser.RawConfigParser):
|
||||
# pylint:disable=too-many-ancestors ; from our perspective, it's just one
|
||||
"""Custom parser, simply to override optionxform behaviour."""
|
||||
|
||||
def optionxform(self, optionstr: str) -> str:
|
||||
"""Non-xforming (ie. uppercase preserving) override.
|
||||
|
||||
This is important because our option names are actually
|
||||
filenames, so case sensitivity is relevant. The default
|
||||
behaviour is to do optionstr.lower()
|
||||
"""
|
||||
return optionstr
|
||||
|
||||
|
||||
OptionVal = typ.Union[str, bool, None]
|
||||
|
||||
BOOL_OPTIONS: typ.Mapping[str, OptionVal] = {'commit': False, 'tag': None, 'push': None}
|
||||
|
||||
|
||||
def _parse_cfg(cfg_buffer: typ.IO[str]) -> RawConfig:
|
||||
cfg_parser = _ConfigParser()
|
||||
|
||||
if hasattr(cfg_parser, 'read_file'):
|
||||
cfg_parser.read_file(cfg_buffer)
|
||||
else:
|
||||
cfg_parser.readfp(cfg_buffer) # python2 compat
|
||||
|
||||
if not cfg_parser.has_section("pycalver"):
|
||||
raise ValueError("Missing [pycalver] section.")
|
||||
|
||||
raw_cfg: RawConfig = dict(cfg_parser.items("pycalver"))
|
||||
|
||||
for option, default_val in BOOL_OPTIONS.items():
|
||||
val: OptionVal = raw_cfg.get(option, default_val)
|
||||
if isinstance(val, six.text_type):
|
||||
val = val.lower() in ("yes", "true", "1", "on")
|
||||
raw_cfg[option] = val
|
||||
|
||||
raw_cfg['file_patterns'] = _parse_cfg_file_patterns(cfg_parser)
|
||||
|
||||
return raw_cfg
|
||||
|
||||
|
||||
def _parse_toml(cfg_buffer: typ.IO[str]) -> RawConfig:
|
||||
raw_full_cfg: typ.Any = toml.load(cfg_buffer)
|
||||
raw_cfg : RawConfig = raw_full_cfg.get('pycalver', {})
|
||||
|
||||
for option, default_val in BOOL_OPTIONS.items():
|
||||
raw_cfg[option] = raw_cfg.get(option, default_val)
|
||||
|
||||
return raw_cfg
|
||||
|
||||
|
||||
def _normalize_file_patterns(raw_cfg: RawConfig) -> FilePatterns:
|
||||
"""Create consistent representation of file_patterns.
|
||||
|
||||
The result the same, regardless of the config format.
|
||||
"""
|
||||
version_str : str = raw_cfg['current_version']
|
||||
version_pattern: str = raw_cfg['version_pattern']
|
||||
pep440_version : str = version.to_pep440(version_str)
|
||||
|
||||
file_patterns: FilePatterns
|
||||
if 'file_patterns' in raw_cfg:
|
||||
file_patterns = raw_cfg['file_patterns']
|
||||
else:
|
||||
file_patterns = {}
|
||||
|
||||
for filepath, patterns in list(file_patterns.items()):
|
||||
if not os.path.exists(filepath):
|
||||
logger.warning(f"Invalid config, no such file: {filepath}")
|
||||
|
||||
normalized_patterns: typ.List[str] = []
|
||||
for pattern in patterns:
|
||||
normalized_pattern = pattern.replace("{version}", version_pattern)
|
||||
if version_pattern == "{pycalver}":
|
||||
normalized_pattern = normalized_pattern.replace(
|
||||
"{pep440_version}", "{pep440_pycalver}"
|
||||
)
|
||||
elif version_pattern == "{semver}":
|
||||
normalized_pattern = normalized_pattern.replace("{pep440_version}", "{semver}")
|
||||
elif "{pep440_version}" in pattern:
|
||||
logger.warning(f"Invalid config, cannot match '{pattern}' for '{filepath}'.")
|
||||
logger.warning(f"No mapping of '{version_pattern}' to '{pep440_version}'")
|
||||
normalized_patterns.append(normalized_pattern)
|
||||
|
||||
file_patterns[filepath] = normalized_patterns
|
||||
|
||||
return file_patterns
|
||||
|
||||
|
||||
def _parse_config(raw_cfg: RawConfig) -> Config:
|
||||
"""Parse configuration which was loaded from an .ini/.cfg or .toml file."""
|
||||
|
||||
if 'current_version' not in raw_cfg:
|
||||
raise ValueError("Missing 'pycalver.current_version'")
|
||||
|
||||
version_str: str = raw_cfg['current_version']
|
||||
version_str = raw_cfg['current_version'] = version_str.strip("'\" ")
|
||||
|
||||
version_pattern: str = raw_cfg.get('version_pattern', "{pycalver}")
|
||||
version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ")
|
||||
|
||||
# NOTE (mb 2019-01-05): Provoke ValueError if version_pattern
|
||||
# and current_version are not compatible.
|
||||
version.parse_version_info(version_str, version_pattern)
|
||||
|
||||
pep440_version = version.to_pep440(version_str)
|
||||
|
||||
commit = raw_cfg['commit']
|
||||
tag = raw_cfg['tag']
|
||||
push = raw_cfg['push']
|
||||
|
||||
if tag is None:
|
||||
tag = raw_cfg['tag'] = False
|
||||
if push is None:
|
||||
push = raw_cfg['push'] = False
|
||||
|
||||
if tag and not commit:
|
||||
raise ValueError("pycalver.commit = true required if pycalver.tag = true")
|
||||
|
||||
if push and not commit:
|
||||
raise ValueError("pycalver.commit = true required if pycalver.push = true")
|
||||
|
||||
file_patterns = _normalize_file_patterns(raw_cfg)
|
||||
|
||||
cfg = Config(
|
||||
current_version=version_str,
|
||||
version_pattern=version_pattern,
|
||||
pep440_version=pep440_version,
|
||||
commit=commit,
|
||||
tag=tag,
|
||||
push=push,
|
||||
file_patterns=file_patterns,
|
||||
)
|
||||
logger.debug(_debug_str(cfg))
|
||||
return cfg
|
||||
|
||||
|
||||
def _parse_current_version_default_pattern(cfg: Config, raw_cfg_text: str) -> str:
|
||||
is_pycalver_section = False
|
||||
for line in raw_cfg_text.splitlines():
|
||||
if is_pycalver_section and line.startswith("current_version"):
|
||||
return line.replace(cfg.current_version, cfg.version_pattern)
|
||||
|
||||
if line.strip() == "[pycalver]":
|
||||
is_pycalver_section = True
|
||||
elif line and line[0] == "[" and line[-1] == "]":
|
||||
is_pycalver_section = False
|
||||
|
||||
raise ValueError("Could not parse pycalver.current_version")
|
||||
|
||||
|
||||
def parse(ctx: ProjectContext) -> MaybeConfig:
|
||||
"""Parse config file if available."""
|
||||
if not ctx.config_filepath.exists():
|
||||
logger.warning(f"File not found: {ctx.config_filepath}")
|
||||
return None
|
||||
|
||||
fobj: typ.IO[str]
|
||||
|
||||
cfg_path: str
|
||||
if ctx.config_filepath.is_absolute():
|
||||
cfg_path = str(ctx.config_filepath.relative_to(ctx.path.absolute()))
|
||||
else:
|
||||
cfg_path = str(ctx.config_filepath)
|
||||
|
||||
raw_cfg: RawConfig
|
||||
|
||||
try:
|
||||
with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj:
|
||||
if ctx.config_format == 'toml':
|
||||
raw_cfg = _parse_toml(fobj)
|
||||
elif ctx.config_format == 'cfg':
|
||||
raw_cfg = _parse_cfg(fobj)
|
||||
else:
|
||||
err_msg = "Invalid config_format='{ctx.config_format}'"
|
||||
raise RuntimeError(err_msg)
|
||||
|
||||
cfg: Config = _parse_config(raw_cfg)
|
||||
|
||||
if cfg_path not in cfg.file_patterns:
|
||||
fobj.seek(0)
|
||||
raw_cfg_text = fobj.read()
|
||||
cfg.file_patterns[cfg_path] = [
|
||||
_parse_current_version_default_pattern(cfg, raw_cfg_text)
|
||||
]
|
||||
|
||||
return cfg
|
||||
except ValueError as ex:
|
||||
logger.warning(f"Couldn't parse {cfg_path}: {str(ex)}")
|
||||
return None
|
||||
|
||||
|
||||
DEFAULT_CONFIGPARSER_BASE_TMPL = """
|
||||
[pycalver]
|
||||
current_version = "{initial_version}"
|
||||
version_pattern = "{{pycalver}}"
|
||||
commit = True
|
||||
tag = True
|
||||
push = True
|
||||
|
||||
[pycalver:file_patterns]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_CONFIGPARSER_SETUP_CFG_STR = """
|
||||
setup.cfg =
|
||||
current_version = "{version}"
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_CONFIGPARSER_SETUP_PY_STR = """
|
||||
setup.py =
|
||||
"{version}"
|
||||
"{pep440_version}"
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_CONFIGPARSER_README_RST_STR = """
|
||||
README.rst =
|
||||
{version}
|
||||
{pep440_version}
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_CONFIGPARSER_README_MD_STR = """
|
||||
README.md =
|
||||
{version}
|
||||
{pep440_version}
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_BASE_TMPL = """
|
||||
[pycalver]
|
||||
current_version = "{initial_version}"
|
||||
version_pattern = "{{pycalver}}"
|
||||
commit = true
|
||||
tag = true
|
||||
push = true
|
||||
|
||||
[pycalver.file_patterns]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_PYCALVER_STR = """
|
||||
"pycalver.toml" = [
|
||||
'current_version = "{version}"',
|
||||
]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_PYPROJECT_STR = """
|
||||
"pyproject.toml" = [
|
||||
'current_version = "{version}"',
|
||||
]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_SETUP_PY_STR = """
|
||||
"setup.py" = [
|
||||
"{version}",
|
||||
"{pep440_version}",
|
||||
]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_README_RST_STR = """
|
||||
"README.rst" = [
|
||||
"{version}",
|
||||
"{pep440_version}",
|
||||
]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
DEFAULT_TOML_README_MD_STR = """
|
||||
"README.md" = [
|
||||
"{version}",
|
||||
"{pep440_version}",
|
||||
]
|
||||
""".lstrip()
|
||||
|
||||
|
||||
def _initial_version() -> str:
|
||||
return dt.datetime.now().strftime("v%Y%m.0001-alpha")
|
||||
|
||||
|
||||
def _initial_version_pep440() -> str:
|
||||
return dt.datetime.now().strftime("%Y%m.1a0")
|
||||
|
||||
|
||||
def default_config(ctx: ProjectContext) -> str:
|
||||
"""Generate initial default config."""
|
||||
fmt = ctx.config_format
|
||||
if fmt == 'cfg':
|
||||
base_tmpl = DEFAULT_CONFIGPARSER_BASE_TMPL
|
||||
|
||||
default_pattern_strs_by_filename = {
|
||||
"setup.cfg" : DEFAULT_CONFIGPARSER_SETUP_CFG_STR,
|
||||
"setup.py" : DEFAULT_CONFIGPARSER_SETUP_PY_STR,
|
||||
"README.rst": DEFAULT_CONFIGPARSER_README_RST_STR,
|
||||
"README.md" : DEFAULT_CONFIGPARSER_README_MD_STR,
|
||||
}
|
||||
elif fmt == 'toml':
|
||||
base_tmpl = DEFAULT_TOML_BASE_TMPL
|
||||
|
||||
default_pattern_strs_by_filename = {
|
||||
"pyproject.toml": DEFAULT_TOML_PYPROJECT_STR,
|
||||
"pycalver.toml" : DEFAULT_TOML_PYCALVER_STR,
|
||||
"setup.py" : DEFAULT_TOML_SETUP_PY_STR,
|
||||
"README.rst" : DEFAULT_TOML_README_RST_STR,
|
||||
"README.md" : DEFAULT_TOML_README_MD_STR,
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Invalid config_format='{fmt}', must be either 'toml' or 'cfg'.")
|
||||
|
||||
cfg_str = base_tmpl.format(initial_version=_initial_version())
|
||||
|
||||
for filename, default_str in default_pattern_strs_by_filename.items():
|
||||
if (ctx.path / filename).exists():
|
||||
cfg_str += default_str
|
||||
|
||||
has_config_file = any((ctx.path / fn).exists() for fn in SUPPORTED_CONFIGS)
|
||||
|
||||
if not has_config_file:
|
||||
if ctx.config_format == 'cfg':
|
||||
cfg_str += DEFAULT_CONFIGPARSER_SETUP_CFG_STR
|
||||
if ctx.config_format == 'toml':
|
||||
cfg_str += DEFAULT_TOML_PYCALVER_STR
|
||||
|
||||
cfg_str += "\n"
|
||||
|
||||
return cfg_str
|
||||
|
||||
|
||||
def write_content(ctx: ProjectContext) -> None:
|
||||
"""Update project config file with initial default config."""
|
||||
fobj: typ.IO[str]
|
||||
|
||||
cfg_content = default_config(ctx)
|
||||
if ctx.config_filepath.exists():
|
||||
cfg_content = "\n" + cfg_content
|
||||
|
||||
with ctx.config_filepath.open(mode="at", encoding="utf-8") as fobj:
|
||||
fobj.write(cfg_content)
|
||||
print(f"Updated {ctx.config_filepath}")
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://gitlab.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""A scheme for lexically ordered numerical ids.
|
||||
|
||||
Throughout the sequence this expression remains true, whether you
|
||||
are dealing with integers or strings:
|
||||
|
||||
older_id < newer_id
|
||||
|
||||
The left most character/digit is only used to maintain lexical
|
||||
order, so that the position in the sequence is maintained in the
|
||||
remaining digits.
|
||||
|
||||
sequence_pos = int(idval[1:], 10)
|
||||
|
||||
lexical sequence_pos
|
||||
0 0
|
||||
11 1
|
||||
12 2
|
||||
...
|
||||
19 9
|
||||
220 20
|
||||
221 21
|
||||
...
|
||||
298 98
|
||||
299 99
|
||||
3300 300
|
||||
3301 301
|
||||
...
|
||||
3998 998
|
||||
3999 999
|
||||
44000 4000
|
||||
44001 4001
|
||||
...
|
||||
899999998 99999998
|
||||
899999999 99999999
|
||||
9900000000 900000000
|
||||
9900000001 900000001
|
||||
...
|
||||
9999999998 999999998
|
||||
9999999999 999999999 # maximum value
|
||||
|
||||
You can add leading zeros to delay the expansion and/or increase
|
||||
the maximum possible value.
|
||||
|
||||
lexical sequence_pos
|
||||
0001 1
|
||||
0002 2
|
||||
0003 3
|
||||
...
|
||||
0999 999
|
||||
11000 1000
|
||||
11001 1001
|
||||
11002 1002
|
||||
...
|
||||
19998 9998
|
||||
19999 9999
|
||||
220000 20000
|
||||
220001 20001
|
||||
...
|
||||
899999999998 99999999998
|
||||
899999999999 99999999999
|
||||
9900000000000 900000000000
|
||||
9900000000001 900000000001
|
||||
...
|
||||
9999999999998 999999999998
|
||||
9999999999999 999999999999 # maximum value
|
||||
|
||||
This scheme is useful when you just want an ordered sequence of
|
||||
numbers, but the numbers don't have any particular meaning or
|
||||
arithmetical relation. The only relation they have to each other
|
||||
is that numbers generated later in the sequence are greater than
|
||||
ones generated earlier.
|
||||
"""
|
||||
|
||||
|
||||
MINIMUM_ID = "0"
|
||||
|
||||
|
||||
def next_id(prev_id: str) -> str:
|
||||
"""Generate next lexical id.
|
||||
|
||||
Increments by one and adds padding if required.
|
||||
|
||||
>>> next_id("0098")
|
||||
'0099'
|
||||
>>> next_id("0099")
|
||||
'0100'
|
||||
>>> next_id("0999")
|
||||
'11000'
|
||||
>>> next_id("11000")
|
||||
'11001'
|
||||
"""
|
||||
|
||||
num_digits = len(prev_id)
|
||||
|
||||
if prev_id.count("9") == num_digits:
|
||||
raise OverflowError("max lexical version reached: " + prev_id)
|
||||
|
||||
_prev_id_val = int(prev_id, 10)
|
||||
_maybe_next_id_val = int(_prev_id_val) + 1
|
||||
_maybe_next_id_str = f"{_maybe_next_id_val:0{num_digits}}"
|
||||
|
||||
_is_padding_ok = prev_id[0] == _maybe_next_id_str[0]
|
||||
_next_id_str: str
|
||||
|
||||
if _is_padding_ok:
|
||||
_next_id_str = _maybe_next_id_str
|
||||
else:
|
||||
_next_id_str = str(_maybe_next_id_val * 11)
|
||||
return _next_id_str
|
||||
|
||||
|
||||
def ord_val(lex_id: str) -> int:
|
||||
"""Parse the ordinal value of a lexical id.
|
||||
|
||||
The ordinal value is the position in the sequence,
|
||||
from repeated calls to next_id.
|
||||
|
||||
>>> ord_val("0098")
|
||||
98
|
||||
>>> ord_val("0099")
|
||||
99
|
||||
>>> ord_val("0100")
|
||||
100
|
||||
>>> ord_val("11000")
|
||||
1000
|
||||
>>> ord_val("11001")
|
||||
1001
|
||||
"""
|
||||
if len(lex_id) == 1:
|
||||
return int(lex_id, 10)
|
||||
else:
|
||||
return int(lex_id[1:], 10)
|
||||
|
||||
|
||||
def _main() -> None:
|
||||
_curr_id = "01"
|
||||
print(f"{'lexical':<13} {'numerical':>12}")
|
||||
|
||||
while True:
|
||||
print(f"{_curr_id:<13} {ord_val(_curr_id):>12}")
|
||||
_next_id = next_id(_curr_id)
|
||||
|
||||
if _next_id.count("9") == len(_next_id):
|
||||
# all nines, we're done
|
||||
print(f"{_next_id:<13} {ord_val(_next_id):>12}")
|
||||
break
|
||||
|
||||
if _next_id[0] != _curr_id[0] and len(_curr_id) > 1:
|
||||
print(f"{_next_id:<13} {ord_val(_next_id):>12}")
|
||||
_next_id = next_id(_next_id)
|
||||
print(f"{_next_id:<13} {ord_val(_next_id):>12}")
|
||||
_next_id = next_id(_next_id)
|
||||
|
||||
print("...")
|
||||
|
||||
# skip ahead
|
||||
_next_id = _next_id[:1] + "9" * (len(_next_id) - 2) + "8"
|
||||
|
||||
_curr_id = _next_id
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_main()
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://gitlab.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Parse PyCalVer strings from files."""
|
||||
|
||||
import typing as typ
|
||||
|
||||
from .patterns import compile_pattern
|
||||
|
||||
|
||||
class PatternMatch(typ.NamedTuple):
|
||||
"""Container to mark a version string in a file."""
|
||||
|
||||
lineno : int # zero based
|
||||
line : str
|
||||
pattern: str
|
||||
span : typ.Tuple[int, int]
|
||||
match : str
|
||||
|
||||
|
||||
PatternMatches = typ.Iterable[PatternMatch]
|
||||
|
||||
|
||||
def _iter_for_pattern(lines: typ.List[str], pattern: str) -> PatternMatches:
|
||||
# The pattern is escaped, so that everything besides the format
|
||||
# string variables is treated literally.
|
||||
pattern_re = compile_pattern(pattern)
|
||||
|
||||
for lineno, line in enumerate(lines):
|
||||
match = pattern_re.search(line)
|
||||
if match:
|
||||
yield PatternMatch(lineno, line, pattern, match.span(), match.group(0))
|
||||
|
||||
|
||||
def iter_matches(lines: typ.List[str], patterns: typ.List[str]) -> PatternMatches:
|
||||
"""Iterate over all matches of any pattern on any line.
|
||||
|
||||
>>> lines = ["__version__ = 'v201712.0002-alpha'"]
|
||||
>>> patterns = ["{pycalver}", "{pep440_pycalver}"]
|
||||
>>> matches = list(iter_matches(lines, patterns))
|
||||
>>> assert matches[0] == PatternMatch(
|
||||
... lineno = 0,
|
||||
... line = "__version__ = 'v201712.0002-alpha'",
|
||||
... pattern= "{pycalver}",
|
||||
... span = (15, 33),
|
||||
... match = "v201712.0002-alpha",
|
||||
... )
|
||||
"""
|
||||
for pattern in patterns:
|
||||
for match in _iter_for_pattern(lines, pattern):
|
||||
yield match
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
# This file is part of the pycalver project
|
||||
# https://gitlab.com/mbarkhau/pycalver
|
||||
#
|
||||
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Rewrite files, updating occurences of version strings."""
|
||||
|
||||
import io
|
||||
import glob
|
||||
import typing as typ
|
||||
import difflib
|
||||
import logging
|
||||
|
||||
import pathlib2 as pl
|
||||
|
||||
from . import parse
|
||||
from . import config
|
||||
from . import version
|
||||
from . import patterns
|
||||
|
||||
logger = logging.getLogger("pycalver.rewrite")
|
||||
|
||||
|
||||
def detect_line_sep(content: str) -> str:
|
||||
r"""Parse line separator from content.
|
||||
|
||||
>>> detect_line_sep('\r\n')
|
||||
'\r\n'
|
||||
>>> detect_line_sep('\r')
|
||||
'\r'
|
||||
>>> detect_line_sep('\n')
|
||||
'\n'
|
||||
>>> detect_line_sep('')
|
||||
'\n'
|
||||
"""
|
||||
if "\r\n" in content:
|
||||
return "\r\n"
|
||||
elif "\r" in content:
|
||||
return "\r"
|
||||
else:
|
||||
return "\n"
|
||||
|
||||
|
||||
class NoPatternMatch(Exception):
|
||||
"""Pattern not found in content.
|
||||
|
||||
logger.error is used to show error info about the patterns so
|
||||
that users can debug what is wrong with them. The class
|
||||
itself doesn't capture that info. This approach is used so
|
||||
that all patter issues can be shown, rather than bubbling
|
||||
all the way up the stack on the very first pattern with no
|
||||
matches.
|
||||
"""
|
||||
|
||||
|
||||
def rewrite_lines(
|
||||
pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, old_lines: typ.List[str]
|
||||
) -> typ.List[str]:
|
||||
"""Replace occurances of pattern_strs in old_lines with new_vinfo.
|
||||
|
||||
>>> new_vinfo = version.parse_version_info("v201811.0123-beta")
|
||||
>>> pattern_strs = ['__version__ = "{pycalver}"']
|
||||
>>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "v201809.0002-beta"'])
|
||||
['__version__ = "v201811.0123-beta"']
|
||||
|
||||
>>> pattern_strs = ['__version__ = "{pep440_version}"']
|
||||
>>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "201809.2b0"'])
|
||||
['__version__ = "201811.123b0"']
|
||||
"""
|
||||
new_lines = old_lines[:]
|
||||
found_patterns = set()
|
||||
|
||||
for match in parse.iter_matches(old_lines, pattern_strs):
|
||||
found_patterns.add(match.pattern)
|
||||
replacement = version.format_version(new_vinfo, match.pattern)
|
||||
span_l, span_r = match.span
|
||||
new_line = match.line[:span_l] + replacement + match.line[span_r:]
|
||||
new_lines[match.lineno] = new_line
|
||||
|
||||
non_matched_patterns = set(pattern_strs) - found_patterns
|
||||
if non_matched_patterns:
|
||||
for non_matched_pattern in non_matched_patterns:
|
||||
logger.error(f"No match for pattern '{non_matched_pattern}'")
|
||||
compiled_pattern_str = patterns.compile_pattern_str(non_matched_pattern)
|
||||
logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'")
|
||||
raise NoPatternMatch("Invalid pattern(s)")
|
||||
else:
|
||||
return new_lines
|
||||
|
||||
|
||||
class RewrittenFileData(typ.NamedTuple):
|
||||
"""Container for line-wise content of rewritten files."""
|
||||
|
||||
path : str
|
||||
line_sep : str
|
||||
old_lines: typ.List[str]
|
||||
new_lines: typ.List[str]
|
||||
|
||||
|
||||
def rfd_from_content(
|
||||
pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, content: str
|
||||
) -> RewrittenFileData:
|
||||
r"""Rewrite pattern occurrences with version string.
|
||||
|
||||
>>> new_vinfo = version.parse_version_info("v201809.0123")
|
||||
>>> pattern_strs = ['__version__ = "{pycalver}"']
|
||||
>>> content = '__version__ = "v201809.0001-alpha"'
|
||||
>>> rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
||||
>>> rfd.new_lines
|
||||
['__version__ = "v201809.0123"']
|
||||
>>>
|
||||
>>> new_vinfo = version.parse_version_info("v1.2.3", "v{semver}")
|
||||
>>> pattern_strs = ['__version__ = "v{semver}"']
|
||||
>>> content = '__version__ = "v1.2.2"'
|
||||
>>> rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
||||
>>> rfd.new_lines
|
||||
['__version__ = "v1.2.3"']
|
||||
"""
|
||||
line_sep = detect_line_sep(content)
|
||||
old_lines = content.split(line_sep)
|
||||
new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines)
|
||||
return RewrittenFileData("<path>", line_sep, old_lines, new_lines)
|
||||
|
||||
|
||||
def _iter_file_paths(
|
||||
file_patterns: config.PatternsByGlob,
|
||||
) -> typ.Iterable[typ.Tuple[pl.Path, config.Patterns]]:
|
||||
for globstr, pattern_strs in file_patterns.items():
|
||||
file_paths = glob.glob(globstr)
|
||||
if not any(file_paths):
|
||||
errmsg = f"No files found for path/glob '{globstr}'"
|
||||
raise IOError(errmsg)
|
||||
for file_path_str in file_paths:
|
||||
file_path = pl.Path(file_path_str)
|
||||
yield (file_path, pattern_strs)
|
||||
|
||||
|
||||
def iter_rewritten(
|
||||
file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo
|
||||
) -> typ.Iterable[RewrittenFileData]:
|
||||
r'''Iterate over files with version string replaced.
|
||||
|
||||
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']}
|
||||
>>> new_vinfo = version.parse_version_info("v201809.0123")
|
||||
>>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo)
|
||||
>>> rfd = list(rewritten_datas)[0]
|
||||
>>> assert rfd.new_lines == [
|
||||
... '# This file is part of the pycalver project',
|
||||
... '# https://gitlab.com/mbarkhau/pycalver',
|
||||
... '#',
|
||||
... '# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License',
|
||||
... '# SPDX-License-Identifier: MIT',
|
||||
... '"""PyCalVer: CalVer for Python Packages."""',
|
||||
... '',
|
||||
... '__version__ = "v201809.0123"',
|
||||
... '',
|
||||
... ]
|
||||
>>>
|
||||
'''
|
||||
|
||||
fobj: typ.IO[str]
|
||||
|
||||
for file_path, pattern_strs in _iter_file_paths(file_patterns):
|
||||
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
||||
content = fobj.read()
|
||||
|
||||
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
||||
yield rfd._replace(path=str(file_path))
|
||||
|
||||
|
||||
def diff_lines(rfd: RewrittenFileData) -> typ.List[str]:
|
||||
r"""Generate unified diff.
|
||||
|
||||
>>> rfd = RewrittenFileData(
|
||||
... path = "<path>",
|
||||
... line_sep = "\n",
|
||||
... old_lines = ["foo"],
|
||||
... new_lines = ["bar"],
|
||||
... )
|
||||
>>> diff_lines(rfd)
|
||||
['--- <path>', '+++ <path>', '@@ -1 +1 @@', '-foo', '+bar']
|
||||
"""
|
||||
lines = difflib.unified_diff(
|
||||
a=rfd.old_lines, b=rfd.new_lines, lineterm="", fromfile=rfd.path, tofile=rfd.path
|
||||
)
|
||||
return list(lines)
|
||||
|
||||
|
||||
def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) -> str:
|
||||
r"""Generate diffs of rewritten files.
|
||||
|
||||
>>> new_vinfo = version.parse_version_info("v201809.0123")
|
||||
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']}
|
||||
>>> diff_str = diff(new_vinfo, file_patterns)
|
||||
>>> lines = diff_str.split("\n")
|
||||
>>> lines[:2]
|
||||
['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py']
|
||||
>>> assert lines[6].startswith('-__version__ = "v2')
|
||||
>>> assert not lines[6].startswith('-__version__ = "v201809.0123"')
|
||||
>>> lines[7]
|
||||
'+__version__ = "v201809.0123"'
|
||||
"""
|
||||
|
||||
full_diff = ""
|
||||
fobj: typ.IO[str]
|
||||
|
||||
for file_path, pattern_strs in sorted(_iter_file_paths(file_patterns)):
|
||||
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
||||
content = fobj.read()
|
||||
|
||||
try:
|
||||
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
||||
except NoPatternMatch:
|
||||
# pylint:disable=raise-missing-from ; we support py2, so not an option
|
||||
errmsg = f"No patterns matched for '{file_path}'"
|
||||
raise NoPatternMatch(errmsg)
|
||||
|
||||
rfd = rfd._replace(path=str(file_path))
|
||||
lines = diff_lines(rfd)
|
||||
if len(lines) == 0:
|
||||
errmsg = f"No patterns matched for '{file_path}'"
|
||||
raise NoPatternMatch(errmsg)
|
||||
|
||||
full_diff += "\n".join(lines) + "\n"
|
||||
|
||||
full_diff = full_diff.rstrip("\n")
|
||||
return full_diff
|
||||
|
||||
|
||||
def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo) -> None:
|
||||
"""Rewrite project files, updating each with the new version."""
|
||||
fobj: typ.IO[str]
|
||||
|
||||
for file_data in iter_rewritten(file_patterns, new_vinfo):
|
||||
new_content = file_data.line_sep.join(file_data.new_lines)
|
||||
with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj:
|
||||
fobj.write(new_content)
|
||||
4
test/fixtures/project_a/README.md
vendored
4
test/fixtures/project_a/README.md
vendored
|
|
@ -1,3 +1,3 @@
|
|||
# PyCalVer README Fixture
|
||||
# Python CalVer README Fixture
|
||||
|
||||
Current Version: v201612.0123-alpha
|
||||
Current Version: v2016.0123-alpha
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
[pycalver]
|
||||
current_version = "v201710.0123-alpha"
|
||||
[bumpver]
|
||||
current_version = "v2017.0123-alpha"
|
||||
version_pattern = "vYYYY.BUILD[-TAG]"
|
||||
commit = true
|
||||
tag = true
|
||||
push = true
|
||||
|
||||
[pycalver.file_patterns]
|
||||
"pycalver.toml" = [
|
||||
[bumpver.file_patterns]
|
||||
"bumpver.toml" = [
|
||||
'current_version = "{version}"',
|
||||
]
|
||||
|
||||
4
test/fixtures/project_b/setup.cfg
vendored
4
test/fixtures/project_b/setup.cfg
vendored
|
|
@ -1,11 +1,11 @@
|
|||
[pycalver]
|
||||
[bumpver]
|
||||
current_version = v201307.0456-beta
|
||||
version_pattern = {pycalver}
|
||||
commit = True
|
||||
tag = True
|
||||
push = True
|
||||
|
||||
[pycalver:file_patterns]
|
||||
[bumpver:file_patterns]
|
||||
setup.cfg =
|
||||
current_version = {version}
|
||||
setup.py =
|
||||
|
|
|
|||
7
test/fixtures/project_b/setup.py
vendored
7
test/fixtures/project_b/setup.py
vendored
|
|
@ -1,3 +1,8 @@
|
|||
import setuptools
|
||||
|
||||
setuptools.setup(name="mylib", license="MIT", version="201307.456b0", keywords="awesome library")
|
||||
setuptools.setup(
|
||||
name="mylib",
|
||||
license="MIT",
|
||||
version="201307.456b0",
|
||||
keywords="awesome library",
|
||||
)
|
||||
|
|
|
|||
2
test/fixtures/project_c/pyproject.toml
vendored
2
test/fixtures/project_c/pyproject.toml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
[pycalver]
|
||||
[bumpver]
|
||||
current_version = "v2017q1.54321"
|
||||
version_pattern = "v{year}q{quarter}.{build_no}"
|
||||
commit = true
|
||||
|
|
|
|||
6
test/fixtures/project_d/pyproject.toml
vendored
Normal file
6
test/fixtures/project_d/pyproject.toml
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[calver]
|
||||
current_version = "v2017q1.54321"
|
||||
version_pattern = "vYYYYqQ.BUILD"
|
||||
commit = true
|
||||
tag = true
|
||||
push = true
|
||||
656
test/test_cli.py
656
test/test_cli.py
|
|
@ -1,18 +1,38 @@
|
|||
# pylint:disable=redefined-outer-name ; pytest fixtures
|
||||
# pylint:disable=protected-access ; allowed for test code
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import shlex
|
||||
import shutil
|
||||
import datetime as dt
|
||||
import subprocess as sp
|
||||
|
||||
import pytest
|
||||
import pathlib2 as pl
|
||||
from click.testing import CliRunner
|
||||
|
||||
import pycalver.cli as cli
|
||||
import pycalver.config as config
|
||||
import pycalver.patterns as patterns
|
||||
from bumpver import cli
|
||||
from bumpver import config
|
||||
from bumpver import v2patterns
|
||||
|
||||
# pylint:disable=redefined-outer-name ; pytest fixtures
|
||||
# pylint:disable=protected-access ; allowed for test code
|
||||
# pylint:disable=unused-argument ; allowed for test code
|
||||
|
||||
|
||||
README_TEXT_FIXTURE = """
|
||||
Hello World v2017.1002-alpha !
|
||||
[aka. 2017.1002a0 !]
|
||||
Hello World v201707.1002-alpha !
|
||||
[aka. 201707.1002a0 !]
|
||||
"""
|
||||
|
||||
|
||||
SETUP_CFG_FIXTURE = """
|
||||
[metadata]
|
||||
|
|
@ -22,7 +42,7 @@ license_file = LICENSE
|
|||
universal = 1
|
||||
"""
|
||||
|
||||
PYCALVER_TOML_FIXTURE = """
|
||||
CALVER_TOML_FIXTURE = """
|
||||
"""
|
||||
|
||||
PYPROJECT_TOML_FIXTURE = """
|
||||
|
|
@ -31,11 +51,11 @@ requires = ["setuptools", "wheel"]
|
|||
"""
|
||||
|
||||
ENV = {
|
||||
'GIT_AUTHOR_NAME' : "pycalver_tester",
|
||||
'GIT_COMMITTER_NAME' : "pycalver_tester",
|
||||
'GIT_AUTHOR_EMAIL' : "pycalver_tester@nowhere.com",
|
||||
'GIT_COMMITTER_EMAIL': "pycalver_tester@nowhere.com",
|
||||
'HGUSER' : "pycalver_tester",
|
||||
'GIT_AUTHOR_NAME' : "bumpver_tester",
|
||||
'GIT_COMMITTER_NAME' : "bumpver_tester",
|
||||
'GIT_AUTHOR_EMAIL' : "bumpver_tester@nowhere.com",
|
||||
'GIT_COMMITTER_EMAIL': "bumpver_tester@nowhere.com",
|
||||
'HGUSER' : "bumpver_tester",
|
||||
'PATH' : os.environ['PATH'],
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +64,16 @@ def shell(*cmd):
|
|||
return sp.check_output(cmd, env=ENV)
|
||||
|
||||
|
||||
ECHO_CAPLOG = os.getenv('ECHO_CAPLOG') == "1"
|
||||
|
||||
|
||||
def _debug_records(caplog):
|
||||
if ECHO_CAPLOG:
|
||||
print()
|
||||
for record in caplog.records:
|
||||
print(record)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner(tmpdir):
|
||||
runner = CliRunner(env=ENV)
|
||||
|
|
@ -51,7 +81,7 @@ def runner(tmpdir):
|
|||
|
||||
_debug = 0
|
||||
if _debug:
|
||||
tmpdir = pl.Path("..") / "tmp_test_pycalver_project"
|
||||
tmpdir = pl.Path("..") / "tmp_test_bumpver_project"
|
||||
if tmpdir.exists():
|
||||
time.sleep(0.2)
|
||||
shutil.rmtree(str(tmpdir))
|
||||
|
|
@ -70,8 +100,8 @@ def runner(tmpdir):
|
|||
def test_help(runner):
|
||||
result = runner.invoke(cli.cli, ['--help', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
assert "PyCalVer" in result.output
|
||||
assert "bump " in result.output
|
||||
assert "CalVer" in result.output
|
||||
assert "update " in result.output
|
||||
assert "test " in result.output
|
||||
assert "init " in result.output
|
||||
assert "show " in result.output
|
||||
|
|
@ -80,30 +110,52 @@ def test_help(runner):
|
|||
def test_version(runner):
|
||||
result = runner.invoke(cli.cli, ['--version', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
assert " version v20" in result.output
|
||||
match = patterns.PYCALVER_RE.search(result.output)
|
||||
assert " version 20" in result.output
|
||||
pattern = v2patterns.compile_pattern("YYYY.BUILD[-TAG]")
|
||||
match = pattern.regexp.search(result.output)
|
||||
assert match
|
||||
|
||||
|
||||
def test_incr_default(runner):
|
||||
old_version = "v201701.0999-alpha"
|
||||
initial_version = config._initial_version()
|
||||
old_version = "v201709.1004-alpha"
|
||||
|
||||
result = runner.invoke(cli.cli, ['test', "-vv", old_version])
|
||||
cmd = ['test', "-vv", "--pin-date", "--tag", "beta", old_version, "{pycalver}"]
|
||||
result = runner.invoke(cli.cli, cmd)
|
||||
assert result.exit_code == 0
|
||||
new_version = initial_version.replace(".0001-alpha", ".11000-alpha")
|
||||
assert f"Version: {new_version}\n" in result.output
|
||||
assert "Version: v201709.1005-beta\n" in result.output
|
||||
|
||||
old_version = "v2017.1004-alpha"
|
||||
|
||||
cmd = ['test', "-vv", "--pin-date", "--tag", "beta", old_version, "v{year}{build}{release}"]
|
||||
result = runner.invoke(cli.cli, cmd)
|
||||
assert result.exit_code == 0
|
||||
assert "Version: v2017.1005-beta\n" in result.output
|
||||
|
||||
cmd = ['test', "-vv", "--pin-date", "--tag", "beta", old_version, "vYYYY.BUILD[-TAG]"]
|
||||
result = runner.invoke(cli.cli, cmd)
|
||||
assert result.exit_code == 0
|
||||
assert "Version: v2017.1005-beta\n" in result.output
|
||||
|
||||
|
||||
def test_incr_pin_date(runner):
|
||||
old_version = "v2017.1999-alpha"
|
||||
pattern = "vYYYY.BUILD[-TAG]"
|
||||
result = runner.invoke(cli.cli, ['test', "-vv", "--pin-date", old_version, pattern])
|
||||
assert result.exit_code == 0
|
||||
assert "Version: v2017.22000-alpha\n" in result.output
|
||||
|
||||
|
||||
def test_incr_semver(runner):
|
||||
semver_pattern = "{MAJOR}.{MINOR}.{PATCH}"
|
||||
semver_patterns = [
|
||||
"{semver}",
|
||||
"{MAJOR}.{MINOR}.{PATCH}",
|
||||
"MAJOR.MINOR.PATCH",
|
||||
]
|
||||
|
||||
for semver_pattern in semver_patterns:
|
||||
old_version = "0.1.0"
|
||||
new_version = "0.1.1"
|
||||
|
||||
result = runner.invoke(cli.cli, ['test', "-vv", "--patch", old_version, "{semver}"])
|
||||
assert result.exit_code == 0
|
||||
assert f"Version: {new_version}\n" in result.output
|
||||
|
||||
result = runner.invoke(cli.cli, ['test', "-vv", "--patch", old_version, semver_pattern])
|
||||
assert result.exit_code == 0
|
||||
assert f"Version: {new_version}\n" in result.output
|
||||
|
|
@ -124,50 +176,71 @@ def test_incr_semver(runner):
|
|||
|
||||
|
||||
def test_incr_semver_invalid(runner, caplog):
|
||||
result = runner.invoke(cli.cli, ['test', "-vv", "--patch", "0.1.1"])
|
||||
pattern = "vYYYY.BUILD[-TAG]"
|
||||
result = runner.invoke(cli.cli, ['test', "-vv", "0.1.1", pattern, "--patch"])
|
||||
assert result.exit_code == 1
|
||||
assert len(caplog.records) > 0
|
||||
log_record = caplog.records[0]
|
||||
assert "Invalid version string" in log_record.message
|
||||
assert "for pattern '{pycalver}'" in log_record.message
|
||||
assert "--patch is not applicable to pattern" in log_record.message
|
||||
assert "to pattern 'vYYYY.BUILD[-TAG]'" in log_record.message
|
||||
|
||||
|
||||
def test_incr_to_beta(runner):
|
||||
old_version = "v201701.0999-alpha"
|
||||
initial_version = config._initial_version()
|
||||
pattern = "vYYYY.BUILD[-TAG]"
|
||||
old_version = "v2017.1999-alpha"
|
||||
new_version = dt.datetime.utcnow().strftime("v%Y.22000-beta")
|
||||
|
||||
result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--release", "beta"])
|
||||
result = runner.invoke(cli.cli, ['test', "-vv", old_version, pattern, "--tag", "beta"])
|
||||
assert result.exit_code == 0
|
||||
new_version = initial_version.replace(".0001-alpha", ".11000-beta")
|
||||
assert f"Version: {new_version}\n" in result.output
|
||||
|
||||
|
||||
def test_incr_to_final(runner):
|
||||
old_version = "v201701.0999-alpha"
|
||||
initial_version = config._initial_version()
|
||||
def test_incr_to_final(runner, caplog):
|
||||
pattern = "vYYYY.BUILD[-TAG]"
|
||||
old_version = "v2017.1999-alpha"
|
||||
new_version = dt.datetime.utcnow().strftime("v%Y.22000")
|
||||
|
||||
result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--release", "final"])
|
||||
result = runner.invoke(cli.cli, ['test', "-vv", old_version, pattern, "--tag", "final"])
|
||||
_debug_records(caplog)
|
||||
assert result.exit_code == 0
|
||||
assert f"Version: {new_version}\n" in result.output
|
||||
|
||||
|
||||
SEMVER = "MAJOR.MINOR.PATCH[PYTAGNUM]"
|
||||
|
||||
|
||||
def test_incr_tag(runner):
|
||||
old_version = "0.1.0"
|
||||
new_version = "0.1.1b0"
|
||||
|
||||
result = runner.invoke(
|
||||
cli.cli, ['test', "-vv", old_version, SEMVER, "--patch", "--tag", "beta"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert f"Version: {new_version}\n" in result.output
|
||||
|
||||
|
||||
def test_incr_tag_num(runner):
|
||||
old_version = "0.1.0b0"
|
||||
new_version = "0.1.0b1"
|
||||
|
||||
result = runner.invoke(cli.cli, ['test', "-vv", old_version, SEMVER, "--tag-num"])
|
||||
assert result.exit_code == 0
|
||||
new_version = initial_version.replace(".0001-alpha", ".11000")
|
||||
assert f"Version: {new_version}\n" in result.output
|
||||
|
||||
|
||||
def test_incr_invalid(runner):
|
||||
old_version = "v201701.0999-alpha"
|
||||
pattern = "vYYYY.BUILD[-TAG]"
|
||||
old_version = "v2017.1999-alpha"
|
||||
|
||||
result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--release", "alfa"])
|
||||
result = runner.invoke(cli.cli, ['test', "-vv", old_version, pattern, "--tag", "alfa"])
|
||||
assert result.exit_code == 1
|
||||
|
||||
|
||||
def _add_project_files(*files):
|
||||
if "README.md" in files:
|
||||
with pl.Path("README.md").open(mode="wt", encoding="utf-8") as fobj:
|
||||
fobj.write(
|
||||
"""
|
||||
Hello World v201701.0002-alpha !
|
||||
aka. 201701.2a0 !
|
||||
"""
|
||||
)
|
||||
fobj.write(README_TEXT_FIXTURE)
|
||||
|
||||
if "setup.cfg" in files:
|
||||
with pl.Path("setup.cfg").open(mode="wt", encoding="utf-8") as fobj:
|
||||
|
|
@ -175,21 +248,40 @@ def _add_project_files(*files):
|
|||
|
||||
if "pycalver.toml" in files:
|
||||
with pl.Path("pycalver.toml").open(mode="wt", encoding="utf-8") as fobj:
|
||||
fobj.write(PYCALVER_TOML_FIXTURE)
|
||||
fobj.write(CALVER_TOML_FIXTURE)
|
||||
|
||||
if "pyproject.toml" in files:
|
||||
with pl.Path("pyproject.toml").open(mode="wt", encoding="utf-8") as fobj:
|
||||
fobj.write(PYPROJECT_TOML_FIXTURE)
|
||||
|
||||
if "bumpver.toml" in files:
|
||||
with pl.Path("bumpver.toml").open(mode="wt", encoding="utf-8") as fobj:
|
||||
fobj.write(CALVER_TOML_FIXTURE)
|
||||
|
||||
|
||||
def _update_config_val(filename, **kwargs):
|
||||
with io.open(filename, mode="r", encoding="utf-8") as fobj:
|
||||
old_cfg_text = fobj.read()
|
||||
|
||||
new_cfg_text = old_cfg_text
|
||||
for key, val in kwargs.items():
|
||||
replacement = "{} = {}".format(key, val)
|
||||
if replacement not in new_cfg_text:
|
||||
pattern = r"^{} = .*$".format(key)
|
||||
new_cfg_text = re.sub(pattern, replacement, new_cfg_text, flags=re.MULTILINE)
|
||||
assert old_cfg_text != new_cfg_text
|
||||
|
||||
with io.open(filename, mode="w", encoding="utf-8") as fobj:
|
||||
fobj.write(new_cfg_text)
|
||||
|
||||
|
||||
def test_nocfg(runner, caplog):
|
||||
_add_project_files("README.md")
|
||||
result = runner.invoke(cli.cli, ['show', "-vv"])
|
||||
assert result.exit_code == 1
|
||||
assert any(
|
||||
bool("Could not parse configuration. Perhaps try 'pycalver init'." in r.message)
|
||||
for r in caplog.records
|
||||
)
|
||||
expected_msg = "Could not parse configuration. Perhaps try 'bumpver init'."
|
||||
_debug_records(caplog)
|
||||
assert any(expected_msg in r.message for r in caplog.records)
|
||||
|
||||
|
||||
def test_novcs_nocfg_init(runner, caplog):
|
||||
|
|
@ -197,26 +289,14 @@ def test_novcs_nocfg_init(runner, caplog):
|
|||
# dry mode test
|
||||
result = runner.invoke(cli.cli, ['init', "-vv", "--dry"])
|
||||
assert result.exit_code == 0
|
||||
assert not os.path.exists("pycalver.toml")
|
||||
|
||||
# check logging
|
||||
assert len(caplog.records) == 1
|
||||
log = caplog.records[0]
|
||||
assert log.levelname == 'WARNING'
|
||||
assert "File not found" in log.message
|
||||
assert not os.path.exists("bumpver.toml")
|
||||
|
||||
# non dry mode
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# check logging
|
||||
assert len(caplog.records) == 2
|
||||
log = caplog.records[1]
|
||||
assert log.levelname == 'WARNING'
|
||||
assert "File not found" in log.message
|
||||
|
||||
assert os.path.exists("pycalver.toml")
|
||||
with pl.Path("pycalver.toml").open(mode="r", encoding="utf-8") as fobj:
|
||||
assert os.path.exists("bumpver.toml")
|
||||
with pl.Path("bumpver.toml").open(mode="r", encoding="utf-8") as fobj:
|
||||
cfg_content = fobj.read()
|
||||
|
||||
base_str = config.DEFAULT_TOML_BASE_TMPL.format(initial_version=config._initial_version())
|
||||
|
|
@ -224,6 +304,7 @@ def test_novcs_nocfg_init(runner, caplog):
|
|||
assert config.DEFAULT_TOML_README_MD_STR in cfg_content
|
||||
|
||||
result = runner.invoke(cli.cli, ['show', "-vv"])
|
||||
_debug_records(caplog)
|
||||
assert result.exit_code == 0
|
||||
assert f"Current Version: {config._initial_version()}\n" in result.output
|
||||
assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output
|
||||
|
|
@ -232,8 +313,8 @@ def test_novcs_nocfg_init(runner, caplog):
|
|||
assert result.exit_code == 1
|
||||
|
||||
# check logging
|
||||
assert len(caplog.records) == 3
|
||||
log = caplog.records[2]
|
||||
assert len(caplog.records) == 1
|
||||
log = caplog.records[0]
|
||||
assert log.levelname == 'ERROR'
|
||||
assert "Configuration already initialized" in log.message
|
||||
|
||||
|
|
@ -258,9 +339,10 @@ def test_novcs_setupcfg_init(runner):
|
|||
assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output
|
||||
|
||||
|
||||
def test_novcs_pyproject_init(runner):
|
||||
def test_novcs_pyproject_init(runner, caplog):
|
||||
_add_project_files("README.md", "pyproject.toml")
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
_debug_records(caplog)
|
||||
assert result.exit_code == 0
|
||||
|
||||
with pl.Path("pyproject.toml").open(mode="r", encoding="utf-8") as fobj:
|
||||
|
|
@ -288,33 +370,57 @@ def _vcs_init(vcs, files=("README.md",)):
|
|||
shell(f"{vcs}", "commit", "-m", "initial commit")
|
||||
|
||||
|
||||
def test_git_init(runner):
|
||||
_today = dt.datetime.utcnow().date()
|
||||
|
||||
|
||||
DEFAULT_VERSION_PATTERNS = [
|
||||
('"vYYYY0M.BUILD[-TAG]"' , _today.strftime("v%Y%m.1001-alpha"), _today.strftime("%Y%m.1001a0")),
|
||||
('"vYYYY.BUILD[-TAG]"' , _today.strftime("v%Y.1001-alpha"), _today.strftime("%Y.1001a0")),
|
||||
('"{pycalver}"' , _today.strftime("v%Y%m.1001-alpha"), _today.strftime("%Y%m.1001a0")),
|
||||
('"v{year}{build}{release}"', _today.strftime("v%Y.1001-alpha"), _today.strftime("%Y.1001a0")),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS)
|
||||
def test_git_init(runner, version_pattern, cur_version, cur_pep440):
|
||||
_add_project_files("README.md")
|
||||
_vcs_init("git")
|
||||
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
_update_config_val(
|
||||
"bumpver.toml",
|
||||
version_pattern=version_pattern,
|
||||
current_version='"' + cur_version + '"',
|
||||
)
|
||||
|
||||
result = runner.invoke(cli.cli, ['show'])
|
||||
assert result.exit_code == 0
|
||||
assert f"Current Version: {config._initial_version()}\n" in result.output
|
||||
assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output
|
||||
assert f"Current Version: {cur_version}\n" in result.output
|
||||
|
||||
|
||||
def test_hg_init(runner):
|
||||
@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS)
|
||||
def test_hg_init(runner, version_pattern, cur_version, cur_pep440):
|
||||
_add_project_files("README.md")
|
||||
_vcs_init("hg")
|
||||
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
_update_config_val(
|
||||
"bumpver.toml",
|
||||
version_pattern=version_pattern,
|
||||
current_version='"' + cur_version + '"',
|
||||
)
|
||||
|
||||
result = runner.invoke(cli.cli, ['show'])
|
||||
assert result.exit_code == 0
|
||||
assert f"Current Version: {config._initial_version()}\n" in result.output
|
||||
assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output
|
||||
assert f"Current Version: {cur_version}\n" in result.output
|
||||
|
||||
|
||||
def test_git_tag_eval(runner):
|
||||
@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS)
|
||||
def test_v1_git_tag_eval(runner, version_pattern, cur_version, cur_pep440):
|
||||
_add_project_files("README.md")
|
||||
_vcs_init("git")
|
||||
|
||||
|
|
@ -322,19 +428,25 @@ def test_git_tag_eval(runner):
|
|||
# we set in the vcs, which should take precedence.
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
initial_version = config._initial_version()
|
||||
tag_version = initial_version.replace(".0001-alpha", ".0123-beta")
|
||||
tag_version_pep440 = tag_version[1:7] + ".123b0"
|
||||
|
||||
_update_config_val(
|
||||
"bumpver.toml",
|
||||
version_pattern=version_pattern,
|
||||
current_version='"' + cur_version + '"',
|
||||
)
|
||||
|
||||
tag_version = cur_version.replace(".1001-alpha", ".1123-beta")
|
||||
assert tag_version != cur_version
|
||||
|
||||
shell("git", "tag", "--annotate", tag_version, "--message", f"bump version to {tag_version}")
|
||||
|
||||
result = runner.invoke(cli.cli, ['show', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
assert f"Current Version: {tag_version}\n" in result.output
|
||||
assert f"PEP440 : {tag_version_pep440}\n" in result.output
|
||||
|
||||
|
||||
def test_hg_tag_eval(runner):
|
||||
@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS)
|
||||
def test_hg_tag_eval(runner, version_pattern, cur_version, cur_pep440):
|
||||
_add_project_files("README.md")
|
||||
_vcs_init("hg")
|
||||
|
||||
|
|
@ -342,9 +454,15 @@ def test_hg_tag_eval(runner):
|
|||
# we set in the vcs, which should take precedence.
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
initial_version = config._initial_version()
|
||||
tag_version = initial_version.replace(".0001-alpha", ".0123-beta")
|
||||
tag_version_pep440 = tag_version[1:7] + ".123b0"
|
||||
|
||||
_update_config_val(
|
||||
"bumpver.toml",
|
||||
version_pattern=version_pattern,
|
||||
current_version='"' + cur_version + '"',
|
||||
)
|
||||
|
||||
tag_version = cur_version.replace(".1001-alpha", ".1123-beta")
|
||||
tag_version_pep440 = tag_version[1:].split(".")[0] + ".1123b0"
|
||||
|
||||
shell("hg", "tag", tag_version, "--message", f"bump version to {tag_version}")
|
||||
|
||||
|
|
@ -354,87 +472,113 @@ def test_hg_tag_eval(runner):
|
|||
assert f"PEP440 : {tag_version_pep440}\n" in result.output
|
||||
|
||||
|
||||
def test_novcs_bump(runner):
|
||||
@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS)
|
||||
def test_novcs_bump(runner, version_pattern, cur_version, cur_pep440):
|
||||
_add_project_files("README.md")
|
||||
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
result = runner.invoke(cli.cli, ['bump', "-vv"])
|
||||
_update_config_val(
|
||||
"bumpver.toml",
|
||||
version_pattern=version_pattern,
|
||||
current_version='"' + cur_version + '"',
|
||||
)
|
||||
|
||||
with pl.Path("README.md").open(mode="r") as fobj:
|
||||
content = fobj.read()
|
||||
|
||||
result = runner.invoke(cli.cli, ['update', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
calver = config._initial_version()[:7]
|
||||
calver = cur_version.split(".")[0]
|
||||
|
||||
with pl.Path("README.md").open() as fobj:
|
||||
content = fobj.read()
|
||||
assert calver + ".0002-alpha !\n" in content
|
||||
assert calver[1:] + ".2a0 !\n" in content
|
||||
assert calver + ".1002-alpha !\n" in content
|
||||
assert calver[1:] + ".1002a0 !]\n" in content
|
||||
|
||||
result = runner.invoke(cli.cli, ['bump', "-vv", "--release", "beta"])
|
||||
result = runner.invoke(cli.cli, ['update', "-vv", "--tag", "beta"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
with pl.Path("README.md").open() as fobj:
|
||||
content = fobj.read()
|
||||
assert calver + ".0003-beta !\n" in content
|
||||
assert calver[1:] + ".3b0 !\n" in content
|
||||
assert calver + ".1003-beta !\n" in content
|
||||
assert calver[1:] + ".1003b0 !]\n" in content
|
||||
|
||||
|
||||
def test_git_bump(runner):
|
||||
@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS)
|
||||
def test_git_bump(runner, caplog, version_pattern, cur_version, cur_pep440):
|
||||
_add_project_files("README.md")
|
||||
_vcs_init("git")
|
||||
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
shell("git", "add", "pycalver.toml")
|
||||
_update_config_val(
|
||||
"bumpver.toml",
|
||||
version_pattern=version_pattern,
|
||||
current_version='"' + cur_version + '"',
|
||||
)
|
||||
|
||||
shell("git", "add", "bumpver.toml")
|
||||
shell("git", "commit", "-m", "initial commit")
|
||||
|
||||
result = runner.invoke(cli.cli, ['bump', "-vv"])
|
||||
result = runner.invoke(cli.cli, ['update', "-vv"])
|
||||
_debug_records(caplog)
|
||||
assert result.exit_code == 0
|
||||
|
||||
calver = config._initial_version()[:7]
|
||||
calver = cur_version.split(".")[0]
|
||||
|
||||
with pl.Path("README.md").open() as fobj:
|
||||
content = fobj.read()
|
||||
assert calver + ".0002-alpha !\n" in content
|
||||
assert calver + ".1002-alpha !\n" in content
|
||||
|
||||
|
||||
def test_hg_bump(runner):
|
||||
@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS)
|
||||
def test_hg_bump(runner, version_pattern, cur_version, cur_pep440):
|
||||
_add_project_files("README.md")
|
||||
_vcs_init("hg")
|
||||
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
shell("hg", "add", "pycalver.toml")
|
||||
_update_config_val(
|
||||
"bumpver.toml",
|
||||
version_pattern=version_pattern,
|
||||
current_version='"' + cur_version + '"',
|
||||
)
|
||||
|
||||
shell("hg", "add", "bumpver.toml")
|
||||
shell("hg", "commit", "-m", "initial commit")
|
||||
|
||||
result = runner.invoke(cli.cli, ['bump', "-vv"])
|
||||
result = runner.invoke(cli.cli, ['update', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
calver = config._initial_version()[:7]
|
||||
calver = cur_version.split(".")[0]
|
||||
|
||||
with pl.Path("README.md").open() as fobj:
|
||||
content = fobj.read()
|
||||
assert calver + ".0002-alpha !\n" in content
|
||||
assert calver + ".1002-alpha !\n" in content
|
||||
|
||||
|
||||
def test_empty_git_bump(runner, caplog):
|
||||
shell("git", "init")
|
||||
with pl.Path("setup.cfg").open(mode="w") as fobj:
|
||||
fobj.write("")
|
||||
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
with pl.Path("setup.cfg").open(mode="r") as fobj:
|
||||
default_cfg_data = fobj.read()
|
||||
|
||||
assert "[pycalver]\n" in default_cfg_data
|
||||
assert "[bumpver]\n" in default_cfg_data
|
||||
assert "\ncurrent_version = " in default_cfg_data
|
||||
assert "\n[pycalver:file_patterns]\n" in default_cfg_data
|
||||
assert "\n[bumpver:file_patterns]\n" in default_cfg_data
|
||||
assert "\nsetup.cfg =\n" in default_cfg_data
|
||||
|
||||
result = runner.invoke(cli.cli, ['bump'])
|
||||
result = runner.invoke(cli.cli, ['update'])
|
||||
|
||||
assert any(("working directory is not clean" in r.message) for r in caplog.records)
|
||||
assert any(("setup.cfg" in r.message) for r in caplog.records)
|
||||
|
|
@ -444,18 +588,19 @@ def test_empty_hg_bump(runner, caplog):
|
|||
shell("hg", "init")
|
||||
with pl.Path("setup.cfg").open(mode="w") as fobj:
|
||||
fobj.write("")
|
||||
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
with pl.Path("setup.cfg").open(mode="r") as fobj:
|
||||
default_cfg_text = fobj.read()
|
||||
|
||||
assert "[pycalver]\n" in default_cfg_text
|
||||
assert "[bumpver]\n" in default_cfg_text
|
||||
assert "\ncurrent_version = " in default_cfg_text
|
||||
assert "\n[pycalver:file_patterns]\n" in default_cfg_text
|
||||
assert "\n[bumpver:file_patterns]\n" in default_cfg_text
|
||||
assert "\nsetup.cfg =\n" in default_cfg_text
|
||||
|
||||
result = runner.invoke(cli.cli, ['bump'])
|
||||
result = runner.invoke(cli.cli, ['update'])
|
||||
|
||||
assert any(("working directory is not clean" in r.message) for r in caplog.records)
|
||||
assert any(("setup.cfg" in r.message) for r in caplog.records)
|
||||
|
|
@ -478,36 +623,48 @@ setup.cfg =
|
|||
"""
|
||||
|
||||
|
||||
def test_bump_semver_warning(runner, caplog):
|
||||
DEFAULT_SEMVER_PATTERNS = [
|
||||
'"{semver}"',
|
||||
'"MAJOR.MINOR.PATCH"',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version_pattern", DEFAULT_SEMVER_PATTERNS)
|
||||
def test_v1_bump_semver_warning(runner, caplog, version_pattern):
|
||||
_add_project_files("README.md")
|
||||
|
||||
with pl.Path("setup.cfg").open(mode="w") as fobj:
|
||||
fobj.write(SETUP_CFG_SEMVER_FIXTURE)
|
||||
|
||||
_update_config_val("setup.cfg", version_pattern=version_pattern)
|
||||
|
||||
_vcs_init("hg", files=["README.md", "setup.cfg"])
|
||||
|
||||
result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry"])
|
||||
result = runner.invoke(cli.cli, ['update', "-vv", "-n", "--dry"])
|
||||
assert result.exit_code == 1
|
||||
|
||||
assert any("version did not change" in r.message for r in caplog.records)
|
||||
assert any("--major/--minor/--patch required" in r.message for r in caplog.records)
|
||||
assert any("[--major/--minor/--patch] required" in r.message for r in caplog.records)
|
||||
|
||||
result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry", "--patch"])
|
||||
result = runner.invoke(cli.cli, ['update', "-vv", "-n", "--dry", "--patch"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_bump_semver_diff(runner, caplog):
|
||||
@pytest.mark.parametrize("version_pattern", DEFAULT_SEMVER_PATTERNS)
|
||||
def test_v1_bump_semver_diff(runner, caplog, version_pattern):
|
||||
_add_project_files("README.md")
|
||||
|
||||
with pl.Path("setup.cfg").open(mode="w") as fobj:
|
||||
fobj.write(SETUP_CFG_SEMVER_FIXTURE)
|
||||
|
||||
_update_config_val("setup.cfg", version_pattern=version_pattern)
|
||||
|
||||
_vcs_init("hg", files=["README.md", "setup.cfg"])
|
||||
|
||||
cases = [("--major", "1.0.0"), ("--minor", "0.2.0"), ("--patch", "0.1.1")]
|
||||
|
||||
for flag, expected in cases:
|
||||
result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry", flag])
|
||||
result = runner.invoke(cli.cli, ['update', "-vv", "-n", "--dry", flag])
|
||||
assert result.exit_code == 0
|
||||
assert len(caplog.records) == 0
|
||||
|
||||
|
|
@ -516,3 +673,274 @@ def test_bump_semver_diff(runner, caplog):
|
|||
assert "+++ setup.cfg" in out_lines
|
||||
assert "-current_version = \"0.1.0\"" in out_lines
|
||||
assert f"+current_version = \"{expected}\"" in out_lines
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS)
|
||||
def test_get_diff(runner, version_pattern, cur_version, cur_pep440):
|
||||
_add_project_files("README.md", "setup.cfg")
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
if len(cur_pep440) == 11:
|
||||
old_version = "v2017.1002-alpha"
|
||||
old_pep440 = "2017.1002a0"
|
||||
elif len(cur_pep440) == 13:
|
||||
old_version = "v201707.1002-alpha"
|
||||
old_pep440 = "201707.1002a0"
|
||||
else:
|
||||
assert False, len(cur_pep440)
|
||||
|
||||
_update_config_val(
|
||||
"setup.cfg",
|
||||
version_pattern=version_pattern,
|
||||
current_version='"' + old_version + '"',
|
||||
)
|
||||
_, cfg = config.init()
|
||||
diff_str = cli.get_diff(cfg, cur_version)
|
||||
diff_lines = set(diff_str.splitlines())
|
||||
|
||||
assert f"- Hello World {old_version} !" in diff_lines
|
||||
assert f"+ Hello World {cur_version} !" in diff_lines
|
||||
|
||||
assert f"- [aka. {old_pep440} !]" in diff_lines
|
||||
assert f"+ [aka. {cur_pep440} !]" in diff_lines
|
||||
|
||||
assert f'-current_version = "{old_version}"' in diff_lines
|
||||
assert f'+current_version = "{cur_version}"' in diff_lines
|
||||
|
||||
|
||||
WEEKNUM_TEST_CASES = [
|
||||
# 2020-12-26 Sat
|
||||
("2020-12-26", "YYYY.0W", "2020.51"),
|
||||
("2020-12-26", "YYYY.0U", "2020.51"),
|
||||
("2020-12-26", "GGGG.0V", "2020.52"),
|
||||
# 2020-12-27 Sun
|
||||
("2020-12-27", "YYYY.0W", "2020.51"),
|
||||
("2020-12-27", "YYYY.0U", "2020.52"),
|
||||
("2020-12-27", "GGGG.0V", "2020.52"),
|
||||
# 2020-12-28 Mon
|
||||
("2020-12-28", "YYYY.0W", "2020.52"),
|
||||
("2020-12-28", "YYYY.0U", "2020.52"),
|
||||
("2020-12-28", "GGGG.0V", "2020.53"),
|
||||
# 2020-12-29 Tue
|
||||
("2020-12-29", "YYYY.0W", "2020.52"),
|
||||
("2020-12-29", "YYYY.0U", "2020.52"),
|
||||
("2020-12-29", "GGGG.0V", "2020.53"),
|
||||
# 2020-12-30 Wed
|
||||
("2020-12-30", "YYYY.0W", "2020.52"),
|
||||
("2020-12-30", "YYYY.0U", "2020.52"),
|
||||
("2020-12-30", "GGGG.0V", "2020.53"),
|
||||
# 2020-12-31 Thu
|
||||
("2020-12-31", "YYYY.0W", "2020.52"),
|
||||
("2020-12-31", "YYYY.0U", "2020.52"),
|
||||
("2020-12-31", "GGGG.0V", "2020.53"),
|
||||
# 2021-01-01 Fri
|
||||
("2021-01-01", "YYYY.0W", "2021.00"),
|
||||
("2021-01-01", "YYYY.0U", "2021.00"),
|
||||
("2021-01-01", "GGGG.0V", "2020.53"),
|
||||
# 2021-01-02 Sat
|
||||
("2021-01-02", "YYYY.0W", "2021.00"),
|
||||
("2021-01-02", "YYYY.0U", "2021.00"),
|
||||
("2021-01-02", "GGGG.0V", "2020.53"),
|
||||
# 2021-01-03 Sun
|
||||
("2021-01-03", "YYYY.0W", "2021.00"),
|
||||
("2021-01-03", "YYYY.0U", "2021.01"),
|
||||
("2021-01-03", "GGGG.0V", "2020.53"),
|
||||
# 2021-01-04 Mon
|
||||
("2021-01-04", "YYYY.0W", "2021.01"),
|
||||
("2021-01-04", "YYYY.0U", "2021.01"),
|
||||
("2021-01-04", "GGGG.0V", "2021.01"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("date, pattern, expected", WEEKNUM_TEST_CASES)
|
||||
def test_weeknum(date, pattern, expected, runner):
|
||||
cmd = shlex.split(f"test -vv --date {date} 2020.40 {pattern}")
|
||||
result = runner.invoke(cli.cli, cmd)
|
||||
assert result.exit_code == 0
|
||||
assert "New Version: " + expected in result.output
|
||||
|
||||
|
||||
def test_hg_commit_message(runner, caplog):
|
||||
_add_project_files("README.md", "setup.cfg")
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
commit_message = """
|
||||
"bump from {old_version} ({old_version_pep440}) to {new_version} ({new_version_pep440})"
|
||||
"""
|
||||
_update_config_val(
|
||||
"setup.cfg",
|
||||
current_version='"v2019.1001-alpha"',
|
||||
version_pattern="vYYYY.BUILD[-TAG]",
|
||||
commit_message=commit_message.strip(),
|
||||
)
|
||||
|
||||
_vcs_init("hg", ["README.md", "setup.cfg"])
|
||||
assert len(caplog.records) > 0
|
||||
|
||||
result = runner.invoke(cli.cli, ['update', "-vv", "--pin-date", "--tag", "beta"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
tags = shell("hg", "tags").decode("utf-8")
|
||||
assert "v2019.1002-beta" in tags
|
||||
|
||||
commits = shell(*shlex.split("hg log -l 2")).decode("utf-8").split("\n\n")
|
||||
|
||||
expected = "bump from v2019.1001-alpha (2019.1001a0) to v2019.1002-beta (2019.1002b0)"
|
||||
summary = commits[1].split("summary:")[-1]
|
||||
assert expected in summary
|
||||
|
||||
|
||||
def test_git_commit_message(runner, caplog):
|
||||
_add_project_files("README.md", "setup.cfg")
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
commit_message = """
|
||||
"bump: {old_version} ({old_version_pep440}) -> {new_version} ({new_version_pep440})"
|
||||
"""
|
||||
_update_config_val(
|
||||
"setup.cfg",
|
||||
current_version='"v2019.1001-alpha"',
|
||||
version_pattern="vYYYY.BUILD[-TAG]",
|
||||
commit_message=commit_message.strip(),
|
||||
)
|
||||
|
||||
_vcs_init("git", ["README.md", "setup.cfg"])
|
||||
assert len(caplog.records) > 0
|
||||
|
||||
result = runner.invoke(cli.cli, ['update', "-vv", "--pin-date", "--tag", "beta"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
tags = shell("git", "tag", "--list").decode("utf-8")
|
||||
assert "v2019.1002-beta" in tags
|
||||
|
||||
commits = shell(*shlex.split("git log -l 2")).decode("utf-8").split("\n\n")
|
||||
|
||||
expected = "bump: v2019.1001-alpha (2019.1001a0) -> v2019.1002-beta (2019.1002b0)"
|
||||
assert expected in commits[1]
|
||||
|
||||
|
||||
def test_grep(runner):
|
||||
_add_project_files("README.md")
|
||||
|
||||
search_re = r"^\s+2:\s+Hello World v2017\.1002-alpha !"
|
||||
|
||||
cmd1 = r'grep "vYYYY.BUILD[-TAG]" README.md'
|
||||
result1 = runner.invoke(cli.cli, shlex.split(cmd1))
|
||||
assert result1.exit_code == 0
|
||||
assert re.search(search_re, result1.output, flags=re.MULTILINE)
|
||||
|
||||
cmd2 = r'grep --version-pattern "vYYYY.BUILD[-TAG]" "{version}" README.md'
|
||||
result2 = runner.invoke(cli.cli, shlex.split(cmd2))
|
||||
assert result2.exit_code == 0
|
||||
assert re.search(search_re, result2.output, flags=re.MULTILINE)
|
||||
|
||||
assert result1.output == result2.output
|
||||
|
||||
search_re = r"^\s+3:\s+\[aka\. 2017\.1002a0 \!\]"
|
||||
|
||||
cmd3 = r'grep "\[aka. YYYY.BLD[PYTAGNUM] \!\]" README.md'
|
||||
result3 = runner.invoke(cli.cli, shlex.split(cmd3))
|
||||
assert result3.exit_code == 0
|
||||
assert re.search(search_re, result3.output, flags=re.MULTILINE)
|
||||
|
||||
cmd4 = r'grep --version-pattern "vYYYY.BUILD[-TAG]" "\[aka. {pep440_version} \!\]" README.md'
|
||||
result4 = runner.invoke(cli.cli, shlex.split(cmd4))
|
||||
assert result4.exit_code == 0
|
||||
assert re.search(search_re, result4.output, flags=re.MULTILINE)
|
||||
|
||||
assert result3.output == result4.output
|
||||
|
||||
|
||||
SETUP_CFG_MULTIMATCH_FILE_PATTERNS_FIXTURE = r"""
|
||||
[pycalver]
|
||||
current_version = "v201701.1002-alpha"
|
||||
version_pattern = "{pycalver}"
|
||||
|
||||
[pycalver:file_patterns]
|
||||
setup.cfg =
|
||||
current_version = "{version}"
|
||||
README.md =
|
||||
Hello World {version} !
|
||||
README.* =
|
||||
[aka. {pep440_version} !]
|
||||
"""
|
||||
|
||||
|
||||
def test_multimatch_file_patterns(runner):
|
||||
_add_project_files("README.md")
|
||||
with pl.Path("setup.cfg").open(mode="w", encoding="utf-8") as fobj:
|
||||
fobj.write(SETUP_CFG_MULTIMATCH_FILE_PATTERNS_FIXTURE)
|
||||
|
||||
result = runner.invoke(cli.cli, ['update', '--tag', 'beta', '--date', "2020-11-22"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
with pl.Path("README.md").open(mode="r", encoding="utf-8") as fobj:
|
||||
content = fobj.read()
|
||||
|
||||
assert "Hello World v202011.1003-beta !" in content
|
||||
assert "[aka. 202011.1003b0 !]" in content
|
||||
|
||||
|
||||
def _kwargs(year, month, minor=False):
|
||||
return {'date': dt.date(year, month, 1), 'minor': minor}
|
||||
|
||||
|
||||
ROLLOVER_TEST_CASES = [
|
||||
# v1 cases
|
||||
["{year}.{month}.{MINOR}", "2020.10.3", "2020.10.4", _kwargs(2020, 10, True)],
|
||||
["{year}.{month}.{MINOR}", "2020.10.3", None, _kwargs(2020, 10, False)],
|
||||
["{year}.{month}.{MINOR}", "2020.10.3", "2020.11.4", _kwargs(2020, 11, True)],
|
||||
["{year}.{month}.{MINOR}", "2020.10.3", "2020.11.3", _kwargs(2020, 11, False)],
|
||||
# v2 cases
|
||||
["YYYY.MM.MINOR" , "2020.10.3", "2020.10.4", _kwargs(2020, 10, True)],
|
||||
["YYYY.MM.MINOR" , "2020.10.3", None, _kwargs(2020, 10, False)],
|
||||
["YYYY.MM.MINOR" , "2020.10.3", "2020.11.0", _kwargs(2020, 11, True)],
|
||||
["YYYY.MM.MINOR" , "2020.10.3", "2020.11.0", _kwargs(2020, 11, False)],
|
||||
["YYYY.MM[.MINOR]", "2020.10.3", "2020.10.4", _kwargs(2020, 10, True)],
|
||||
["YYYY.MM[.MINOR]", "2020.10.3", "2020.11", _kwargs(2020, 11, False)],
|
||||
["YYYY.MM.MINOR" , "2020.10.3", "2021.10.0", _kwargs(2021, 10, False)],
|
||||
# incr0/incr1 part
|
||||
["YYYY.MM.INC0", "2020.10.3", "2020.10.4", _kwargs(2020, 10)],
|
||||
["YYYY.MM.INC0", "2020.10.3", "2020.11.0", _kwargs(2020, 11)],
|
||||
["YYYY.MM.INC0", "2020.10.3", "2021.10.0", _kwargs(2021, 10)],
|
||||
["YYYY.MM.INC1", "2020.10.3", "2020.10.4", _kwargs(2020, 10)],
|
||||
["YYYY.MM.INC1", "2020.10.3", "2020.11.1", _kwargs(2020, 11)],
|
||||
["YYYY.MM.INC1", "2020.10.3", "2021.10.1", _kwargs(2021, 10)],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version_pattern, old_version, expected, kwargs", ROLLOVER_TEST_CASES)
|
||||
def test_rollover(version_pattern, old_version, expected, kwargs):
|
||||
new_version = cli.incr_dispatch(old_version, raw_pattern=version_pattern, **kwargs)
|
||||
if new_version is None:
|
||||
assert expected is None
|
||||
else:
|
||||
assert new_version == expected
|
||||
|
||||
|
||||
def test_get_latest_vcs_version_tag(runner):
|
||||
result = runner.invoke(cli.cli, ['init', "-vv"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
_update_config_val("bumpver.toml", push="false")
|
||||
_update_config_val("bumpver.toml", current_version='"0.1.8"')
|
||||
_update_config_val("bumpver.toml", version_pattern='"MAJOR.MINOR.PATCH"')
|
||||
|
||||
_vcs_init("git", files=["bumpver.toml"])
|
||||
|
||||
result = runner.invoke(cli.cli, ['update', "--patch"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
_, cfg = config.init()
|
||||
latest_version = cli.get_latest_vcs_version_tag(cfg, fetch=False)
|
||||
assert latest_version == "0.1.9"
|
||||
|
||||
result = runner.invoke(cli.cli, ['update', "--patch"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
_, cfg = config.init()
|
||||
latest_version = cli.get_latest_vcs_version_tag(cfg, fetch=False)
|
||||
assert latest_version == "0.1.10"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import io
|
||||
from test import util
|
||||
|
||||
from bumpver import config
|
||||
|
||||
# pylint:disable=redefined-outer-name ; pytest fixtures
|
||||
# pylint:disable=protected-access ; allowed for test code
|
||||
|
||||
import io
|
||||
|
||||
from pycalver import config
|
||||
|
||||
from . import util
|
||||
|
||||
PYCALVER_TOML_FIXTURE_1 = """
|
||||
[pycalver]
|
||||
current_version = "v201808.0123-alpha"
|
||||
current_version = "v2020.1003-alpha"
|
||||
version_pattern = "vYYYY.BUILD[-TAG]"
|
||||
commit = true
|
||||
tag = true
|
||||
push = true
|
||||
|
|
@ -29,6 +36,9 @@ PYCALVER_TOML_FIXTURE_2 = """
|
|||
[pycalver]
|
||||
current_version = "1.2.3"
|
||||
version_pattern = "{semver}"
|
||||
commit = false
|
||||
tag = false
|
||||
push = false
|
||||
|
||||
[pycalver.file_patterns]
|
||||
"README.md" = [
|
||||
|
|
@ -40,15 +50,34 @@ version_pattern = "{semver}"
|
|||
]
|
||||
"""
|
||||
|
||||
CALVER_TOML_FIXTURE_3 = """
|
||||
[bumpver]
|
||||
current_version = "v201808.0123-alpha"
|
||||
version_pattern = "vYYYY0M.BUILD[-TAG]"
|
||||
commit = true
|
||||
tag = true
|
||||
push = true
|
||||
|
||||
[bumpver.file_patterns]
|
||||
"README.md" = [
|
||||
"{version}",
|
||||
"{pep440_version}",
|
||||
]
|
||||
"bumpver.toml" = [
|
||||
'current_version = "{version}"',
|
||||
]
|
||||
"""
|
||||
|
||||
|
||||
SETUP_CFG_FIXTURE = """
|
||||
[pycalver]
|
||||
[bumpver]
|
||||
current_version = "v201808.0456-beta"
|
||||
version_pattern = "vYYYY0M.BUILD[-TAG]"
|
||||
commit = True
|
||||
tag = True
|
||||
push = True
|
||||
|
||||
[pycalver:file_patterns]
|
||||
[bumpver:file_patterns]
|
||||
setup.py =
|
||||
{version}
|
||||
{pep440_version}
|
||||
|
|
@ -57,6 +86,26 @@ setup.cfg =
|
|||
"""
|
||||
|
||||
|
||||
NEW_PATTERN_CFG_FIXTURE = """
|
||||
[bumpver]
|
||||
current_version = "v201808.1456-beta"
|
||||
version_pattern = "vYYYY0M.BUILD[-TAG]"
|
||||
commit_message = "bump version to {new_version}"
|
||||
commit = True
|
||||
tag = True
|
||||
push = True
|
||||
|
||||
[bumpver:file_patterns]
|
||||
setup.py =
|
||||
{version}
|
||||
{pep440_version}
|
||||
setup.cfg =
|
||||
current_version = "{version}"
|
||||
src/project/*.py =
|
||||
Copyright (c) 2018-YYYY
|
||||
"""
|
||||
|
||||
|
||||
def mk_buf(text):
|
||||
buf = io.StringIO()
|
||||
buf.write(text)
|
||||
|
|
@ -64,21 +113,31 @@ def mk_buf(text):
|
|||
return buf
|
||||
|
||||
|
||||
def _parse_raw_patterns_by_filepath(cfg):
|
||||
return {
|
||||
filepath: [pattern.raw_pattern for pattern in patterns]
|
||||
for filepath, patterns in cfg.file_patterns.items()
|
||||
}
|
||||
|
||||
|
||||
def test_parse_toml_1():
|
||||
buf = mk_buf(PYCALVER_TOML_FIXTURE_1)
|
||||
|
||||
raw_cfg = config._parse_toml(buf)
|
||||
cfg = config._parse_config(raw_cfg)
|
||||
|
||||
assert cfg.current_version == "v201808.0123-alpha"
|
||||
assert cfg.version_pattern == "{pycalver}"
|
||||
assert cfg.current_version == "v2020.1003-alpha"
|
||||
assert cfg.version_pattern == "vYYYY.BUILD[-TAG]"
|
||||
assert cfg.commit is True
|
||||
assert cfg.tag is True
|
||||
assert cfg.push is True
|
||||
|
||||
assert "pycalver.toml" in cfg.file_patterns
|
||||
assert cfg.file_patterns["README.md" ] == ["{pycalver}", "{pep440_pycalver}"]
|
||||
assert cfg.file_patterns["pycalver.toml"] == ['current_version = "{pycalver}"']
|
||||
files = set(cfg.file_patterns)
|
||||
assert "pycalver.toml" in files
|
||||
|
||||
raw_patterns_by_path = _parse_raw_patterns_by_filepath(cfg)
|
||||
assert raw_patterns_by_path["README.md" ] == ["vYYYY.BUILD[-TAG]", "YYYY.BLD[PYTAGNUM]"]
|
||||
assert raw_patterns_by_path["pycalver.toml"] == ['current_version = "vYYYY.BUILD[-TAG]"']
|
||||
|
||||
|
||||
def test_parse_toml_2():
|
||||
|
|
@ -94,11 +153,33 @@ def test_parse_toml_2():
|
|||
assert cfg.push is False
|
||||
|
||||
assert "pycalver.toml" in cfg.file_patterns
|
||||
assert cfg.file_patterns["README.md" ] == ["{semver}", "{semver}"]
|
||||
assert cfg.file_patterns["pycalver.toml"] == ['current_version = "{semver}"']
|
||||
|
||||
raw_patterns_by_path = _parse_raw_patterns_by_filepath(cfg)
|
||||
assert raw_patterns_by_path["README.md" ] == ["{semver}", "{semver}"]
|
||||
assert raw_patterns_by_path["pycalver.toml"] == ['current_version = "{semver}"']
|
||||
|
||||
|
||||
def test_parse_cfg():
|
||||
def test_parse_toml_3():
|
||||
buf = mk_buf(CALVER_TOML_FIXTURE_3)
|
||||
|
||||
raw_cfg = config._parse_toml(buf)
|
||||
cfg = config._parse_config(raw_cfg)
|
||||
|
||||
assert cfg.current_version == "v201808.0123-alpha"
|
||||
assert cfg.version_pattern == "vYYYY0M.BUILD[-TAG]"
|
||||
assert cfg.commit is True
|
||||
assert cfg.tag is True
|
||||
assert cfg.push is True
|
||||
|
||||
files = set(cfg.file_patterns)
|
||||
assert "bumpver.toml" in files
|
||||
|
||||
raw_patterns_by_path = _parse_raw_patterns_by_filepath(cfg)
|
||||
assert raw_patterns_by_path["README.md" ] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"]
|
||||
assert raw_patterns_by_path["bumpver.toml"] == ['current_version = "vYYYY0M.BUILD[-TAG]"']
|
||||
|
||||
|
||||
def test_parse_v1_cfg():
|
||||
buf = mk_buf(SETUP_CFG_FIXTURE)
|
||||
|
||||
raw_cfg = config._parse_cfg(buf)
|
||||
|
|
@ -109,9 +190,33 @@ def test_parse_cfg():
|
|||
assert cfg.tag is True
|
||||
assert cfg.push is True
|
||||
|
||||
assert "setup.cfg" in cfg.file_patterns
|
||||
assert cfg.file_patterns["setup.py" ] == ["{pycalver}", "{pep440_pycalver}"]
|
||||
assert cfg.file_patterns["setup.cfg"] == ['current_version = "{pycalver}"']
|
||||
files = set(cfg.file_patterns)
|
||||
assert "setup.cfg" in files
|
||||
|
||||
raw_patterns_by_path = _parse_raw_patterns_by_filepath(cfg)
|
||||
assert raw_patterns_by_path["setup.py" ] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"]
|
||||
assert raw_patterns_by_path["setup.cfg"] == ['current_version = "vYYYY0M.BUILD[-TAG]"']
|
||||
|
||||
|
||||
def test_parse_v2_cfg():
|
||||
buf = mk_buf(NEW_PATTERN_CFG_FIXTURE)
|
||||
|
||||
raw_cfg = config._parse_cfg(buf)
|
||||
cfg = config._parse_config(raw_cfg)
|
||||
assert cfg.current_version == "v201808.1456-beta"
|
||||
assert cfg.commit_message == "bump version to {new_version}"
|
||||
assert cfg.commit is True
|
||||
assert cfg.tag is True
|
||||
assert cfg.push is True
|
||||
|
||||
files = set(cfg.file_patterns)
|
||||
assert "setup.py" in files
|
||||
assert "setup.cfg" in files
|
||||
|
||||
raw_patterns_by_path = _parse_raw_patterns_by_filepath(cfg)
|
||||
assert raw_patterns_by_path["setup.py"] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"]
|
||||
assert raw_patterns_by_path["setup.cfg"] == ['current_version = "vYYYY0M.BUILD[-TAG]"']
|
||||
assert raw_patterns_by_path["src/project/*.py"] == ["Copyright (c) 2018-YYYY"]
|
||||
|
||||
|
||||
def test_parse_default_toml():
|
||||
|
|
@ -142,31 +247,34 @@ def test_parse_default_cfg():
|
|||
|
||||
def test_parse_project_toml():
|
||||
project_path = util.FIXTURES_DIR / "project_a"
|
||||
config_path = util.FIXTURES_DIR / "project_a" / "pycalver.toml"
|
||||
config_path = util.FIXTURES_DIR / "project_a" / "bumpver.toml"
|
||||
config_rel_path = "bumpver.toml"
|
||||
|
||||
with config_path.open() as fobj:
|
||||
config_data = fobj.read()
|
||||
|
||||
assert "v201710.0123-alpha" in config_data
|
||||
assert "v2017.0123-alpha" in config_data
|
||||
|
||||
ctx = config.init_project_ctx(project_path)
|
||||
assert ctx == config.ProjectContext(project_path, config_path, "toml", None)
|
||||
assert ctx == config.ProjectContext(project_path, config_path, config_rel_path, "toml", None)
|
||||
|
||||
cfg = config.parse(ctx)
|
||||
|
||||
assert cfg
|
||||
|
||||
assert cfg.current_version == "v201710.0123-alpha"
|
||||
assert cfg.current_version == "v2017.0123-alpha"
|
||||
assert cfg.commit is True
|
||||
assert cfg.tag is True
|
||||
assert cfg.push is True
|
||||
|
||||
assert set(cfg.file_patterns.keys()) == {"pycalver.toml", "README.md"}
|
||||
files = set(cfg.file_patterns.keys())
|
||||
assert files == {"bumpver.toml", "README.md"}
|
||||
|
||||
|
||||
def test_parse_project_cfg():
|
||||
project_path = util.FIXTURES_DIR / "project_b"
|
||||
config_path = util.FIXTURES_DIR / "project_b" / "setup.cfg"
|
||||
config_rel_path = "setup.cfg"
|
||||
|
||||
with config_path.open() as fobj:
|
||||
config_data = fobj.read()
|
||||
|
|
@ -174,7 +282,7 @@ def test_parse_project_cfg():
|
|||
assert "v201307.0456-beta" in config_data
|
||||
|
||||
ctx = config.init_project_ctx(project_path)
|
||||
assert ctx == config.ProjectContext(project_path, config_path, 'cfg', None)
|
||||
assert ctx == config.ProjectContext(project_path, config_path, config_rel_path, 'cfg', None)
|
||||
|
||||
cfg = config.parse(ctx)
|
||||
|
||||
|
|
@ -194,44 +302,51 @@ def test_parse_project_cfg():
|
|||
|
||||
def test_parse_toml_file(tmpdir):
|
||||
project_path = tmpdir.mkdir("minimal")
|
||||
setup_cfg = project_path.join("pycalver.toml")
|
||||
setup_cfg.write(PYCALVER_TOML_FIXTURE_1)
|
||||
cfg_file = project_path.join("pycalver.toml")
|
||||
cfg_file.write(PYCALVER_TOML_FIXTURE_1)
|
||||
cfg_file_rel_path = "pycalver.toml"
|
||||
|
||||
ctx = config.init_project_ctx(project_path)
|
||||
assert ctx == config.ProjectContext(project_path, setup_cfg, 'toml', None)
|
||||
assert ctx == config.ProjectContext(project_path, cfg_file, cfg_file_rel_path, 'toml', None)
|
||||
|
||||
cfg = config.parse(ctx)
|
||||
|
||||
assert cfg
|
||||
assert cfg.current_version == "v201808.0123-alpha"
|
||||
assert cfg.current_version == "v2020.1003-alpha"
|
||||
assert cfg.version_pattern == "vYYYY.BUILD[-TAG]"
|
||||
assert cfg.tag is True
|
||||
assert cfg.commit is True
|
||||
assert cfg.push is True
|
||||
|
||||
assert cfg.file_patterns == {
|
||||
"README.md" : ["{pycalver}", "{pep440_pycalver}"],
|
||||
"pycalver.toml": ['current_version = "{pycalver}"'],
|
||||
raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg)
|
||||
assert raw_patterns_by_filepath == {
|
||||
"README.md" : ["vYYYY.BUILD[-TAG]", "YYYY.BLD[PYTAGNUM]"],
|
||||
"pycalver.toml": ['current_version = "vYYYY.BUILD[-TAG]"'],
|
||||
}
|
||||
|
||||
|
||||
def test_parse_default_pattern():
|
||||
project_path = util.FIXTURES_DIR / "project_c"
|
||||
config_path = util.FIXTURES_DIR / "project_c" / "pyproject.toml"
|
||||
config_rel_path = "pyproject.toml"
|
||||
|
||||
ctx = config.init_project_ctx(project_path)
|
||||
|
||||
assert ctx == config.ProjectContext(project_path, config_path, "toml", None)
|
||||
assert ctx == config.ProjectContext(project_path, config_path, config_rel_path, "toml", None)
|
||||
|
||||
cfg = config.parse(ctx)
|
||||
|
||||
assert cfg
|
||||
|
||||
assert cfg.current_version == "v2017q1.54321"
|
||||
# assert cfg.version_pattern == "vYYYYqQ.BUILD"
|
||||
assert cfg.version_pattern == "v{year}q{quarter}.{build_no}"
|
||||
assert cfg.commit is True
|
||||
assert cfg.tag is True
|
||||
assert cfg.push is True
|
||||
|
||||
assert cfg.file_patterns == {
|
||||
raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg)
|
||||
assert raw_patterns_by_filepath == {
|
||||
"pyproject.toml": [r'current_version = "v{year}q{quarter}.{build_no}"']
|
||||
}
|
||||
|
||||
|
|
@ -240,21 +355,24 @@ def test_parse_cfg_file(tmpdir):
|
|||
project_path = tmpdir.mkdir("minimal")
|
||||
setup_cfg = project_path.join("setup.cfg")
|
||||
setup_cfg.write(SETUP_CFG_FIXTURE)
|
||||
setup_cfg_rel_path = "setup.cfg"
|
||||
|
||||
ctx = config.init_project_ctx(project_path)
|
||||
assert ctx == config.ProjectContext(project_path, setup_cfg, 'cfg', None)
|
||||
assert ctx == config.ProjectContext(project_path, setup_cfg, setup_cfg_rel_path, 'cfg', None)
|
||||
|
||||
cfg = config.parse(ctx)
|
||||
|
||||
assert cfg
|
||||
assert cfg.current_version == "v201808.0456-beta"
|
||||
assert cfg.version_pattern == "vYYYY0M.BUILD[-TAG]"
|
||||
assert cfg.tag is True
|
||||
assert cfg.commit is True
|
||||
assert cfg.push is True
|
||||
|
||||
assert cfg.file_patterns == {
|
||||
"setup.py" : ["{pycalver}", "{pep440_pycalver}"],
|
||||
"setup.cfg": ['current_version = "{pycalver}"'],
|
||||
raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg)
|
||||
assert raw_patterns_by_filepath == {
|
||||
"setup.py" : ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"],
|
||||
"setup.cfg": ['current_version = "vYYYY0M.BUILD[-TAG]"'],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -288,8 +406,8 @@ def test_parse_missing_version(tmpdir):
|
|||
setup_path.write(
|
||||
"\n".join(
|
||||
(
|
||||
"[pycalver]",
|
||||
# f"current_version = v201808.0001-dev",
|
||||
"[bumpver]",
|
||||
# f"current_version = v201808.1001-dev",
|
||||
"commit = False",
|
||||
)
|
||||
)
|
||||
|
|
@ -304,7 +422,7 @@ def test_parse_missing_version(tmpdir):
|
|||
|
||||
def test_parse_invalid_version(tmpdir):
|
||||
setup_path = tmpdir.mkdir("fail").join("setup.cfg")
|
||||
setup_path.write("\n".join(("[pycalver]", "current_version = 0.1.0", "commit = False")))
|
||||
setup_path.write("\n".join(("[bumpver]", "current_version = 0.1.0", "commit = False")))
|
||||
|
||||
ctx = config.init_project_ctx(setup_path)
|
||||
assert ctx
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
# pylint:disable=protected-access ; allowed for test code
|
||||
|
||||
import random
|
||||
|
||||
from pycalver import lex_id
|
||||
|
||||
|
||||
def test_next_id_basic():
|
||||
assert lex_id.next_id("01") == "02"
|
||||
assert lex_id.next_id("09") == "110"
|
||||
|
||||
|
||||
def test_next_id_overflow():
|
||||
try:
|
||||
prev_id = "9999"
|
||||
next_id = lex_id.next_id(prev_id)
|
||||
assert False, (prev_id, "->", next_id)
|
||||
except OverflowError:
|
||||
pass
|
||||
|
||||
|
||||
def test_next_id_random():
|
||||
for _ in range(1000):
|
||||
prev_id = str(random.randint(1, 100 * 1000))
|
||||
try:
|
||||
next_id = lex_id.next_id(prev_id)
|
||||
assert prev_id < next_id
|
||||
except OverflowError:
|
||||
assert len(prev_id) == prev_id.count("9")
|
||||
|
||||
|
||||
def test_ord_val():
|
||||
assert lex_id.ord_val("1" ) == 1
|
||||
assert lex_id.ord_val("01" ) == 1
|
||||
assert lex_id.ord_val("02" ) == 2
|
||||
assert lex_id.ord_val("09" ) == 9
|
||||
assert lex_id.ord_val("110") == 10
|
||||
|
||||
|
||||
def test_main(capsys):
|
||||
lex_id._main()
|
||||
captured = capsys.readouterr()
|
||||
assert len(captured.err) == 0
|
||||
|
||||
lines = iter(captured.out.splitlines())
|
||||
header = next(lines)
|
||||
|
||||
assert "lexical" in header
|
||||
assert "numerical" in header
|
||||
|
||||
ids = []
|
||||
ord_vals = []
|
||||
|
||||
for line in lines:
|
||||
if "..." in line:
|
||||
continue
|
||||
_id, _ord_val = line.split()
|
||||
|
||||
assert _id.endswith(_ord_val)
|
||||
assert int(_ord_val) == int(_ord_val, 10)
|
||||
|
||||
ids.append(_id.strip())
|
||||
ord_vals.append(int(_ord_val.strip()))
|
||||
|
||||
assert len(ids) > 0
|
||||
assert sorted(ids) == ids
|
||||
assert sorted(ord_vals) == ord_vals
|
||||
|
|
@ -1,4 +1,11 @@
|
|||
from pycalver import parse
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from bumpver import parse
|
||||
from bumpver import v1patterns
|
||||
|
||||
SETUP_PY_FIXTURE = """
|
||||
# setup.py
|
||||
|
|
@ -13,15 +20,15 @@ setuptools.setup(
|
|||
def test_default_parse_patterns():
|
||||
lines = SETUP_PY_FIXTURE.splitlines()
|
||||
patterns = ["{pycalver}", "{pep440_pycalver}"]
|
||||
|
||||
matches = list(parse.iter_matches(lines, patterns))
|
||||
re_patterns = [v1patterns.compile_pattern(p) for p in patterns]
|
||||
matches = list(parse.iter_matches(lines, re_patterns))
|
||||
assert len(matches) == 2
|
||||
|
||||
assert matches[0].lineno == 3
|
||||
assert matches[1].lineno == 6
|
||||
|
||||
assert matches[0].pattern == patterns[0]
|
||||
assert matches[1].pattern == patterns[1]
|
||||
assert matches[0].pattern == re_patterns[0]
|
||||
assert matches[1].pattern == re_patterns[1]
|
||||
|
||||
assert matches[0].match == "v201712.0002-alpha"
|
||||
assert matches[1].match == "201712.2a0"
|
||||
|
|
@ -31,15 +38,15 @@ def test_explicit_parse_patterns():
|
|||
lines = SETUP_PY_FIXTURE.splitlines()
|
||||
|
||||
patterns = ["__version__ = '{pycalver}'", "version='{pep440_pycalver}'"]
|
||||
|
||||
matches = list(parse.iter_matches(lines, patterns))
|
||||
re_patterns = [v1patterns.compile_pattern(p) for p in patterns]
|
||||
matches = list(parse.iter_matches(lines, re_patterns))
|
||||
assert len(matches) == 2
|
||||
|
||||
assert matches[0].lineno == 3
|
||||
assert matches[1].lineno == 6
|
||||
|
||||
assert matches[0].pattern == patterns[0]
|
||||
assert matches[1].pattern == patterns[1]
|
||||
assert matches[0].pattern == re_patterns[0]
|
||||
assert matches[1].pattern == re_patterns[1]
|
||||
|
||||
assert matches[0].match == "__version__ = 'v201712.0002-alpha'"
|
||||
assert matches[1].match == "version='201712.2a0'"
|
||||
|
|
@ -58,15 +65,16 @@ def test_badge_parse_patterns():
|
|||
lines = README_RST_FIXTURE.splitlines()
|
||||
|
||||
patterns = ["badge/CalVer-{calver}{build}-{release}-blue.svg", ":alt: CalVer {pycalver}"]
|
||||
re_patterns = [v1patterns.compile_pattern(p) for p in patterns]
|
||||
matches = list(parse.iter_matches(lines, re_patterns))
|
||||
|
||||
matches = list(parse.iter_matches(lines, patterns))
|
||||
assert len(matches) == 2
|
||||
|
||||
assert matches[0].lineno == 3
|
||||
assert matches[1].lineno == 5
|
||||
|
||||
assert matches[0].pattern == patterns[0]
|
||||
assert matches[1].pattern == patterns[1]
|
||||
assert matches[0].pattern == re_patterns[0]
|
||||
assert matches[1].pattern == re_patterns[1]
|
||||
|
||||
assert matches[0].match == "badge/CalVer-v201809.0002--beta-blue.svg"
|
||||
assert matches[1].match == ":alt: CalVer v201809.0002-beta"
|
||||
|
|
|
|||
|
|
@ -1,17 +1,177 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from pycalver import patterns
|
||||
from bumpver import v1patterns
|
||||
from bumpver import v2patterns
|
||||
|
||||
V2_PART_PATTERN_CASES = [
|
||||
(['YYYY', 'GGGG'], "2020" , "2020"),
|
||||
(['YYYY', 'GGGG'], "" , None),
|
||||
(['YYYY', 'GGGG'], "A020" , None),
|
||||
(['YYYY', 'GGGG'], "020" , None),
|
||||
(['YYYY', 'GGGG'], "12020", None),
|
||||
(['YY' , 'GG' ], "20" , "20"),
|
||||
(['YY' , 'GG' ], "3" , "3"),
|
||||
(['YY' , 'GG' ], "03" , None),
|
||||
(['YY' , 'GG' ], "2X" , None),
|
||||
(['YY' , 'GG' ], "" , None),
|
||||
(['0Y' , '0G' ], "20" , "20"),
|
||||
(['0Y' , '0G' ], "03" , "03"),
|
||||
(['0Y' , '0G' ], "3" , None),
|
||||
(['0Y' , '0G' ], "2X" , None),
|
||||
(['0Y' , '0G' ], "" , None),
|
||||
# quarter
|
||||
(['Q'], "0", None),
|
||||
(['Q'], "1", "1"),
|
||||
(['Q'], "2", "2"),
|
||||
(['Q'], "3", "3"),
|
||||
(['Q'], "4", "4"),
|
||||
(['Q'], "5", None),
|
||||
(['Q'], "X", None),
|
||||
# months
|
||||
(['MM'], "0" , None),
|
||||
(['MM'], "01", None),
|
||||
(['MM'], "1" , "1"),
|
||||
(['MM'], "10", "10"),
|
||||
(['MM'], "12", "12"),
|
||||
(['MM'], "13", None),
|
||||
(['0M'], "00", None),
|
||||
(['0M'], "1" , None),
|
||||
(['0M'], "01", "01"),
|
||||
(['MM'], "10", "10"),
|
||||
(['MM'], "12", "12"),
|
||||
(['MM'], "13", None),
|
||||
# day of month
|
||||
(['DD'], "0" , None),
|
||||
(['DD'], "01", None),
|
||||
(['DD'], "1" , "1"),
|
||||
(['DD'], "10", "10"),
|
||||
(['DD'], "31", "31"),
|
||||
(['DD'], "32", None),
|
||||
(['0D'], "00", None),
|
||||
(['0D'], "1" , None),
|
||||
(['0D'], "01", "01"),
|
||||
(['0D'], "10", "10"),
|
||||
(['0D'], "31", "31"),
|
||||
(['0D'], "32", None),
|
||||
(['DD'], "0" , None),
|
||||
(['DD'], "01", None),
|
||||
(['DD'], "1" , "1"),
|
||||
(['DD'], "10", "10"),
|
||||
(['DD'], "31", "31"),
|
||||
(['DD'], "32", None),
|
||||
(['0D'], "00", None),
|
||||
(['0D'], "1" , None),
|
||||
(['0D'], "01", "01"),
|
||||
(['0D'], "10", "10"),
|
||||
(['0D'], "31", "31"),
|
||||
(['0D'], "32", None),
|
||||
# day of year
|
||||
(['JJJ'], "0" , None),
|
||||
(['JJJ'], "01" , None),
|
||||
(['JJJ'], "1" , "1"),
|
||||
(['JJJ'], "10" , "10"),
|
||||
(['JJJ'], "31" , "31"),
|
||||
(['JJJ'], "32" , "32"),
|
||||
(['JJJ'], "100", "100"),
|
||||
(['JJJ'], "365", "365"),
|
||||
(['JJJ'], "366", "366"),
|
||||
(['JJJ'], "367", None),
|
||||
(['00J'], "000", None),
|
||||
(['00J'], "01" , None),
|
||||
(['00J'], "1" , None),
|
||||
(['00J'], "001", "001"),
|
||||
(['00J'], "010", "010"),
|
||||
(['00J'], "031", "031"),
|
||||
(['00J'], "032", "032"),
|
||||
(['00J'], "100", "100"),
|
||||
(['00J'], "365", "365"),
|
||||
(['00J'], "366", "366"),
|
||||
(['00J'], "367", None),
|
||||
# week numbers
|
||||
(['WW', 'UU'], "00", None),
|
||||
(['WW', 'UU'], "01", None),
|
||||
(['WW', 'UU'], "0" , "0"),
|
||||
(['WW', 'UU'], "1" , "1"),
|
||||
(['WW', 'UU'], "10", "10"),
|
||||
(['WW', 'UU'], "52", "52"),
|
||||
(['WW', 'UU'], "53", None),
|
||||
(['0W', '0U'], "00", "00"),
|
||||
(['0W', '0U'], "01", "01"),
|
||||
(['0W', '0U'], "0" , None),
|
||||
(['0W', '0U'], "1" , None),
|
||||
(['0W', '0U'], "10", "10"),
|
||||
(['0W', '0U'], "52", "52"),
|
||||
(['0W', '0U'], "53", None),
|
||||
(['VV'], "00", None),
|
||||
(['VV'], "01", None),
|
||||
(['VV'], "0" , None),
|
||||
(['VV'], "1" , "1"),
|
||||
(['VV'], "10", "10"),
|
||||
(['VV'], "52", "52"),
|
||||
(['VV'], "53", "53"),
|
||||
(['VV'], "54", None),
|
||||
(['0V'], "00", None),
|
||||
(['0V'], "01", "01"),
|
||||
(['0V'], "0" , None),
|
||||
(['0V'], "1" , None),
|
||||
(['0V'], "10", "10"),
|
||||
(['0V'], "52", "52"),
|
||||
(['0V'], "53", "53"),
|
||||
(['0V'], "54", None),
|
||||
(['MAJOR', 'MINOR', 'PATCH'], "0", "0"),
|
||||
(['TAG' ], "alpha" , "alpha"),
|
||||
(['TAG' ], "alfa" , None),
|
||||
(['TAG' ], "beta" , "beta"),
|
||||
(['TAG' ], "rc" , "rc"),
|
||||
(['TAG' ], "post" , "post"),
|
||||
(['TAG' ], "final" , "final"),
|
||||
(['TAG' ], "latest", None),
|
||||
(['PYTAG'], "a" , "a"),
|
||||
(['PYTAG'], "b" , "b"),
|
||||
(['PYTAG'], "rc" , "rc"),
|
||||
(['PYTAG'], "post" , "post"),
|
||||
(['PYTAG'], "post" , "post"),
|
||||
(['PYTAG'], "x" , None),
|
||||
(['NUM' ], "a" , None),
|
||||
(['NUM' ], "0" , "0"),
|
||||
(['NUM' ], "1" , "1"),
|
||||
(['NUM' ], "10" , "10"),
|
||||
]
|
||||
|
||||
|
||||
def _part_re_by_name(name):
|
||||
return re.compile(patterns.PART_PATTERNS[name])
|
||||
def _compile_part_re(pattern_str):
|
||||
grouped_pattern_str = r"^(?:" + pattern_str + r")$"
|
||||
# print("\n", grouped_pattern_str)
|
||||
return re.compile(grouped_pattern_str, flags=re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("part_name", patterns.PART_PATTERNS.keys())
|
||||
def test_part_compilation(part_name):
|
||||
assert _part_re_by_name(part_name)
|
||||
@pytest.mark.parametrize("parts, testcase, expected", V2_PART_PATTERN_CASES)
|
||||
def test_v2_part_patterns(parts, testcase, expected):
|
||||
for part in parts:
|
||||
part_re = _compile_part_re(v2patterns.PART_PATTERNS[part])
|
||||
match = part_re.match(testcase)
|
||||
if match is None:
|
||||
assert expected is None
|
||||
else:
|
||||
assert match.group(0) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("part_name", v2patterns.PART_PATTERNS.keys())
|
||||
def test_v1_part_compilation(part_name):
|
||||
assert _compile_part_re(v2patterns.PART_PATTERNS[part_name])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("part_name", v1patterns.PART_PATTERNS.keys())
|
||||
def test_v2_part_compilation(part_name):
|
||||
assert _compile_part_re(v1patterns.PART_PATTERNS[part_name])
|
||||
|
||||
|
||||
PATTERN_PART_CASES = [
|
||||
|
|
@ -20,8 +180,8 @@ PATTERN_PART_CASES = [
|
|||
("pep440_pycalver", "201712.0033-alpha" , None),
|
||||
("pycalver" , "v201712.0034" , "v201712.0034"),
|
||||
("pycalver" , "v201712.0035-alpha" , "v201712.0035-alpha"),
|
||||
("pycalver" , "v201712.0036-alpha0", "v201712.0036-alpha"),
|
||||
("pycalver" , "v201712.0037-pre" , "v201712.0037"),
|
||||
("pycalver" , "v201712.0036-alpha0", None),
|
||||
("pycalver" , "v201712.0037-pre" , None), # pre not available for v1 patterns
|
||||
("pycalver" , "201712.38a0" , None),
|
||||
("pycalver" , "201712.0039" , None),
|
||||
("semver" , "1.23.456" , "1.23.456"),
|
||||
|
|
@ -38,34 +198,34 @@ PATTERN_PART_CASES = [
|
|||
("release" , "-dev" , "-dev"),
|
||||
("release" , "-rc" , "-rc"),
|
||||
("release" , "-post" , "-post"),
|
||||
("release" , "-pre" , ""),
|
||||
("release" , "alpha" , ""),
|
||||
("release" , "-pre" , None), # pre not available for v1 patterns
|
||||
("release" , "alpha" , None), # missing dash "-" prefix
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("part_name, line, expected", PATTERN_PART_CASES)
|
||||
def test_re_pattern_parts(part_name, line, expected):
|
||||
part_re = _part_re_by_name(part_name)
|
||||
result = part_re.search(line)
|
||||
def test_v1_re_pattern_parts(part_name, line, expected):
|
||||
part_re = _compile_part_re(v1patterns.PART_PATTERNS[part_name])
|
||||
result = part_re.match(line)
|
||||
if result is None:
|
||||
assert expected is None, (part_name, line)
|
||||
assert expected is None, (part_name, line, result)
|
||||
else:
|
||||
result_val = result.group(0)
|
||||
assert result_val == expected, (part_name, line)
|
||||
|
||||
|
||||
PATTERN_CASES = [
|
||||
PATTERN_V1_CASES = [
|
||||
(r"v{year}.{month}.{MINOR}" , "v2017.11.1" , "v2017.11.1"),
|
||||
(r"v{year}.{month}.{MINOR}" , "v2017.07.12", "v2017.07.12"),
|
||||
(r"v{year}.{month_short}.{MINOR}", "v2017.11.1" , "v2017.11.1"),
|
||||
(r"v{year}.{month_short}.{MINOR}", "v2017.7.12" , "v2017.7.12"),
|
||||
(r"v{year}.{month_short}.{PATCH}", "v2017.11.1" , "v2017.11.1"),
|
||||
(r"v{year}.{month_short}.{PATCH}", "v2017.7.12" , "v2017.7.12"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pattern_str, line, expected", PATTERN_CASES)
|
||||
def test_patterns(pattern_str, line, expected):
|
||||
pattern_re = patterns.compile_pattern(pattern_str)
|
||||
result = pattern_re.search(line)
|
||||
@pytest.mark.parametrize("pattern_str, line, expected", PATTERN_V1_CASES)
|
||||
def test_v1_patterns(pattern_str, line, expected):
|
||||
pattern = v1patterns.compile_pattern(pattern_str)
|
||||
result = pattern.regexp.search(line)
|
||||
if result is None:
|
||||
assert expected is None, (pattern_str, line)
|
||||
else:
|
||||
|
|
@ -73,6 +233,24 @@ def test_patterns(pattern_str, line, expected):
|
|||
assert result_val == expected, (pattern_str, line)
|
||||
|
||||
|
||||
PATTERN_V2_CASES = [
|
||||
("vYYYY.0M.MINOR" , "v2017.11.1" , "v2017.11.1"),
|
||||
("vYYYY.0M.MINOR" , "v2017.07.12", "v2017.07.12"),
|
||||
("YYYY.MM[.PATCH]", "2017.11.1" , "2017.11.1"),
|
||||
("YYYY.MM[.PATCH]", "2017.7.12" , "2017.7.12"),
|
||||
("YYYY.MM[.PATCH]", "2017.7" , "2017.7"),
|
||||
("YYYY0M.BUILD" , "201707.1000", "201707.1000"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pattern_str, line, expected", PATTERN_V2_CASES)
|
||||
def test_v2_patterns(pattern_str, line, expected):
|
||||
pattern = v2patterns.compile_pattern(pattern_str)
|
||||
result = pattern.regexp.search(line)
|
||||
result_val = None if result is None else result.group(0)
|
||||
assert result_val == expected, (pattern_str, line, pattern.regexp.pattern)
|
||||
|
||||
|
||||
CLI_MAIN_FIXTURE = """
|
||||
@click.group()
|
||||
@click.version_option(version="v201812.0123-beta")
|
||||
|
|
@ -81,9 +259,9 @@ CLI_MAIN_FIXTURE = """
|
|||
|
||||
|
||||
def test_pattern_escapes():
|
||||
pattern = 'click.version_option(version="{pycalver}")'
|
||||
pattern_re = patterns.compile_pattern(pattern)
|
||||
match = pattern_re.search(CLI_MAIN_FIXTURE)
|
||||
pattern_str = 'click.version_option(version="{pycalver}")'
|
||||
pattern = v1patterns.compile_pattern(pattern_str)
|
||||
match = pattern.regexp.search(CLI_MAIN_FIXTURE)
|
||||
expected = 'click.version_option(version="v201812.0123-beta")'
|
||||
assert match.group(0) == expected
|
||||
|
||||
|
|
@ -94,8 +272,18 @@ package_metadata = {"name": "mypackage", "version": "v201812.0123-beta"}
|
|||
|
||||
|
||||
def test_curly_escapes():
|
||||
pattern = 'package_metadata = {"name": "mypackage", "version": "{pycalver}"}'
|
||||
pattern_re = patterns.compile_pattern(pattern)
|
||||
match = pattern_re.search(CURLY_BRACE_FIXTURE)
|
||||
pattern_str = 'package_metadata = {"name": "mypackage", "version": "{pycalver}"}'
|
||||
pattern = v1patterns.compile_pattern(pattern_str)
|
||||
match = pattern.regexp.search(CURLY_BRACE_FIXTURE)
|
||||
expected = 'package_metadata = {"name": "mypackage", "version": "v201812.0123-beta"}'
|
||||
assert match.group(0) == expected
|
||||
|
||||
|
||||
def test_part_field_mapping_v2():
|
||||
a_names = set(v2patterns.PATTERN_PART_FIELDS.keys())
|
||||
b_names = set(v2patterns.PART_PATTERNS.keys())
|
||||
|
||||
a_extra_names = a_names - b_names
|
||||
assert not any(a_extra_names), sorted(a_extra_names)
|
||||
b_extra_names = b_names - a_names
|
||||
assert not any(b_extra_names), sorted(b_extra_names)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import copy
|
||||
from test import util
|
||||
|
||||
from bumpver import config
|
||||
from bumpver import rewrite
|
||||
from bumpver import v1rewrite
|
||||
from bumpver import v1version
|
||||
from bumpver import v2rewrite
|
||||
from bumpver import v2version
|
||||
from bumpver import v1patterns
|
||||
from bumpver import v2patterns
|
||||
|
||||
# pylint:disable=protected-access ; allowed for test code
|
||||
|
||||
import copy
|
||||
|
||||
from pycalver import config
|
||||
from pycalver import rewrite
|
||||
from pycalver import version
|
||||
# Fix for Python<3.7
|
||||
# https://stackoverflow.com/a/56935186/62997
|
||||
copy._deepcopy_dispatch[type(re.compile(''))] = lambda r, _: r
|
||||
|
||||
from . import util
|
||||
|
||||
REWRITE_FIXTURE = """
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
|
@ -14,25 +31,60 @@ __version__ = "v201809.0002-beta"
|
|||
"""
|
||||
|
||||
|
||||
def test_rewrite_lines():
|
||||
def test_v1_rewrite_lines_basic():
|
||||
pattern = v1patterns.compile_pattern("{pycalver}", '__version__ = "{pycalver}"')
|
||||
new_vinfo = v1version.parse_version_info("v201911.0003")
|
||||
|
||||
old_lines = REWRITE_FIXTURE.splitlines()
|
||||
patterns = ['__version__ = "{pycalver}"']
|
||||
new_vinfo = version.parse_version_info("v201911.0003")
|
||||
new_lines = rewrite.rewrite_lines(patterns, new_vinfo, old_lines)
|
||||
new_lines = v1rewrite.rewrite_lines([pattern], new_vinfo, old_lines)
|
||||
|
||||
assert len(new_lines) == len(old_lines)
|
||||
assert "v201911.0003" not in "\n".join(old_lines)
|
||||
assert "v201911.0003" in "\n".join(new_lines)
|
||||
|
||||
|
||||
def test_rewrite_final():
|
||||
def test_v1_rewrite_lines():
|
||||
version_pattern = "{pycalver}"
|
||||
new_vinfo = v1version.parse_version_info("v201811.0123-beta", version_pattern)
|
||||
patterns = [v1patterns.compile_pattern(version_pattern, '__version__ = "{pycalver}"')]
|
||||
lines = v1rewrite.rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-beta"'])
|
||||
assert lines == ['__version__ = "v201811.0123-beta"']
|
||||
|
||||
patterns = [v1patterns.compile_pattern(version_pattern, '__version__ = "{pep440_version}"')]
|
||||
lines = v1rewrite.rewrite_lines(patterns, new_vinfo, ['__version__ = "201809.2b0"'])
|
||||
assert lines == ['__version__ = "201811.123b0"']
|
||||
|
||||
|
||||
def test_v2_rewrite_lines():
|
||||
version_pattern = "vYYYY0M.BUILD[-TAG]"
|
||||
new_vinfo = v2version.parse_version_info("v201811.0123-beta", version_pattern)
|
||||
patterns = [v2patterns.compile_pattern(version_pattern, '__version__ = "{version}"')]
|
||||
lines = v2rewrite.rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" '])
|
||||
assert lines == ['__version__ = "v201811.0123-beta" ']
|
||||
|
||||
lines = v2rewrite.rewrite_lines(
|
||||
patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" # comment']
|
||||
)
|
||||
assert lines == ['__version__ = "v201811.0123-beta" # comment']
|
||||
|
||||
patterns = [v2patterns.compile_pattern(version_pattern, '__version__ = "YYYY0M.BLD[PYTAGNUM]"')]
|
||||
old_lines = ['__version__ = "201809.2a0"']
|
||||
lines = v2rewrite.rewrite_lines(patterns, new_vinfo, old_lines)
|
||||
assert lines == ['__version__ = "201811.123b0"']
|
||||
|
||||
|
||||
def test_v1_rewrite_final():
|
||||
# Patterns written with {release_tag} placeholder preserve
|
||||
# the release tag even if the new version is -final
|
||||
|
||||
pattern = v1patterns.compile_pattern(
|
||||
"v{year}{month}.{build_no}-{release_tag}",
|
||||
'__version__ = "v{year}{month}.{build_no}-{release_tag}"',
|
||||
)
|
||||
new_vinfo = v1version.parse_version_info("v201911.0003")
|
||||
|
||||
old_lines = REWRITE_FIXTURE.splitlines()
|
||||
patterns = ['__version__ = "v{year}{month}.{build_no}-{release_tag}"']
|
||||
new_vinfo = version.parse_version_info("v201911.0003")
|
||||
new_lines = rewrite.rewrite_lines(patterns, new_vinfo, old_lines)
|
||||
new_lines = v1rewrite.rewrite_lines([pattern], new_vinfo, old_lines)
|
||||
|
||||
assert len(new_lines) == len(old_lines)
|
||||
assert "v201911.0003" not in "\n".join(old_lines)
|
||||
|
|
@ -43,14 +95,14 @@ def test_rewrite_final():
|
|||
def test_iter_file_paths():
|
||||
with util.Project(project="a") as project:
|
||||
ctx = config.init_project_ctx(project.dir)
|
||||
assert ctx
|
||||
cfg = config.parse(ctx)
|
||||
assert cfg
|
||||
|
||||
file_paths = {
|
||||
str(file_path) for file_path, patterns in rewrite._iter_file_paths(cfg.file_patterns)
|
||||
}
|
||||
_paths_and_patterns = rewrite.iter_path_patterns_items(cfg.file_patterns)
|
||||
file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns}
|
||||
|
||||
assert file_paths == {"pycalver.toml", "README.md"}
|
||||
assert file_paths == {"bumpver.toml", "README.md"}
|
||||
|
||||
|
||||
def test_iter_file_globs():
|
||||
|
|
@ -59,9 +111,8 @@ def test_iter_file_globs():
|
|||
cfg = config.parse(ctx)
|
||||
assert cfg
|
||||
|
||||
file_paths = {
|
||||
str(file_path) for file_path, patterns in rewrite._iter_file_paths(cfg.file_patterns)
|
||||
}
|
||||
_paths_and_patterns = rewrite.iter_path_patterns_items(cfg.file_patterns)
|
||||
file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns}
|
||||
|
||||
assert file_paths == {
|
||||
"setup.cfg",
|
||||
|
|
@ -80,24 +131,30 @@ def test_error_bad_path():
|
|||
|
||||
(project.dir / "setup.py").unlink()
|
||||
try:
|
||||
list(rewrite._iter_file_paths(cfg.file_patterns))
|
||||
list(rewrite.iter_path_patterns_items(cfg.file_patterns))
|
||||
assert False, "expected IOError"
|
||||
except IOError as ex:
|
||||
assert "setup.py" in str(ex)
|
||||
|
||||
|
||||
def test_error_bad_pattern():
|
||||
def test_v1_error_bad_pattern():
|
||||
with util.Project(project="b") as project:
|
||||
ctx = config.init_project_ctx(project.dir)
|
||||
cfg = config.parse(ctx)
|
||||
assert cfg
|
||||
|
||||
patterns = copy.deepcopy(cfg.file_patterns)
|
||||
patterns["setup.py"] = patterns["setup.py"][0] + "invalid"
|
||||
original_pattern = patterns["setup.py"][0]
|
||||
invalid_pattern = v1patterns.compile_pattern(
|
||||
original_pattern.version_pattern,
|
||||
original_pattern.raw_pattern + ".invalid",
|
||||
)
|
||||
patterns["setup.py"] = [invalid_pattern]
|
||||
|
||||
try:
|
||||
new_vinfo = version.parse_version_info("v201809.1234")
|
||||
list(rewrite.diff(new_vinfo, patterns))
|
||||
old_vinfo = v1version.parse_version_info("v201808.0233")
|
||||
new_vinfo = v1version.parse_version_info("v201809.1234")
|
||||
list(v1rewrite.diff(old_vinfo, new_vinfo, patterns))
|
||||
assert False, "expected rewrite.NoPatternMatch"
|
||||
except rewrite.NoPatternMatch as ex:
|
||||
assert "setup.py" in str(ex)
|
||||
|
|
@ -109,23 +166,173 @@ __version__ = "2018.0002-beta"
|
|||
"""
|
||||
|
||||
|
||||
def test_optional_release():
|
||||
old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines()
|
||||
pattern = "{year}.{build_no}{release}"
|
||||
patterns = ['__version__ = "{year}.{build_no}{release}"']
|
||||
def test_v1_optional_release():
|
||||
version_pattern = "{year}.{build_no}{release}"
|
||||
new_vinfo = v1version.parse_version_info("2019.0003", version_pattern)
|
||||
|
||||
new_vinfo = version.parse_version_info("2019.0003", pattern)
|
||||
new_lines = rewrite.rewrite_lines(patterns, new_vinfo, old_lines)
|
||||
raw_pattern = '__version__ = "{year}.{build_no}{release}"'
|
||||
pattern = v1patterns.compile_pattern(version_pattern, raw_pattern)
|
||||
|
||||
old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines()
|
||||
new_lines = v1rewrite.rewrite_lines([pattern], new_vinfo, old_lines)
|
||||
|
||||
assert len(new_lines) == len(old_lines)
|
||||
assert "2019.0003" not in "\n".join(old_lines)
|
||||
new_text = "\n".join(new_lines)
|
||||
assert "2019.0003" in new_text
|
||||
assert "2019.0003" in "\n".join(new_lines)
|
||||
assert '__version__ = "2019.0003"' in "\n".join(new_lines)
|
||||
|
||||
new_vinfo = version.parse_version_info("2019.0004-beta", pattern)
|
||||
new_lines = rewrite.rewrite_lines(patterns, new_vinfo, old_lines)
|
||||
new_vinfo = v1version.parse_version_info("2019.0004-beta", version_pattern)
|
||||
new_lines = v1rewrite.rewrite_lines([pattern], new_vinfo, old_lines)
|
||||
|
||||
# make sure optional release tag is added back on
|
||||
assert len(new_lines) == len(old_lines)
|
||||
assert "2019.0004-beta" not in "\n".join(old_lines)
|
||||
assert "2019.0004-beta" in "\n".join(new_lines)
|
||||
assert '__version__ = "2019.0004-beta"' in "\n".join(new_lines)
|
||||
|
||||
|
||||
def test_v2_optional_release():
|
||||
version_pattern = "YYYY.BUILD[-TAG]"
|
||||
new_vinfo = v2version.parse_version_info("2019.0003", version_pattern)
|
||||
|
||||
raw_pattern = '__version__ = "YYYY.BUILD[-TAG]"'
|
||||
pattern = v2patterns.compile_pattern(version_pattern, raw_pattern)
|
||||
|
||||
old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines()
|
||||
new_lines = v2rewrite.rewrite_lines([pattern], new_vinfo, old_lines)
|
||||
|
||||
assert len(new_lines) == len(old_lines)
|
||||
assert "2019.0003" not in "\n".join(old_lines)
|
||||
assert "2019.0003" in "\n".join(new_lines)
|
||||
assert '__version__ = "2019.0003"' in "\n".join(new_lines)
|
||||
|
||||
new_vinfo = v2version.parse_version_info("2019.0004-beta", version_pattern)
|
||||
new_lines = v2rewrite.rewrite_lines([pattern], new_vinfo, old_lines)
|
||||
|
||||
# make sure optional release tag is added back on
|
||||
assert len(new_lines) == len(old_lines)
|
||||
assert "2019.0004-beta" not in "\n".join(old_lines)
|
||||
assert "2019.0004-beta" in "\n".join(new_lines)
|
||||
assert '__version__ = "2019.0004-beta"' in "\n".join(new_lines)
|
||||
|
||||
|
||||
def test_v1_iter_rewritten():
|
||||
version_pattern = "{year}{build}{release}"
|
||||
new_vinfo = v1version.parse_version_info("2018.0123", version_pattern)
|
||||
|
||||
init_pattern = v1patterns.compile_pattern(
|
||||
version_pattern, '__version__ = "{year}{build}{release}"'
|
||||
)
|
||||
file_patterns = {"src/bumpver/__init__.py": [init_pattern]}
|
||||
rewritten_datas = v1rewrite.iter_rewritten(file_patterns, new_vinfo)
|
||||
rfd = list(rewritten_datas)[0]
|
||||
expected = [
|
||||
"# This file is part of the pycalver project",
|
||||
"# https://github.com/mbarkhau/pycalver",
|
||||
"#",
|
||||
"# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License",
|
||||
"# SPDX-License-Identifier: MIT",
|
||||
'"""BumpVer: A CLI program for versioning."""',
|
||||
'',
|
||||
'__version__ = "2018.0123"',
|
||||
'',
|
||||
]
|
||||
assert rfd.new_lines == expected
|
||||
|
||||
|
||||
def test_v2_iter_rewritten():
|
||||
version_pattern = "YYYY.BUILD[-TAG]"
|
||||
new_vinfo = v2version.parse_version_info("2018.0123", version_pattern)
|
||||
|
||||
file_patterns = {
|
||||
"src/bumpver/__init__.py": [
|
||||
v2patterns.compile_pattern(version_pattern, '__version__ = "YYYY.BUILD[-TAG]"'),
|
||||
]
|
||||
}
|
||||
|
||||
rewritten_datas = v2rewrite.iter_rewritten(file_patterns, new_vinfo)
|
||||
rfd = list(rewritten_datas)[0]
|
||||
expected = [
|
||||
"# This file is part of the pycalver project",
|
||||
"# https://github.com/mbarkhau/pycalver",
|
||||
"#",
|
||||
"# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License",
|
||||
"# SPDX-License-Identifier: MIT",
|
||||
'"""BumpVer: A CLI program for versioning."""',
|
||||
'',
|
||||
'__version__ = "2018.0123"',
|
||||
'',
|
||||
]
|
||||
assert rfd.new_lines == expected
|
||||
|
||||
|
||||
def test_v1_diff():
|
||||
version_pattern = "{year}{build}{release}"
|
||||
raw_pattern = '__version__ = "{year}{build}{release}"'
|
||||
pattern = v1patterns.compile_pattern(version_pattern, raw_pattern)
|
||||
file_patterns = {"src/bumpver/__init__.py": [pattern]}
|
||||
|
||||
old_vinfo = v1version.parse_version_info("v201809.0123")
|
||||
new_vinfo = v1version.parse_version_info("v201911.1124")
|
||||
assert new_vinfo > old_vinfo
|
||||
|
||||
old_vinfo = v1version.parse_version_info("2018.0123", version_pattern)
|
||||
new_vinfo = v1version.parse_version_info("2019.1124", version_pattern)
|
||||
|
||||
diff_str = v1rewrite.diff(old_vinfo, new_vinfo, file_patterns)
|
||||
lines = diff_str.split("\n")
|
||||
|
||||
assert lines[:2] == [
|
||||
"--- src/bumpver/__init__.py",
|
||||
"+++ src/bumpver/__init__.py",
|
||||
]
|
||||
|
||||
assert lines[6].startswith('-__version__ = "20')
|
||||
assert lines[7].startswith('+__version__ = "20')
|
||||
|
||||
assert not lines[6].startswith('-__version__ = "2018.0123"')
|
||||
|
||||
assert lines[7] == '+__version__ = "2019.1124"'
|
||||
|
||||
raw_pattern = "Copyright (c) 2018-{year}"
|
||||
pattern = v1patterns.compile_pattern(version_pattern, raw_pattern)
|
||||
file_patterns = {'LICENSE': [pattern]}
|
||||
diff_str = v1rewrite.diff(old_vinfo, new_vinfo, file_patterns)
|
||||
|
||||
lines = diff_str.split("\n")
|
||||
assert lines[3].startswith("-MIT License Copyright (c) 2018-20")
|
||||
assert lines[4].startswith("+MIT License Copyright (c) 2018-2019")
|
||||
|
||||
|
||||
def test_v2_diff():
|
||||
version_pattern = "YYYY.BUILD[-TAG]"
|
||||
raw_pattern = '__version__ = "YYYY.BUILD[-TAG]"'
|
||||
pattern = v2patterns.compile_pattern(version_pattern, raw_pattern)
|
||||
file_patterns = {"src/bumpver/__init__.py": [pattern]}
|
||||
|
||||
old_vinfo = v2version.parse_version_info("2018.0123", version_pattern)
|
||||
new_vinfo = v2version.parse_version_info("2019.1124", version_pattern)
|
||||
|
||||
diff_str = v2rewrite.diff(old_vinfo, new_vinfo, file_patterns)
|
||||
lines = diff_str.split("\n")
|
||||
|
||||
assert lines[:2] == [
|
||||
"--- src/bumpver/__init__.py",
|
||||
"+++ src/bumpver/__init__.py",
|
||||
]
|
||||
|
||||
assert lines[6].startswith('-__version__ = "20')
|
||||
assert lines[7].startswith('+__version__ = "20')
|
||||
|
||||
assert not lines[6].startswith('-__version__ = "2018.0123"')
|
||||
|
||||
assert lines[7] == '+__version__ = "2019.1124"'
|
||||
|
||||
raw_pattern = "Copyright (c) 2018-YYYY"
|
||||
pattern = v2patterns.compile_pattern(version_pattern, raw_pattern)
|
||||
file_patterns = {'LICENSE': [pattern]}
|
||||
diff_str = v2rewrite.diff(old_vinfo, new_vinfo, file_patterns)
|
||||
|
||||
lines = diff_str.split("\n")
|
||||
assert lines[3].startswith("-MIT License Copyright (c) 2018-20")
|
||||
assert lines[4].startswith("+MIT License Copyright (c) 2018-2019")
|
||||
|
|
|
|||
|
|
@ -1,32 +1,41 @@
|
|||
# pylint:disable=protected-access ; allowed for test code
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import random
|
||||
import datetime as dt
|
||||
|
||||
import pytest
|
||||
|
||||
from pycalver import version
|
||||
from pycalver import patterns
|
||||
from bumpver import version
|
||||
from bumpver import v1version
|
||||
from bumpver import v2version
|
||||
from bumpver import v1patterns
|
||||
from bumpver import v2patterns
|
||||
|
||||
# pylint:disable=protected-access ; allowed for test code
|
||||
|
||||
|
||||
def test_bump_beta():
|
||||
cur_version = "v201712.0001-beta"
|
||||
assert cur_version < version.incr(cur_version)
|
||||
assert version.incr(cur_version).endswith("-beta")
|
||||
assert version.incr(cur_version, release="alpha").endswith("-alpha")
|
||||
assert version.incr(cur_version, release="final").endswith("0002")
|
||||
assert cur_version < v1version.incr(cur_version)
|
||||
assert v1version.incr(cur_version).endswith("-beta")
|
||||
assert v1version.incr(cur_version, tag="alpha").endswith("-alpha")
|
||||
assert v1version.incr(cur_version, tag="final").endswith("0002")
|
||||
|
||||
|
||||
def test_bump_final():
|
||||
cur_version = "v201712.0001"
|
||||
assert cur_version < version.incr(cur_version)
|
||||
assert version.incr(cur_version).endswith(".0002")
|
||||
assert version.incr(cur_version, release="alpha").endswith("-alpha")
|
||||
assert cur_version < v1version.incr(cur_version)
|
||||
assert v1version.incr(cur_version).endswith(".0002")
|
||||
assert v1version.incr(cur_version, tag="alpha").endswith("-alpha")
|
||||
|
||||
assert version.incr(cur_version, release="final").endswith(".0002")
|
||||
assert v1version.incr(cur_version, tag="final").endswith(".0002")
|
||||
|
||||
pre_version = cur_version + "-beta"
|
||||
assert version.incr(pre_version, release="final").endswith(".0002")
|
||||
assert v1version.incr(pre_version, tag="final").endswith(".0002")
|
||||
|
||||
|
||||
def test_bump_future():
|
||||
|
|
@ -34,7 +43,7 @@ def test_bump_future():
|
|||
future_date = dt.datetime.today() + dt.timedelta(days=300)
|
||||
future_calver = future_date.strftime("v%Y%m")
|
||||
cur_version = future_calver + ".0001"
|
||||
new_version = version.incr(cur_version)
|
||||
new_version = v1version.incr(cur_version)
|
||||
assert cur_version < new_version
|
||||
|
||||
|
||||
|
|
@ -46,8 +55,8 @@ def test_bump_random(monkeypatch):
|
|||
|
||||
for _ in range(1000):
|
||||
cur_date += dt.timedelta(days=int((1 + random.random()) ** 10))
|
||||
new_version = version.incr(
|
||||
cur_version, release=random.choice([None, "alpha", "beta", "rc", "final", "post"])
|
||||
new_version = v1version.incr(
|
||||
cur_version, tag=random.choice([None, "alpha", "beta", "rc", "final", "post"])
|
||||
)
|
||||
assert cur_version < new_version
|
||||
cur_version = new_version
|
||||
|
|
@ -55,7 +64,7 @@ def test_bump_random(monkeypatch):
|
|||
|
||||
def test_parse_version_info():
|
||||
version_str = "v201712.0001-alpha"
|
||||
version_info = version.parse_version_info(version_str)
|
||||
version_info = v1version.parse_version_info(version_str)
|
||||
|
||||
# assert version_info.pep440_version == "201712.1a0"
|
||||
# assert version_info.version == "v201712.0001-alpha"
|
||||
|
|
@ -65,7 +74,7 @@ def test_parse_version_info():
|
|||
assert version_info.tag == "alpha"
|
||||
|
||||
version_str = "v201712.0001"
|
||||
version_info = version.parse_version_info(version_str)
|
||||
version_info = v1version.parse_version_info(version_str)
|
||||
|
||||
# assert version_info.pep440_version == "201712.1"
|
||||
# assert version_info.version == "v201712.0001"
|
||||
|
|
@ -77,7 +86,7 @@ def test_parse_version_info():
|
|||
|
||||
def test_readme_pycalver1():
|
||||
version_str = "v201712.0001-alpha"
|
||||
version_info = patterns.PYCALVER_RE.match(version_str).groupdict()
|
||||
version_info = v1patterns.PYCALVER_RE.match(version_str).groupdict()
|
||||
|
||||
assert version_info == {
|
||||
'pycalver' : "v201712.0001-alpha",
|
||||
|
|
@ -93,7 +102,7 @@ def test_readme_pycalver1():
|
|||
|
||||
def test_readme_pycalver2():
|
||||
version_str = "v201712.0033"
|
||||
version_info = patterns.PYCALVER_RE.match(version_str).groupdict()
|
||||
version_info = v1patterns.PYCALVER_RE.match(version_str).groupdict()
|
||||
|
||||
assert version_info == {
|
||||
'pycalver' : "v201712.0033",
|
||||
|
|
@ -109,7 +118,7 @@ def test_readme_pycalver2():
|
|||
|
||||
def test_parse_error_empty():
|
||||
try:
|
||||
version.parse_version_info("")
|
||||
v1version.parse_version_info("")
|
||||
assert False
|
||||
except version.PatternError as err:
|
||||
assert "Invalid version string" in str(err)
|
||||
|
|
@ -117,7 +126,7 @@ def test_parse_error_empty():
|
|||
|
||||
def test_parse_error_noprefix():
|
||||
try:
|
||||
version.parse_version_info("201809.0002")
|
||||
v1version.parse_version_info("201809.0002")
|
||||
assert False
|
||||
except version.PatternError as err:
|
||||
assert "Invalid version string" in str(err)
|
||||
|
|
@ -125,60 +134,141 @@ def test_parse_error_noprefix():
|
|||
|
||||
def test_parse_error_nopadding():
|
||||
try:
|
||||
version.parse_version_info("v201809.2b0")
|
||||
v1version.parse_version_info("v201809.2b0")
|
||||
assert False
|
||||
except version.PatternError as err:
|
||||
assert "Invalid version string" in str(err)
|
||||
|
||||
|
||||
def test_part_field_mapping():
|
||||
a_names = set(version.PATTERN_PART_FIELDS.keys())
|
||||
b_names = set(patterns.PART_PATTERNS.keys())
|
||||
c_names = set(patterns.COMPOSITE_PART_PATTERNS.keys())
|
||||
def test_part_field_mapping_v1():
|
||||
a_names = set(v1patterns.PATTERN_PART_FIELDS.keys())
|
||||
b_names = set(v1patterns.PART_PATTERNS.keys())
|
||||
c_names = set(v1patterns.COMPOSITE_PART_PATTERNS.keys())
|
||||
|
||||
extra_names = a_names - b_names
|
||||
assert not any(extra_names)
|
||||
missing_names = b_names - a_names
|
||||
assert missing_names == c_names
|
||||
a_extra_names = a_names - b_names
|
||||
assert not any(a_extra_names), sorted(a_extra_names)
|
||||
b_extra_names = b_names - (a_names | c_names)
|
||||
assert not any(b_extra_names), sorted(b_extra_names)
|
||||
|
||||
a_fields = set(version.PATTERN_PART_FIELDS.values())
|
||||
b_fields = set(version.VersionInfo._fields)
|
||||
a_fields = set(v1patterns.PATTERN_PART_FIELDS.values())
|
||||
b_fields = set(version.V1VersionInfo._fields)
|
||||
|
||||
assert a_fields == b_fields
|
||||
a_extra_fields = a_fields - b_fields
|
||||
b_extra_fields = b_fields - a_fields
|
||||
assert not any(a_extra_fields), sorted(a_extra_fields)
|
||||
assert not any(b_extra_fields), sorted(b_extra_fields)
|
||||
|
||||
|
||||
def vnfo(**field_values):
|
||||
return version._parse_field_values(field_values)
|
||||
def v1vnfo(**field_values):
|
||||
return v1version._parse_field_values(field_values)
|
||||
|
||||
|
||||
PARSE_VERSION_TEST_CASES = [
|
||||
["{year}.{month}.{dom}" , "2017.06.07", vnfo(year="2017", month="06", dom="07")],
|
||||
["{year}.{month}.{dom_short}" , "2017.06.7" , vnfo(year="2017", month="06", dom="7" )],
|
||||
["{year}.{month}.{dom_short}" , "2017.06.7" , vnfo(year="2017", month="06", dom="7" )],
|
||||
["{year}.{month_short}.{dom_short}", "2017.6.7" , vnfo(year="2017", month="6" , dom="7" )],
|
||||
def v2vnfo(**field_values):
|
||||
return v2version.parse_field_values_to_vinfo(field_values)
|
||||
|
||||
|
||||
PARSE_V1_VERSION_TEST_CASES = [
|
||||
["{year}.{month}.{dom}" , "2017.06.07", v1vnfo(year="2017", month="06", dom="07")],
|
||||
["{year}.{month}.{dom_short}" , "2017.06.7" , v1vnfo(year="2017", month="06", dom="7" )],
|
||||
["{year}.{month}.{dom_short}" , "2017.06.7" , v1vnfo(year="2017", month="06", dom="7" )],
|
||||
["{year}.{month_short}.{dom_short}", "2017.6.7" , v1vnfo(year="2017", month="6" , dom="7" )],
|
||||
["{year}.{month}.{dom}" , "2017.6.07" , None],
|
||||
["{year}.{month}.{dom}" , "2017.06.7" , None],
|
||||
["{year}.{month_short}.{dom}" , "2017.06.7" , None],
|
||||
["{year}.{month}.{dom_short}" , "2017.6.07" , None],
|
||||
["{year}.{month_short}.{MINOR}" , "2017.6.7" , vnfo(year="2017", month="6" , minor="7" )],
|
||||
["{year}.{month}.{MINOR}" , "2017.06.7" , vnfo(year="2017", month="06", minor="7" )],
|
||||
["{year}.{month}.{MINOR}" , "2017.06.07", vnfo(year="2017", month="06", minor="07")],
|
||||
["{year}.{month_short}.{MINOR}" , "2017.6.7" , v1vnfo(year="2017", month="6" , minor="7" )],
|
||||
["{year}.{month}.{MINOR}" , "2017.06.7" , v1vnfo(year="2017", month="06", minor="7" )],
|
||||
["{year}.{month}.{MINOR}" , "2017.06.07", v1vnfo(year="2017", month="06", minor="07")],
|
||||
["{year}.{month}.{MINOR}" , "2017.6.7" , None],
|
||||
["YYYY.0M.0D" , "2017.06.07", v2vnfo(year_y="2017", month="06", dom="07")],
|
||||
["YYYY.MM.DD" , "2017.6.7" , v2vnfo(year_y="2017", month="6" , dom="7" )],
|
||||
["YYYY.MM.MD" , "2017.06.07", None],
|
||||
["YYYY.0M.0D" , "2017.6.7" , None],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pattern_str, line, expected_vinfo", PARSE_VERSION_TEST_CASES)
|
||||
def test_parse_versions(pattern_str, line, expected_vinfo):
|
||||
pattern_re = patterns.compile_pattern(pattern_str)
|
||||
version_match = pattern_re.search(line)
|
||||
@pytest.mark.parametrize("pattern_str, line, expected_vinfo", PARSE_V1_VERSION_TEST_CASES)
|
||||
def test_v1_parse_versions(pattern_str, line, expected_vinfo):
|
||||
if "{" in pattern_str:
|
||||
pattern = v1patterns.compile_pattern(pattern_str)
|
||||
version_match = pattern.regexp.search(line)
|
||||
else:
|
||||
pattern = v2patterns.compile_pattern(pattern_str)
|
||||
version_match = pattern.regexp.search(line)
|
||||
|
||||
if expected_vinfo is None:
|
||||
assert version_match is None
|
||||
return
|
||||
|
||||
else:
|
||||
assert version_match is not None
|
||||
|
||||
version_str = version_match.group(0)
|
||||
version_info = version.parse_version_info(version_str, pattern_str)
|
||||
|
||||
if "{" in pattern_str:
|
||||
version_info = v1version.parse_version_info(version_str, pattern_str)
|
||||
assert version_info == expected_vinfo
|
||||
else:
|
||||
version_info = v2version.parse_version_info(version_str, pattern_str)
|
||||
assert version_info == expected_vinfo
|
||||
|
||||
|
||||
def test_v2_parse_versions():
|
||||
_vnfo = v2version.parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]")
|
||||
fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"}
|
||||
assert _vnfo == v2version.parse_field_values_to_vinfo(fvals)
|
||||
|
||||
|
||||
def test_v2_format_version():
|
||||
version_pattern = "vYYYY0M.BUILD[-TAG[NUM]]"
|
||||
in_version = "v200701.0033-beta"
|
||||
|
||||
vinfo = v2version.parse_version_info(in_version, raw_pattern=version_pattern)
|
||||
out_version = v2version.format_version(vinfo, raw_pattern=version_pattern)
|
||||
assert in_version == out_version
|
||||
|
||||
result = v2version.format_version(vinfo, raw_pattern="v0Y.BUILD[-TAG]")
|
||||
assert result == "v07.0033-beta"
|
||||
|
||||
result = v2version.format_version(vinfo, raw_pattern="vYY.BLD[-TAG]")
|
||||
assert result == "v7.33-beta"
|
||||
|
||||
result = v2version.format_version(vinfo, raw_pattern="vYY.BLD-TAG")
|
||||
assert result == "v7.33-beta"
|
||||
|
||||
result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BUILD[-TAG]"')
|
||||
assert result == '__version__ = "2007.0033-beta"'
|
||||
|
||||
result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BLD"')
|
||||
assert result == '__version__ = "2007.33"'
|
||||
|
||||
|
||||
WEEK_PATTERN_TEXT_CASES = [
|
||||
("YYYYWW.PATCH", True),
|
||||
("YYYYUU.PATCH", True),
|
||||
("GGGGVV.PATCH", True),
|
||||
("YYWW.PATCH" , True),
|
||||
("YYUU.PATCH" , True),
|
||||
("GGVV.PATCH" , True),
|
||||
("0YWW.PATCH" , True),
|
||||
("0YUU.PATCH" , True),
|
||||
("0GVV.PATCH" , True),
|
||||
("0Y0W.PATCH" , True),
|
||||
("0Y0U.PATCH" , True),
|
||||
("0G0V.PATCH" , True),
|
||||
("GGGGWW.PATCH", False),
|
||||
("GGGGUU.PATCH", False),
|
||||
("YYYYVV.PATCH", False),
|
||||
("GGWW.PATCH" , False),
|
||||
("GGUU.PATCH" , False),
|
||||
("YYVV.PATCH" , False),
|
||||
("0GWW.PATCH" , False),
|
||||
("0GUU.PATCH" , False),
|
||||
("0YVV.PATCH" , False),
|
||||
("0G0W.PATCH" , False),
|
||||
("0G0U.PATCH" , False),
|
||||
("0Y0V.PATCH" , False),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pattern, expected", WEEK_PATTERN_TEXT_CASES)
|
||||
def test_is_valid_week_pattern(pattern, expected):
|
||||
assert v2version.is_valid_week_pattern(pattern) == expected
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ FIXTURE_PATH_PARTS = [
|
|||
["setup.cfg"],
|
||||
["setup.py"],
|
||||
["pycalver.toml"],
|
||||
["bumpver.toml"],
|
||||
["src", "module_v1", "__init__.py"],
|
||||
["src", "module_v2", "__init__.py"],
|
||||
]
|
||||
|
|
@ -40,7 +41,7 @@ class Project:
|
|||
self.tmpdir = tmpdir
|
||||
self.prev_cwd = os.getcwd()
|
||||
|
||||
self.dir = tmpdir / "pycalver_project"
|
||||
self.dir = tmpdir / "bumpver_project"
|
||||
self.dir.mkdir()
|
||||
|
||||
if project is None:
|
||||
|
|
@ -76,7 +77,7 @@ class Project:
|
|||
for path_parts in FIXTURE_PATH_PARTS:
|
||||
maybe_file_path = self.dir.joinpath(*path_parts)
|
||||
if maybe_file_path.exists():
|
||||
self.shell(f"{cmd} add {str(maybe_file_path)}")
|
||||
self.shell(cmd + " add " + str(maybe_file_path))
|
||||
added_file_paths.append(maybe_file_path)
|
||||
|
||||
assert len(added_file_paths) >= 2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue