mirror of
https://github.com/TECHNOFAB11/bumpver.git
synced 2025-12-12 14:30:09 +01:00
WIP: mostly scratch code still
This commit is contained in:
parent
e2e218bce9
commit
3471560eaa
20 changed files with 1757 additions and 1 deletions
66
.gitignore
vendored
Normal file
66
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
venv2/
|
||||||
|
venv3/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
.mypy_cache/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
.eggs/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
.coverage_percent.txt
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
.idea
|
||||||
|
.*.make_marker
|
||||||
|
README.html
|
||||||
|
envs.txt
|
||||||
|
test_build_logs/*.log
|
||||||
7
CHANGELOG.rst
Normal file
7
CHANGELOG.rst
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
Changelog for pycalver
|
||||||
|
======================
|
||||||
|
|
||||||
|
v201809.0001-alpha
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Initial release
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2018 mbarkhau
|
Copyright (c) 2018 Manuel Barkhau (@mbarkhau)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
160
Makefile
Normal file
160
Makefile
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
.PHONY: setup_conda_envs install \
|
||||||
|
test lint \
|
||||||
|
clean rm_site_packages \
|
||||||
|
build readme upload
|
||||||
|
|
||||||
|
|
||||||
|
build/.setup_conda_envs.make_marker:
|
||||||
|
conda create --name pycalver_37 python=3.7 --yes
|
||||||
|
conda create --name pycalver_36 python=3.6 --yes
|
||||||
|
conda create --name pycalver_27 python=2.7 --yes
|
||||||
|
@mkdir -p build/
|
||||||
|
@touch build/.setup_conda_envs.make_marker
|
||||||
|
|
||||||
|
|
||||||
|
build/envs.txt: build/.setup_conda_envs.make_marker
|
||||||
|
@mkdir -p build/
|
||||||
|
conda env list | grep pycalver | rev | cut -d " " -f1 | rev > build/envs.txt.tmp
|
||||||
|
mv build/envs.txt.tmp build/envs.txt
|
||||||
|
|
||||||
|
|
||||||
|
PYENV37 ?= $(shell bash -c "grep 37 build/envs.txt || true")
|
||||||
|
PYENV36 ?= $(shell bash -c "grep 36 build/envs.txt || true")
|
||||||
|
PYENV27 ?= $(shell bash -c "grep 27 build/envs.txt || true")
|
||||||
|
PYTHON37 ?= $(PYENV37)/bin/python
|
||||||
|
PYTHON36 ?= $(PYENV36)/bin/python
|
||||||
|
PYTHON27 ?= $(PYENV27)/bin/python
|
||||||
|
|
||||||
|
BDIST_WHEEL_LIB3TO6 = $(shell bash -c "ls -1t dist/pycalver*py2*.whl | head -n 1")
|
||||||
|
SDIST_LIB3TO6 = $(shell bash -c "ls -1t dist/pycalver*.tar.gz | head -n 1")
|
||||||
|
DIST_WHEEL_TEST = $(shell bash -c "ls -1t test_project/dist/*py2*.whl | head -n 1")
|
||||||
|
BUILD_LOG_DIR = "test_build_logs/"
|
||||||
|
BUILD_LOG_FILE := $(shell date +"$(BUILD_LOG_DIR)%Y%m%dt%H%M%S%N.log")
|
||||||
|
|
||||||
|
|
||||||
|
build/.install.make_marker: setup.py build/envs.txt
|
||||||
|
$(PYTHON36) -m pip install --upgrade --quiet \
|
||||||
|
pip setuptools wheel twine \
|
||||||
|
flake8 mypy typing-extensions \
|
||||||
|
rst2html5 \
|
||||||
|
pytest pytest-cov \
|
||||||
|
ipython pudb \
|
||||||
|
pathlib2 click;
|
||||||
|
|
||||||
|
$(PYTHON37) -m pip install --upgrade --quiet pip setuptools wheel;
|
||||||
|
$(PYTHON36) -m pip install --upgrade --quiet pip setuptools wheel;
|
||||||
|
$(PYTHON27) -m pip install --upgrade --quiet pip setuptools wheel;
|
||||||
|
|
||||||
|
@mkdir -p build/
|
||||||
|
@touch build/.install.make_marker
|
||||||
|
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f build/envs.txt
|
||||||
|
rm -f build/.setup_conda_envs.make_marker
|
||||||
|
rm -f build/.install.make_marker
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE (mb 2018-08-23): The linter has an issue running with
|
||||||
|
# python 3.7 because some code in pycodestyle=2.3.1
|
||||||
|
# but we have to wait for a flake8 update because
|
||||||
|
# reasons... https://github.com/PyCQA/pycodestyle/issues/728
|
||||||
|
|
||||||
|
|
||||||
|
lint: build/.install.make_marker
|
||||||
|
@echo -n "lint.."
|
||||||
|
@$(PYTHON36) -m flake8 src/pycalver/
|
||||||
|
@echo "ok"
|
||||||
|
|
||||||
|
|
||||||
|
mypy: build/.install.make_marker
|
||||||
|
@echo -n "mypy.."
|
||||||
|
@MYPYPATH=stubs/ $(PYTHON36) -m mypy \
|
||||||
|
src/pycalver/
|
||||||
|
@echo "ok"
|
||||||
|
|
||||||
|
|
||||||
|
test: build/.install.make_marker
|
||||||
|
@PYTHONPATH=src/:$$PYTHONPATH \
|
||||||
|
$(PYTHON36) -m pytest \
|
||||||
|
--cov-report html \
|
||||||
|
--cov=pycalver \
|
||||||
|
test/
|
||||||
|
|
||||||
|
|
||||||
|
devtest: build/.install.make_marker
|
||||||
|
PYTHONPATH=src/:$$PYTHONPATH \
|
||||||
|
$(PYTHON36) -m pytest -v \
|
||||||
|
--cov-report term \
|
||||||
|
--cov=pycalver \
|
||||||
|
--capture=no \
|
||||||
|
--exitfirst \
|
||||||
|
test/
|
||||||
|
|
||||||
|
|
||||||
|
build/.coverage_percent.txt: test
|
||||||
|
@mkdir -p build/
|
||||||
|
@grep -oP '>[0-9]+%</td>' htmlcov/index.html \
|
||||||
|
| head -n 1 \
|
||||||
|
| grep -oP '[.0-9]+' \
|
||||||
|
> build/.coverage_percent.txt
|
||||||
|
|
||||||
|
|
||||||
|
README.rst: build/.coverage_percent.txt
|
||||||
|
@sed -i "s/coverage-[0-9]*/coverage-$$(cat build/.coverage_percent.txt)/" README.rst
|
||||||
|
|
||||||
|
|
||||||
|
build/README.html: build/.install.make_marker README.rst CHANGELOG.rst
|
||||||
|
@cat README.rst > build/.full_readme.rst
|
||||||
|
@echo "\n" >> build/.full_readme.rst
|
||||||
|
@cat CHANGELOG.rst >> build/.full_readme.rst
|
||||||
|
@$(PYENV36)/bin/rst2html5 --strict \
|
||||||
|
build/.full_readme.rst > build/README.html.tmp
|
||||||
|
@mv build/README.html.tmp build/README.html
|
||||||
|
@echo "updated build/README.html"
|
||||||
|
|
||||||
|
|
||||||
|
readme: build/README.html
|
||||||
|
|
||||||
|
|
||||||
|
build/.src_files.txt: setup.py build/envs.txt src/pycalver/*.py
|
||||||
|
@mkdir -p build/
|
||||||
|
@ls -l setup.py build/envs.txt src/pycalver/*.py > build/.src_files.txt.tmp
|
||||||
|
@mv build/.src_files.txt.tmp build/.src_files.txt
|
||||||
|
|
||||||
|
|
||||||
|
rm_site_packages:
|
||||||
|
rm -rf $(PYENV36)/lib/python3.6/site-packages/pycalver/
|
||||||
|
rm -rf $(PYENV36)/lib/python3.6/site-packages/pycalver*.dist-info/
|
||||||
|
rm -rf $(PYENV36)/lib/python3.6/site-packages/pycalver*.egg-info/
|
||||||
|
rm -f $(PYENV36)/lib/python3.6/site-packages/pycalver*.egg
|
||||||
|
|
||||||
|
|
||||||
|
build/.local_install.make_marker: build/.src_files.txt rm_site_packages
|
||||||
|
@echo "installing pycalver.."
|
||||||
|
@$(PYTHON36) setup.py install --no-compile --verbose
|
||||||
|
@mkdir -p build/
|
||||||
|
@$(PYTHON36) -c "import pycalver"
|
||||||
|
@echo "install completed for pycalver"
|
||||||
|
@touch build/.local_install.make_marker
|
||||||
|
|
||||||
|
|
||||||
|
build: build/.local_install.make_marker
|
||||||
|
@mkdir -p $(BUILD_LOG_DIR)
|
||||||
|
@echo "writing full build log to $(BUILD_LOG_FILE)"
|
||||||
|
@echo "building pycalver.."
|
||||||
|
@$(PYTHON36) setup.py bdist_wheel --python-tag=py2.py3 >> $(BUILD_LOG_FILE)
|
||||||
|
@echo "build completed for pycalver"
|
||||||
|
|
||||||
|
|
||||||
|
upload: build/.install.make_marker build/README.html
|
||||||
|
$(PYTHON36) setup.py bdist_wheel --python-tag=py2.py3
|
||||||
|
$(PYENV36)/bin/twine upload $(BDIST_WHEEL_LIB3TO6)
|
||||||
|
|
||||||
|
|
||||||
|
setup_conda_envs: build/.setup_conda_envs.make_marker
|
||||||
|
|
||||||
|
install: build/.install.make_marker
|
||||||
|
|
||||||
|
run_main:
|
||||||
|
PYTHONPATH=src/:$$PYTHONPATH $(PYTHON36) -m pycalver --help
|
||||||
546
README.rst
Normal file
546
README.rst
Normal file
|
|
@ -0,0 +1,546 @@
|
||||||
|
PyCalVer: Automatic CalVer Versioning for Python Packages
|
||||||
|
=========================================================
|
||||||
|
|
||||||
|
PyCalVer is a very simple versioning system,
|
||||||
|
which is compatible with python packaging software
|
||||||
|
(
|
||||||
|
`setuptools <https://setuptools.readthedocs.io/en/latest/setuptools.html#specifying-your-project-s-version>`_,
|
||||||
|
`PEP440 <https://www.python.org/dev/peps/pep-0440/>`_
|
||||||
|
).
|
||||||
|
|
||||||
|
.. start-badges
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:stub-columns: 1
|
||||||
|
|
||||||
|
* - package
|
||||||
|
- | |license| |wheel| |pyversions| |pypi| |version|
|
||||||
|
* - tests
|
||||||
|
- | |travis| |mypy| |coverage|
|
||||||
|
|
||||||
|
.. |travis| image:: https://api.travis-ci.org/mbarkhau/pycalver.svg?branch=master
|
||||||
|
:target: https://travis-ci.org/mbarkhau/pycalver
|
||||||
|
:alt: Build Status
|
||||||
|
|
||||||
|
.. |mypy| image:: http://www.mypy-lang.org/static/mypy_badge.svg
|
||||||
|
:target: http://mypy-lang.org/
|
||||||
|
:alt: Checked with mypy
|
||||||
|
|
||||||
|
.. |coverage| image:: https://img.shields.io/badge/coverage-86%25-green.svg
|
||||||
|
:target: https://travis-ci.org/mbarkhau/pycalver
|
||||||
|
:alt: Code Coverage
|
||||||
|
|
||||||
|
.. |license| image:: https://img.shields.io/pypi/l/pycalver.svg
|
||||||
|
:target: https://github.com/mbarkhau/pycalver/blob/master/LICENSE
|
||||||
|
:alt: MIT License
|
||||||
|
|
||||||
|
.. |pypi| image:: https://img.shields.io/pypi/v/pycalver.svg
|
||||||
|
:target: https://pypi.python.org/pypi/pycalver
|
||||||
|
:alt: PyPI Version
|
||||||
|
|
||||||
|
.. |version| image:: https://img.shields.io/badge/CalVer-v201809.0001--beta-blue.svg
|
||||||
|
:target: https://calver.org/
|
||||||
|
:alt: CalVer v201809.0001-beta
|
||||||
|
|
||||||
|
.. |wheel| image:: https://img.shields.io/pypi/wheel/pycalver.svg
|
||||||
|
:target: https://pypi.org/project/pycalver/#files
|
||||||
|
:alt: PyPI Wheel
|
||||||
|
|
||||||
|
.. |pyversions| image:: https://img.shields.io/pypi/pyversions/pycalver.svg
|
||||||
|
:target: https://pypi.python.org/pypi/pycalver
|
||||||
|
:alt: Supported Python Versions
|
||||||
|
|
||||||
|
|
||||||
|
The PyCalVer package provides the ``pycalver`` command and
|
||||||
|
module to generate version strings which follow the format:
|
||||||
|
``v{calendar_version}.{build_number}[-{tag}]``
|
||||||
|
|
||||||
|
Some examples:
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
v201711.0001-alpha
|
||||||
|
v201712.0027-beta
|
||||||
|
v201801.0031
|
||||||
|
v201801.0032-post
|
||||||
|
...
|
||||||
|
v202207.18133
|
||||||
|
v202207.18134
|
||||||
|
|
||||||
|
|
||||||
|
The ``pycalver bump`` command will parse specified/configured
|
||||||
|
files for such strings and rewrite them with an updated version
|
||||||
|
string.
|
||||||
|
|
||||||
|
The format accepted by PyCalVer can be parsed with this regular
|
||||||
|
expression:
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
pycalver_re = re.compile(r"""
|
||||||
|
# https://regex101.com/r/fnj60p/9
|
||||||
|
\b
|
||||||
|
(?P<version>
|
||||||
|
(?P<calver>
|
||||||
|
v # "v" version prefix
|
||||||
|
(?P<year>\d{4})
|
||||||
|
(?P<month>\d{2})
|
||||||
|
)
|
||||||
|
(?:
|
||||||
|
\. # "." build nr prefix
|
||||||
|
(?P<build>\d{4,})
|
||||||
|
)
|
||||||
|
(?:
|
||||||
|
\- # "-" release prefix
|
||||||
|
(?P<release>
|
||||||
|
alpha|beta|dev|rc|post
|
||||||
|
)
|
||||||
|
)?
|
||||||
|
)(?:\s|$)
|
||||||
|
""", flags=re.VERBOSE)
|
||||||
|
|
||||||
|
version_str = "v201712.0001-alpha"
|
||||||
|
version_info = pycalver_re.match(version_str).groupdict()
|
||||||
|
|
||||||
|
assert version_info == {
|
||||||
|
"version" : "v201712.0001-alpha",
|
||||||
|
"calver" : "v201712",
|
||||||
|
"year" : "2017",
|
||||||
|
"month" : "12",
|
||||||
|
"build" : "0001",
|
||||||
|
"release" : "alpha",
|
||||||
|
}
|
||||||
|
|
||||||
|
version_str = "v201712.0033"
|
||||||
|
version_info = pycalver_re.match(version_str).groupdict()
|
||||||
|
|
||||||
|
assert version_info == {
|
||||||
|
"version" : "v201712.0033",
|
||||||
|
"calver" : "v201712",
|
||||||
|
"year" : "2017",
|
||||||
|
"month" : "12",
|
||||||
|
"build" : "0033",
|
||||||
|
"release" : None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Before we look at project setup, we can simply install and test
|
||||||
|
by passing a version string to ``pycalver bump``.
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pip install pycalver
|
||||||
|
$ pycalver bump v201801.0033-beta
|
||||||
|
v201809.0034-beta
|
||||||
|
|
||||||
|
|
||||||
|
The CalVer component is set to the current year and month, the
|
||||||
|
build number is incremented by one and the optional release tag
|
||||||
|
is preserved as is, unless specified otherwise via the
|
||||||
|
``--release=<tag>`` parameter.
|
||||||
|
|
||||||
|
To setup a project, add the following lines to your ``setup.cfg``
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[pycalver]
|
||||||
|
current_version = v201809.0001-beta
|
||||||
|
commit = True
|
||||||
|
tag = True
|
||||||
|
|
||||||
|
[pycalver:file:setup.cfg]
|
||||||
|
patterns =
|
||||||
|
current_version = {version}
|
||||||
|
|
||||||
|
[pycalver:file:setup.py]
|
||||||
|
patterns =
|
||||||
|
version="{pep440_version}",
|
||||||
|
|
||||||
|
[pycalver:file:src/myproject.py]
|
||||||
|
patterns =
|
||||||
|
__version__ = "{version}"
|
||||||
|
|
||||||
|
[pycalver:file:README.rst]
|
||||||
|
patterns =
|
||||||
|
badge/CalVer-{calver}{build}-{release}-blue.svg
|
||||||
|
:alt: CalVer {version}
|
||||||
|
|
||||||
|
|
||||||
|
The above setup.cfg file is very explicit, and can be shortened quite a bit.
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[pycalver]
|
||||||
|
current_version = v201809.0001-beta
|
||||||
|
commit = True
|
||||||
|
tag = True
|
||||||
|
|
||||||
|
[pycalver:file:setup.cfg]
|
||||||
|
[pycalver:file:setup.py]
|
||||||
|
[pycalver:file:src/myproject.py]
|
||||||
|
[pycalver:file:README.rst]
|
||||||
|
patterns =
|
||||||
|
badge/CalVer-{calver}{build}-{release}-blue.svg
|
||||||
|
:alt: CalVer {version}
|
||||||
|
|
||||||
|
|
||||||
|
This makes use of the default ``patterns = {version}``, which
|
||||||
|
will replace all occurrences of a PyCalVer version string with
|
||||||
|
the updated ``current_version``.
|
||||||
|
|
||||||
|
Now we can call ``pycalver bump`` to bump all occurrences of
|
||||||
|
version strings in these files. Normally this will change local
|
||||||
|
files, but the ``--dry`` flag will instead display a diff of the
|
||||||
|
changes that would be applied.
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pycalver show
|
||||||
|
Current Version: v201809.0001-beta
|
||||||
|
PEP440 Version: 201809.1b0
|
||||||
|
|
||||||
|
$ pycalver bump --dry
|
||||||
|
TODO
|
||||||
|
Don't forget to git push --tags
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Other Versioning Software
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
This project is very similar to bumpversion, upon which it is
|
||||||
|
partially based, but since the PyCalVer version strings can be
|
||||||
|
generated automatically, usage is quite a bit more simple. Users
|
||||||
|
do not have to deal with parsing and generating version strings.
|
||||||
|
Most of the interaction that users will have is reduced to two
|
||||||
|
commands:
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pycalver bump
|
||||||
|
TODO: Output
|
||||||
|
|
||||||
|
|
||||||
|
More rarely, when changing the release type:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pycalver bump --release beta
|
||||||
|
TODO: Output
|
||||||
|
|
||||||
|
$ pycalver bump --release final
|
||||||
|
TODO: Output
|
||||||
|
|
||||||
|
|
||||||
|
Some Details
|
||||||
|
------------
|
||||||
|
|
||||||
|
- Version numbers are for public releases. For the purposes of
|
||||||
|
development of the project itself, reference VCS branches and
|
||||||
|
commit ids are more appropriate.
|
||||||
|
- There should be only one person or system responsible for
|
||||||
|
updating the version number at the time of release, otherwise
|
||||||
|
the same version number may be generated for different builds.
|
||||||
|
- Lexeographical order is
|
||||||
|
|
||||||
|
|
||||||
|
Canonical PyCalVer version strings can be parsed with this
|
||||||
|
regular expression:
|
||||||
|
|
||||||
|
|
||||||
|
These are the full version strings, for public announcements and
|
||||||
|
conversations it will often be sufficient to refer simply to
|
||||||
|
``v201801``, by which the most recent ``post`` release build of
|
||||||
|
that month is meant.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
version_str = "v201712.0027-beta"
|
||||||
|
version_dict = pycalver_re.match("v201712.0027-beta").groupdict()
|
||||||
|
import pkg_resources # from setuptools
|
||||||
|
version = pkg_resources.parse_version(version_str)
|
||||||
|
--
|
||||||
|
|
||||||
|
In [2]: version_dict
|
||||||
|
{'year': '2017', 'month': '12', 'build_nr': '0027', 'tag': 'beta'}
|
||||||
|
>>> str(version)
|
||||||
|
201712.27b0
|
||||||
|
|
||||||
|
|
||||||
|
Lexical Ids
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Most projects will be served perfectly well by the default four
|
||||||
|
digit zero padded build number. Depending on your build system
|
||||||
|
however, you may get into higher build numbers. Since build
|
||||||
|
numbers have no semantic meaning (beyond larger = later/newer),
|
||||||
|
they are incremented in a way that preserves lexical ordering as
|
||||||
|
well as numerical order. Examples will perhaps illustrate more
|
||||||
|
clearly.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
"0001"
|
||||||
|
"0002"
|
||||||
|
"0003"
|
||||||
|
...
|
||||||
|
"0999"
|
||||||
|
"11000"
|
||||||
|
"11001"
|
||||||
|
...
|
||||||
|
"19998"
|
||||||
|
"19999"
|
||||||
|
"220000"
|
||||||
|
"220001"
|
||||||
|
|
||||||
|
What is happening here is that the left-most digit is incremented
|
||||||
|
early, whenever the left-most digit changes. The formula is very simple:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
prev_id = "0999"
|
||||||
|
next_id = str(int(prev_id, 10) + 1) # "1000"
|
||||||
|
if prev_id[0] != next_id[0]: # "0" != "1"
|
||||||
|
next_id = str(int(next_id, 10) * 11) # 1000 * 11 = 11000
|
||||||
|
|
||||||
|
|
||||||
|
In practice you can just ignore the left-most digit, in case you
|
||||||
|
do want to read something into the semantically meaningless
|
||||||
|
build number.
|
||||||
|
|
||||||
|
|
||||||
|
Realities of Verion Numbers
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Nobody knows what the semantics of a version number are, because
|
||||||
|
nobody can guarantee that a given release adheres to whatever
|
||||||
|
convention one would like to imbibe it with. Lets just keep things
|
||||||
|
simple.
|
||||||
|
|
||||||
|
- Version numbers should be recognizable as such, that's what
|
||||||
|
the "v" prefix does.
|
||||||
|
- A number like 201808 is recognizable to many as a number
|
||||||
|
derived from a calendar.
|
||||||
|
- alpha, beta are common parlance indicating software which is
|
||||||
|
still under development.
|
||||||
|
|
||||||
|
Some additional constraints are applied to conform with PEP440
|
||||||
|
|
||||||
|
|
||||||
|
Should I use PyCalVer for my Project?
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
If your project is 1. not useful by itself, but only when used
|
||||||
|
by other software, 2. has a finite scope/a definition of "done",
|
||||||
|
3. your project has CI, a test suite with and decent code
|
||||||
|
coverage, then PyCalVer is worth considering.
|
||||||
|
You release at most once per month.
|
||||||
|
|
||||||
|
|
||||||
|
Marketing/Vanity
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Quotes from http://sedimental.org/designing_a_version.html
|
||||||
|
|
||||||
|
|
||||||
|
Rational
|
||||||
|
--------
|
||||||
|
|
||||||
|
PyCalVer is opinionated software. This keeps things simple,
|
||||||
|
when the opintions match yours, but makes it useless for
|
||||||
|
everybody else.
|
||||||
|
|
||||||
|
The less machine parsable semantics you put in your version
|
||||||
|
string, the better. The ideal would be to only have a single
|
||||||
|
semantic: newer == better.
|
||||||
|
|
||||||
|
Some projects depend recursively on hundreds of libraries, so
|
||||||
|
compatability issues generated by your project can be a heavy
|
||||||
|
burdon on thousands of users; users who learn of the existance
|
||||||
|
of your library for the first time in the form of a stacktrace.
|
||||||
|
PyCalVer is for projects that are comitted to and can maintain
|
||||||
|
backward compatability. Newer versions are always better,
|
||||||
|
updates are always safe, an update won't break things, and if it
|
||||||
|
does, the maintainer's hair is on fire and they will publish a
|
||||||
|
new release containing a fix ASAP.
|
||||||
|
|
||||||
|
Ideally, your user can just declare your library as a
|
||||||
|
dependency, without any extra version qualifier, and never have
|
||||||
|
to think about it again. If you do break something by accident,
|
||||||
|
their remedy is not to change their code, but to temporarily pin
|
||||||
|
an earlier version, until your bugfix release is ready.
|
||||||
|
|
||||||
|
PyCalVer is for projects which are the mundane but dependable
|
||||||
|
foundations of other big shiny projects, which get to do their
|
||||||
|
big and exciting 2.0 major releases.
|
||||||
|
|
||||||
|
|
||||||
|
Breaking Things is a Big Deal
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Using an increment in a version string to express that a release
|
||||||
|
may break client code is not tennable. A developer cannot be
|
||||||
|
expected to think about how their code may or may not break as a
|
||||||
|
consequence of your decision to rename some functions. As the
|
||||||
|
author of any software, there is a great temptation to move fast
|
||||||
|
and break things. This is great when no other software depends
|
||||||
|
on yours. If something breaks, you jump up and fix it. The story
|
||||||
|
is quite different even when only a few dozen people depend on
|
||||||
|
your software.
|
||||||
|
|
||||||
|
|
||||||
|
The less the users of your library have to know about your
|
||||||
|
project, the better. The less they have to deal with issues
|
||||||
|
of compatability, the better. SemVer can be overly specifc
|
||||||
|
for some kinds of projects. If you are writing a library
|
||||||
|
and you have a commitment to backward compatability
|
||||||
|
|
||||||
|
PyCalVer version strings can be parsed according to PEP440
|
||||||
|
https://www.python.org/dev/peps/pep-0440/
|
||||||
|
|
||||||
|
|
||||||
|
A Word on Marketing
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
This setup of expectations for users can go one of two ways,
|
||||||
|
|
||||||
|
We use version numbers to communicate between the authors
|
||||||
|
of software and its users. For users of libraries Particularly
|
||||||
|
for libraries, it pays to keep things as simple as possible for
|
||||||
|
your human users.
|
||||||
|
|
||||||
|
|
||||||
|
Commitment to Compatability
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Software projects can depend on many libraries. Consider that one
|
||||||
|
package introducing a breaking change is enough to mess up your
|
||||||
|
day. Especially in the case of libraries, your users should be
|
||||||
|
able to write code that uses it and not have that code break at
|
||||||
|
any point in the future. Users cannot be asked to keep track of
|
||||||
|
all the changes to every little library that they use.
|
||||||
|
|
||||||
|
PyCalVer is explicitly non semantic. A PyCalVer version number does
|
||||||
|
not express anything about
|
||||||
|
|
||||||
|
- Don't ever break things. When users depend on your
|
||||||
|
software, backward compatability matters and the way to
|
||||||
|
express backward incompatible changes is not to bump a
|
||||||
|
version number, but to change the package name. A change
|
||||||
|
in the package name clearly communicates that a user must
|
||||||
|
change their code so that it will work with the changed
|
||||||
|
API. Everybody who does not have the bandwith for those
|
||||||
|
changes, doesn't even have to be aware of your new
|
||||||
|
release.
|
||||||
|
|
||||||
|
- When you do break something, that should be considered a
|
||||||
|
bug that has to be fixed as quickly as possible in a new
|
||||||
|
version. It should always be safe for a user to update
|
||||||
|
their dependencies. If something does break, users have to
|
||||||
|
temporarilly pin an older (known good) version, or update
|
||||||
|
to a newer fixed version.
|
||||||
|
|
||||||
|
- Version numbers should not require a parser (present
|
||||||
|
package excluded of course). A newer version number should
|
||||||
|
always be lexeographically greater than an older one.
|
||||||
|
TODO:
|
||||||
|
https://setuptools.readthedocs.io/en/latest/setuptools.html#specifying-your-project-s-version
|
||||||
|
|
||||||
|
|
||||||
|
The main component of the version number is based on the
|
||||||
|
calendar date. This is allows you to show your commitment (or
|
||||||
|
lack thereof) to the maintenance of your libarary. It also
|
||||||
|
allows users to see at a glance that their dependency might be
|
||||||
|
out of date. In this versioning scheme it is completely
|
||||||
|
reasonable to bump the version number without any changes,
|
||||||
|
simply to express to your users, that you are still actively
|
||||||
|
maintaining the software and that it is in a known good state.
|
||||||
|
|
||||||
|
|
||||||
|
For a much more detailed exposition of CalVer, see
|
||||||
|
http://sedimental.org/designing_a_version.html
|
||||||
|
https://calver.org/
|
||||||
|
|
||||||
|
from pkg_resources import parse_version
|
||||||
|
|
||||||
|
|
||||||
|
The Life of a Library
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
mylib v201711.001-alpha # birth of a project (in alpha)
|
||||||
|
mylib v201711.002-alpha # new features (in alpha)
|
||||||
|
mylib v201712.003-beta # bugfix release (in beta)
|
||||||
|
mylib v201712.004-rc # release candidate
|
||||||
|
mylib v201712.005 # stable release
|
||||||
|
mylib v201712.006 # stable bugfix release
|
||||||
|
|
||||||
|
mylib2 v201712.007-beta # breaking change (new package name!)
|
||||||
|
mylib2 v201801.008-beta # new features (in beta)
|
||||||
|
mylib2 v201801.009 # stable release
|
||||||
|
|
||||||
|
mylib v201802.007 # security fix for legacy version
|
||||||
|
mylib2 v201802.010 # security fix
|
||||||
|
|
||||||
|
mylib2 v202604.9900 # threshold for four digit build numbers
|
||||||
|
mylib2 v202604.9901 # still four digits in the same month
|
||||||
|
mylib2 v202604.9911 # last build number with four digits
|
||||||
|
mylib2 v202605.09912 # build number zero padding added with date turnover
|
||||||
|
mylib2 v202605.09913 # stable release
|
||||||
|
|
||||||
|
mylib2 v203202.16051-rc # release candidate
|
||||||
|
mylib2 v203202.16052 # stable release
|
||||||
|
|
||||||
|
...
|
||||||
|
v202008.500 # 500 is the limit for four digit build numbers, but
|
||||||
|
v202008.508 # zero padding is added only after the turnover to
|
||||||
|
v202009.0509 # a new month, so that lexical ordering is preserved.
|
||||||
|
|
||||||
|
The date portion of the version, gives the user an indication of
|
||||||
|
how up their dependency is, whether or not a project is still
|
||||||
|
being maintained.
|
||||||
|
|
||||||
|
The build number, gives the user an idea of the maturity of the
|
||||||
|
project. A project which has been around long enough to produce
|
||||||
|
hundreds of builds, might be considered mature, or at least a
|
||||||
|
project that is only on build number 10, is probably still in
|
||||||
|
early development.
|
||||||
|
|
||||||
|
|
||||||
|
FAQ
|
||||||
|
---
|
||||||
|
|
||||||
|
Q: "So you're trying to tell me I need to create a whole new
|
||||||
|
package every time I introduce a introduce a breaking change?!".
|
||||||
|
|
||||||
|
A: First of all, what the hell are you doing? Secondly, YES!
|
||||||
|
Let's assume your little package has even just 100 users. Do you
|
||||||
|
have any idea about the total effort that will be expended
|
||||||
|
because you decided it would be nice to change the name of a
|
||||||
|
function? It is completely reasonable introduce that the
|
||||||
|
friction for the package author when the price to users is
|
||||||
|
orders of magnitude larger.
|
||||||
|
|
||||||
|
|
||||||
|
1801
|
||||||
|
|
||||||
|
https://calver.org/
|
||||||
|
|
||||||
|
I have given up on the idea that version numbers express
|
||||||
|
anything about changes made between versions. Trying to
|
||||||
|
express such information assumes 1. that the author of a package
|
||||||
|
is aware of how a given change needs to be reflected in a
|
||||||
|
version number and 2. that users and packaging softare correctly
|
||||||
|
parse that meaning. When I used semantic versioning, I realized that
|
||||||
|
the major version number of my packages would never change, because I don't think breaking changes should ever be One of the biggest offenses expres
|
||||||
10
license.header
Normal file
10
license.header
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
Individual files contain the following tag instead of the full license text.
|
||||||
|
|
||||||
|
This file is part of the pycalver project
|
||||||
|
https://github.com/mbarkhau/pycalver
|
||||||
|
|
||||||
|
(C) 2018 Manuel Barkhau (@mbarkhau)
|
||||||
|
SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
This enables machine processing of license information based on the SPDX
|
||||||
|
License Identifiers that are here available: https://spdx.org/licenses/
|
||||||
109
scratch.py
Normal file
109
scratch.py
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
from pkg_resources import parse_version
|
||||||
|
import re
|
||||||
|
|
||||||
|
canonical_version_re = re.compile(r"""
|
||||||
|
\b
|
||||||
|
(?P<version>
|
||||||
|
(?P<calver>
|
||||||
|
v # "v" version prefix
|
||||||
|
(?P<year>\d{4})
|
||||||
|
(?P<month>\d{2})
|
||||||
|
)
|
||||||
|
(?:
|
||||||
|
\. # "." build nr prefix
|
||||||
|
(?P<build_nr>\d{3,})
|
||||||
|
)
|
||||||
|
)(?:\s|$)
|
||||||
|
""", flags=re.VERBOSE)
|
||||||
|
|
||||||
|
|
||||||
|
full_version_re = re.compile(r"""
|
||||||
|
\b
|
||||||
|
(?P<version>
|
||||||
|
(?P<calver>
|
||||||
|
v # "v" version prefix
|
||||||
|
(?P<year>\d{4})
|
||||||
|
(?P<month>\d{2})
|
||||||
|
)
|
||||||
|
(?:
|
||||||
|
\. # "." build nr prefix
|
||||||
|
(?P<build>\d+)
|
||||||
|
)
|
||||||
|
(?:
|
||||||
|
\- # "-" tag prefix
|
||||||
|
(?P<tag>a|alpha|b|beta|dev|c|rc|pre|preview|post)
|
||||||
|
(?P<tag_nr>\d*)
|
||||||
|
)?
|
||||||
|
)(?:\s|$)
|
||||||
|
""", flags=re.VERBOSE)
|
||||||
|
|
||||||
|
versions = [
|
||||||
|
"v201711.0001-alpha",
|
||||||
|
"v201711.0002-alpha",
|
||||||
|
"v201712.0003-beta3",
|
||||||
|
"v201712.0004-preview",
|
||||||
|
"v201712.0005",
|
||||||
|
"v201712.0006",
|
||||||
|
"v201712.0007-beta",
|
||||||
|
"v201801.0008-beta",
|
||||||
|
"v201801.0008-dev",
|
||||||
|
"v201801.0009",
|
||||||
|
"v201802.0010",
|
||||||
|
"v201802.0007",
|
||||||
|
"v201904.0050",
|
||||||
|
"v201905.0051",
|
||||||
|
"v201905.0052",
|
||||||
|
"v201712.0027-beta1",
|
||||||
|
"v201712.beta-0027",
|
||||||
|
"v201712.post-0027",
|
||||||
|
"v201712.post-0027",
|
||||||
|
]
|
||||||
|
|
||||||
|
for vstr in versions:
|
||||||
|
v = parse_version(vstr)
|
||||||
|
print(vstr.ljust(20), repr(v).ljust(30), int(v.is_prerelease))
|
||||||
|
v = full_version_re.match(vstr)
|
||||||
|
print("\t", v and v.groupdict())
|
||||||
|
v = canonical_version_re.match(vstr)
|
||||||
|
print("\t", v and v.groupdict())
|
||||||
|
|
||||||
|
a = "v201711.beta-0001"
|
||||||
|
b = "v201711.0002-beta"
|
||||||
|
c = "v201711.0002"
|
||||||
|
d = "0.9.2"
|
||||||
|
|
||||||
|
va = parse_version(a)
|
||||||
|
vb = parse_version(b)
|
||||||
|
vc = parse_version(c)
|
||||||
|
vd = parse_version(d)
|
||||||
|
|
||||||
|
print(a, repr(va))
|
||||||
|
print(b, repr(vb))
|
||||||
|
print(c, repr(vc))
|
||||||
|
print(d, repr(vd), vd < vc)
|
||||||
|
|
||||||
|
|
||||||
|
# https://regex101.com/r/fnj60p/3
|
||||||
|
pycalver_re = re.compile(r"""
|
||||||
|
\b
|
||||||
|
(?P<full_version>
|
||||||
|
(?P<calver>
|
||||||
|
v # "v" version prefix
|
||||||
|
(?P<year>\d{4})
|
||||||
|
(?P<month>\d{2})
|
||||||
|
)
|
||||||
|
(?:
|
||||||
|
. # "." build nr prefix
|
||||||
|
(?P<build_nr>\d{4,})
|
||||||
|
)
|
||||||
|
)(?:\s|$)
|
||||||
|
""", flags=re.VERBOSE)
|
||||||
|
|
||||||
|
print(pycalver_re.match("v201712.0027").groupdict())
|
||||||
|
|
||||||
|
print(repr(parse_version("v201712.0027")))
|
||||||
|
print(repr(parse_version("v201712.0027")))
|
||||||
|
print(repr(parse_version("v201712.0027")))
|
||||||
|
print(repr(parse_version("v201712") == parse_version("v201712.0")))
|
||||||
|
|
||||||
|
print("v201712.0027" > "v201712.0028")
|
||||||
35
setup.cfg
Normal file
35
setup.cfg
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
[flake8]
|
||||||
|
ignore = E203
|
||||||
|
max-line-length = 110
|
||||||
|
exclude = .git,__pycache__,.eggs/,dist/,.mypy_cache
|
||||||
|
|
||||||
|
[mypy]
|
||||||
|
check_untyped_defs = True
|
||||||
|
disallow_untyped_calls = True
|
||||||
|
follow_imports = silent
|
||||||
|
strict_optional = True
|
||||||
|
|
||||||
|
[bdist_wheel]
|
||||||
|
universal = 1
|
||||||
|
|
||||||
|
[pycalver]
|
||||||
|
current_version = v201808.0001-beta
|
||||||
|
commit = True
|
||||||
|
tag = True
|
||||||
|
|
||||||
|
[pycalver:file:setup.cfg]
|
||||||
|
patterns =
|
||||||
|
current_version = {version}
|
||||||
|
|
||||||
|
[pycalver:file:setup.py]
|
||||||
|
patterns =
|
||||||
|
__version__ = "{version}"
|
||||||
|
|
||||||
|
[pycalver:file:src/pycalver/__init__.py]
|
||||||
|
patterns =
|
||||||
|
__version__ = "{version}"
|
||||||
|
|
||||||
|
[pycalver:file:README.rst]
|
||||||
|
patterns =
|
||||||
|
badge/CalVer-{calver}{build}-{release}-blue.svg
|
||||||
|
:alt: CalVer {version}
|
||||||
80
setup.py
Normal file
80
setup.py
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# (C) 2018 Manuel Barkhau (mbarkhau@gmail.com)
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import setuptools
|
||||||
|
|
||||||
|
|
||||||
|
def project_path(filename):
|
||||||
|
dirpath = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
return os.path.join(dirpath, filename)
|
||||||
|
|
||||||
|
|
||||||
|
def read(filename):
|
||||||
|
with open(project_path(filename), mode="rb") as fh:
|
||||||
|
return fh.read().decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
packages = setuptools.find_packages(project_path("src"))
|
||||||
|
package_dir = {"": "src"}
|
||||||
|
|
||||||
|
|
||||||
|
if any(arg.startswith("bdist") for arg in sys.argv):
|
||||||
|
import lib3to6
|
||||||
|
package_dir = lib3to6.fix(package_dir)
|
||||||
|
|
||||||
|
|
||||||
|
long_description = (
|
||||||
|
read("README.rst") +
|
||||||
|
"\n\n" +
|
||||||
|
read("CHANGELOG.rst")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
name="pycalver",
|
||||||
|
license="MIT",
|
||||||
|
author="Manuel Barkhau",
|
||||||
|
author_email="mbarkhau@gmail.com",
|
||||||
|
url="https://github.com/mbarkhau/pycalver",
|
||||||
|
version="201809.1a0",
|
||||||
|
|
||||||
|
description="CalVer versioning for python projects",
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type="text/x-rst",
|
||||||
|
|
||||||
|
packages=packages,
|
||||||
|
package_dir=package_dir,
|
||||||
|
zip_safe=True,
|
||||||
|
install_requires=["typing", "click", "setuptools"],
|
||||||
|
setup_requires=["lib3to6==v201809.0017-alpha"],
|
||||||
|
entry_points='''
|
||||||
|
[console_scripts]
|
||||||
|
pycalver=pycalver.__main__:cli
|
||||||
|
''',
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: Console",
|
||||||
|
"Environment :: Other Environment",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: Unix",
|
||||||
|
"Operating System :: POSIX",
|
||||||
|
"Operating System :: Microsoft :: Windows",
|
||||||
|
"Operating System :: MacOS :: MacOS X",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Programming Language :: Python :: 2.7",
|
||||||
|
"Programming Language :: Python :: 3.4",
|
||||||
|
"Programming Language :: Python :: 3.5",
|
||||||
|
"Programming Language :: Python :: 3.6",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
|
"Topic :: Software Development :: Libraries",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
],
|
||||||
|
)
|
||||||
11
src/pycalver/__init__.py
Normal file
11
src/pycalver/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# (C) 2018 Manuel Barkhau (@mbarkhau)
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
__version__ = "v201809.0001-beta"
|
||||||
|
|
||||||
|
DEBUG = os.environ.get("PYCALVER_DEBUG", "0") == "1"
|
||||||
146
src/pycalver/__main__.py
Normal file
146
src/pycalver/__main__.py
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# (C) 2018 Manuel Barkhau (@mbarkhau)
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import click
|
||||||
|
import logging
|
||||||
|
import typing as typ
|
||||||
|
|
||||||
|
from . import DEBUG
|
||||||
|
from . import vcs
|
||||||
|
from . import parse
|
||||||
|
from . import config
|
||||||
|
from . import version
|
||||||
|
from . import rewrite
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger("pycalver.__main__")
|
||||||
|
|
||||||
|
|
||||||
|
def _init_loggers(verbose: bool) -> None:
|
||||||
|
if DEBUG:
|
||||||
|
log_formatter = logging.Formatter('%(levelname)s - %(name)s - %(message)s')
|
||||||
|
log_level = logging.DEBUG
|
||||||
|
elif verbose:
|
||||||
|
log_formatter = logging.Formatter('%(levelname)s - %(message)s')
|
||||||
|
log_level = logging.INFO
|
||||||
|
else:
|
||||||
|
log_formatter = logging.Formatter('%(message)s')
|
||||||
|
log_level = logging.WARNING
|
||||||
|
|
||||||
|
loggers = [log, vcs.log, parse.log, config.log, rewrite.log, version.log]
|
||||||
|
|
||||||
|
for logger in loggers:
|
||||||
|
if len(logger.handlers) == 0:
|
||||||
|
ch = logging.StreamHandler(sys.stderr)
|
||||||
|
ch.setFormatter(log_formatter)
|
||||||
|
logger.addHandler(ch)
|
||||||
|
logger.setLevel(log_level)
|
||||||
|
|
||||||
|
log.debug("Loggers initialized.")
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
def cli():
|
||||||
|
"""parse and update project versions automatically."""
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def show() -> None:
|
||||||
|
_init_loggers(verbose=False)
|
||||||
|
|
||||||
|
cfg = config.parse()
|
||||||
|
if cfg is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Current Version: {cfg['current_version']}")
|
||||||
|
print(f"PEP440 Version: {cfg['pep440_version']}")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option(
|
||||||
|
"--dry",
|
||||||
|
default=False,
|
||||||
|
is_flag=True,
|
||||||
|
help="Display diff of changes, don't rewrite files.",
|
||||||
|
)
|
||||||
|
def init(dry: bool) -> None:
|
||||||
|
"""Initialize [pycalver] configuration in setup.cfg"""
|
||||||
|
_init_loggers(verbose=False)
|
||||||
|
|
||||||
|
cfg = config.parse()
|
||||||
|
if cfg:
|
||||||
|
log.error("Configuration already initialized in setup.cfg")
|
||||||
|
return
|
||||||
|
|
||||||
|
cfg_lines = config.default_config_lines()
|
||||||
|
|
||||||
|
if dry:
|
||||||
|
print("Exiting because of '--dry'. Would have written to setup.cfg:")
|
||||||
|
print("\n " + "\n ".join(cfg_lines))
|
||||||
|
return
|
||||||
|
|
||||||
|
if os.path.exists("setup.cfg"):
|
||||||
|
cfg_content = "\n" + "\n".join(cfg_lines)
|
||||||
|
with io.open("setup.cfg", mode="at", encoding="utf-8") as fh:
|
||||||
|
fh.write(cfg_content)
|
||||||
|
print("Updated setup.cfg")
|
||||||
|
else:
|
||||||
|
cfg_content = "\n".join(cfg_lines)
|
||||||
|
with io.open("setup.cfg", mode="at", encoding="utf-8") as fh:
|
||||||
|
fh.write(cfg_content)
|
||||||
|
print("Created setup.cfg")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option(
|
||||||
|
"--verbose",
|
||||||
|
default=False,
|
||||||
|
is_flag=True,
|
||||||
|
help="Log applied changes to stderr",
|
||||||
|
)
|
||||||
|
@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="Override release name of current_version",
|
||||||
|
)
|
||||||
|
def bump(verbose: bool, dry: bool, release: typ.Optional[str] = None) -> None:
|
||||||
|
_init_loggers(verbose)
|
||||||
|
if release and release not in parse.VALID_RELESE_VALUES:
|
||||||
|
log.error(f"Invalid argument --release={release}")
|
||||||
|
log.error(f"Valid arguments are: {', '.join(parse.VALID_RELESE_VALUES)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
cfg = config.parse()
|
||||||
|
|
||||||
|
if cfg is None:
|
||||||
|
log.error("Unable to parse pycalver configuration from setup.cfg")
|
||||||
|
return
|
||||||
|
|
||||||
|
old_version = cfg["current_version"]
|
||||||
|
new_version = version.bump(old_version, release=release)
|
||||||
|
|
||||||
|
log.info(f"Old Version: {old_version}")
|
||||||
|
log.info(f"New Version: {new_version}")
|
||||||
|
|
||||||
|
matches: typ.List[parse.PatternMatch]
|
||||||
|
for filepath, patterns in cfg["file_patterns"].items():
|
||||||
|
with io.open(filepath, mode="rt", encoding="utf-8") as fh:
|
||||||
|
content = fh.read()
|
||||||
|
lines = content.splitlines()
|
||||||
|
matches = parse.parse_patterns(lines, patterns)
|
||||||
|
for m in matches:
|
||||||
|
print(m)
|
||||||
122
src/pycalver/config.py
Normal file
122
src/pycalver/config.py
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# (C) 2018 Manuel Barkhau (@mbarkhau)
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import configparser
|
||||||
|
import pkg_resources
|
||||||
|
import typing as typ
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .parse import PYCALVER_RE
|
||||||
|
|
||||||
|
log = logging.getLogger("pycalver.config")
|
||||||
|
|
||||||
|
|
||||||
|
def parse(config_file="setup.cfg") -> typ.Optional[typ.Dict[str, typ.Any]]:
|
||||||
|
if not os.path.exists(config_file):
|
||||||
|
log.error("File not found: setup.cfg")
|
||||||
|
return None
|
||||||
|
|
||||||
|
cfg_parser = configparser.RawConfigParser("")
|
||||||
|
with io.open(config_file, mode="rt", encoding="utf-8") as fh:
|
||||||
|
cfg_parser.readfp(fh)
|
||||||
|
|
||||||
|
if "pycalver" not in cfg_parser:
|
||||||
|
log.error("setup.cfg does not contain a [pycalver] section.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
cfg = dict(cfg_parser.items("pycalver"))
|
||||||
|
|
||||||
|
if "current_version" not in cfg:
|
||||||
|
log.error("setup.cfg does not have 'pycalver.current_version'")
|
||||||
|
return None
|
||||||
|
|
||||||
|
current_version = cfg["current_version"]
|
||||||
|
if PYCALVER_RE.match(current_version) is None:
|
||||||
|
log.error(f"setup.cfg 'pycalver.current_version is invalid")
|
||||||
|
log.error(f"current_version = {current_version}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
cfg["pep440_version"] = str(pkg_resources.parse_version(current_version))
|
||||||
|
|
||||||
|
cfg["tag"] = cfg.get("tag", "").lower() in ("yes", "true", "1", "on")
|
||||||
|
cfg["commit"] = cfg.get("commit", "").lower() in ("yes", "true", "1", "on")
|
||||||
|
|
||||||
|
cfg["file_patterns"] = {}
|
||||||
|
|
||||||
|
for section_name in cfg_parser.sections():
|
||||||
|
if not section_name.startswith("pycalver:file:"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
filepath = section_name.split(":", 2)[-1]
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
log.error(f"No such file: {filepath} from {section_name} in setup.cfg")
|
||||||
|
return None
|
||||||
|
|
||||||
|
section = dict(cfg_parser.items(section_name))
|
||||||
|
|
||||||
|
if "patterns" in section:
|
||||||
|
cfg["file_patterns"][filepath] = [
|
||||||
|
line.strip()
|
||||||
|
for line in section["patterns"].splitlines()
|
||||||
|
if line.strip()
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
cfg["file_patterns"][filepath] = ["{version}", "{pep440_version}"]
|
||||||
|
|
||||||
|
if not cfg["file_patterns"]:
|
||||||
|
cfg["file_patterns"]["setup.cfg"] = ["{version}", "{pep440_version}"]
|
||||||
|
|
||||||
|
log.debug(f"Config Parsed: {cfg}")
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
def default_config_lines() -> typ.List[str]:
|
||||||
|
initial_version = dt.datetime.now().strftime("v%Y%m.0001-dev")
|
||||||
|
|
||||||
|
cfg_lines = [
|
||||||
|
"[pycalver]",
|
||||||
|
f"current_version = {initial_version}",
|
||||||
|
"commit = True",
|
||||||
|
"tag = True",
|
||||||
|
"",
|
||||||
|
"[pycalver:file:setup.cfg]",
|
||||||
|
"patterns = ",
|
||||||
|
" current_version = {version}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
if os.path.exists("setup.py"):
|
||||||
|
cfg_lines.extend([
|
||||||
|
"[pycalver:file:setup.py]",
|
||||||
|
"patterns = ",
|
||||||
|
" \"{version}\"",
|
||||||
|
" \"{pep440_version}\"",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
|
||||||
|
if os.path.exists("README.rst"):
|
||||||
|
cfg_lines.extend([
|
||||||
|
"[pycalver:file:README.rst]",
|
||||||
|
"patterns = ",
|
||||||
|
" {version}",
|
||||||
|
" {pep440_version}",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
|
||||||
|
if os.path.exists("README.md"):
|
||||||
|
cfg_lines.extend([
|
||||||
|
"[pycalver:file:README.md]",
|
||||||
|
" {version}",
|
||||||
|
" {pep440_version}",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
|
||||||
|
return cfg_lines
|
||||||
137
src/pycalver/lex_id.py
Normal file
137
src/pycalver/lex_id.py
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# (C) 2018 Manuel Barkhau (@mbarkhau)
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
This is a simple scheme for numerical ids which are ordered both
|
||||||
|
numerically and lexically.
|
||||||
|
|
||||||
|
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:
|
||||||
|
num_digits = len(prev_id)
|
||||||
|
_prev_id = int(prev_id, 10)
|
||||||
|
_next_id = int(_prev_id) + 1
|
||||||
|
next_id = f"{_next_id:0{num_digits}}"
|
||||||
|
if prev_id[0] != next_id[0]:
|
||||||
|
next_id = str(_next_id * 11)
|
||||||
|
return next_id
|
||||||
|
|
||||||
|
|
||||||
|
def ord_val(lex_id: str) -> int:
|
||||||
|
if len(lex_id) == 1:
|
||||||
|
return int(lex_id, 10)
|
||||||
|
return int(lex_id[1:], 10)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
_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)
|
||||||
|
|
||||||
|
assert _curr_id < next_id
|
||||||
|
assert int(_curr_id, 10) < int(next_id, 10)
|
||||||
|
assert ord_val(_curr_id) < ord_val(next_id)
|
||||||
|
|
||||||
|
# while next_id.startswith("0") and int(next_id) < 1000:
|
||||||
|
# _next_id = next_id(_next_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()
|
||||||
94
src/pycalver/parse.py
Normal file
94
src/pycalver/parse.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# (C) 2018 Manuel Barkhau (@mbarkhau)
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import re
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import typing as typ
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from pkg_resources import parse_version
|
||||||
|
|
||||||
|
from . import lex_id
|
||||||
|
|
||||||
|
log = logging.getLogger("pycalver.parse")
|
||||||
|
|
||||||
|
|
||||||
|
VALID_RELESE_VALUES = ("alpha", "beta", "dev", "rc", "post")
|
||||||
|
|
||||||
|
|
||||||
|
PYCALVER_RE: typ.re.Pattern[str] = re.compile(r"""
|
||||||
|
\b
|
||||||
|
(?P<version>
|
||||||
|
(?P<calver>
|
||||||
|
v # "v" version prefix
|
||||||
|
(?P<year>\d{4})
|
||||||
|
(?P<month>\d{2})
|
||||||
|
)
|
||||||
|
(?:
|
||||||
|
\. # "." build nr prefix
|
||||||
|
(?P<build>\d{4,})
|
||||||
|
)
|
||||||
|
(?:
|
||||||
|
\- # "-" release prefix
|
||||||
|
(?P<release>
|
||||||
|
alpha|beta|dev|rc|post
|
||||||
|
)
|
||||||
|
)?
|
||||||
|
)(?:\s|$)
|
||||||
|
""", flags=re.VERBOSE)
|
||||||
|
|
||||||
|
|
||||||
|
RE_PATTERN_PARTS = {
|
||||||
|
"pep440_version" : r"\d{6}\.\d+(a|b|dev|rc|post)?\d*",
|
||||||
|
"version" : r"v\d{6}\.\d{4,}\-(?:alpha|beta|dev|rc|post)",
|
||||||
|
"calver" : r"v\d{6}",
|
||||||
|
"build" : r"\.\d{4,}",
|
||||||
|
"release" : r"(\-(?:alpha|beta|dev|rc|post))?",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PatternMatch(typ.NamedTuple):
|
||||||
|
|
||||||
|
lineno : int
|
||||||
|
line : str
|
||||||
|
pattern : str
|
||||||
|
span : typ.Tuple[int, int]
|
||||||
|
match : str
|
||||||
|
|
||||||
|
|
||||||
|
MaybeMatch = typ.Optional[typ.re.Match[str]]
|
||||||
|
PyCalVerInfo = typ.Dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
def iter_pattern_matches(lines: typ.List[str], pattern: str) -> typ.Iterable[PatternMatch]:
|
||||||
|
# The pattern is escaped, so that everything besides the format
|
||||||
|
# string variables is treated literally.
|
||||||
|
pattern_re = re.compile(
|
||||||
|
pattern
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("-", "\\-")
|
||||||
|
.replace(".", "\\.")
|
||||||
|
.replace("+", "\\+")
|
||||||
|
.replace("*", "\\*")
|
||||||
|
.replace("[", "\\[")
|
||||||
|
.replace("(", "\\(")
|
||||||
|
.format(**RE_PATTERN_PARTS)
|
||||||
|
)
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
match = pattern_re.search(line)
|
||||||
|
if match:
|
||||||
|
lineno = i + 1
|
||||||
|
yield PatternMatch(lineno, line, pattern, match.span(), match.group(0))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_patterns(lines: typ.List[str], patterns: typ.List[str]) -> typ.List[PatternMatch]:
|
||||||
|
all_matches: typ.List[PatternMatch] = []
|
||||||
|
for pattern in patterns:
|
||||||
|
all_matches.extend(iter_pattern_matches(lines, pattern))
|
||||||
|
return all_matches
|
||||||
21
src/pycalver/rewrite.py
Normal file
21
src/pycalver/rewrite.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# (C) 2018 Manuel Barkhau (@mbarkhau)
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import difflib
|
||||||
|
|
||||||
|
log = logging.getLogger("pycalver.rewrite")
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_file(file: str, pattern: str, dry=False) -> None:
|
||||||
|
difflib.unified_diff(
|
||||||
|
file_content_before.splitlines(),
|
||||||
|
file_content_after.splitlines(),
|
||||||
|
lineterm="",
|
||||||
|
fromfile="a/" + file,
|
||||||
|
tofile="b/" + file,
|
||||||
|
)
|
||||||
111
src/pycalver/vcs.py
Normal file
111
src/pycalver/vcs.py
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# (C) 2018 Manuel Barkhau (@mbarkhau)
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
#
|
||||||
|
# pycalver/vcs.py (this file) is based on code from the
|
||||||
|
# bumpversion project: https://github.com/peritus/bumpversion
|
||||||
|
# MIT License - (C) 2013-2014 Filip Noetzel
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
import subprocess as sp
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger("pycalver.vcs")
|
||||||
|
|
||||||
|
|
||||||
|
class WorkingDirectoryIsDirtyException(Exception):
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
class BaseVCS:
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def commit(cls, message):
|
||||||
|
f = tempfile.NamedTemporaryFile("wb", delete=False)
|
||||||
|
f.write(message.encode("utf-8"))
|
||||||
|
f.close()
|
||||||
|
cmd = cls._COMMIT_COMMAND + [f.name]
|
||||||
|
env_items = list(os.environ.items()) + [(b"HGENCODING", b"utf-8")]
|
||||||
|
sp.check_output(cmd, env=dict(env_items))
|
||||||
|
os.unlink(f.name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_usable(cls):
|
||||||
|
try:
|
||||||
|
return sp.call(
|
||||||
|
cls._TEST_USABLE_COMMAND,
|
||||||
|
stderr=sp.PIPE,
|
||||||
|
stdout=sp.PIPE,
|
||||||
|
) == 0
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == 2:
|
||||||
|
# mercurial is not installed then, ok.
|
||||||
|
return False
|
||||||
|
raise
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def assert_nondirty(cls):
|
||||||
|
status_output = sp.check_output(cls._STATUS_COMMAND)
|
||||||
|
lines = [
|
||||||
|
line.strip()
|
||||||
|
for line in status_output.splitlines()
|
||||||
|
if not line.strip().startswith(b"??")
|
||||||
|
]
|
||||||
|
|
||||||
|
if lines:
|
||||||
|
cleaned_output = b"\n".join(lines)
|
||||||
|
cls_name = cls.__name__
|
||||||
|
raise WorkingDirectoryIsDirtyException(
|
||||||
|
f"{cls_name} working directory is not clean:\n{cleaned_output}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Git(BaseVCS):
|
||||||
|
|
||||||
|
_TEST_USABLE_COMMAND = ["git", "rev-parse", "--git-dir"]
|
||||||
|
_COMMIT_COMMAND = ["git", "commit", "-F"]
|
||||||
|
_STATUS_COMMAND = ["git", "status", "--porcelain"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tag(cls, name):
|
||||||
|
sp.check_output(["git", "tag", name])
|
||||||
|
|
||||||
|
|
||||||
|
class Mercurial(BaseVCS):
|
||||||
|
|
||||||
|
_TEST_USABLE_COMMAND = ["hg", "root"]
|
||||||
|
_COMMIT_COMMAND = ["hg", "commit", "--logfile"]
|
||||||
|
_STATUS_COMMAND = ["hg", "status", "-mard"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tag(cls, name):
|
||||||
|
sp.check_output(["hg", "tag", name])
|
||||||
|
|
||||||
|
|
||||||
|
VCS = [Git, Mercurial]
|
||||||
|
|
||||||
|
|
||||||
|
def get_vcs(allow_dirty=False):
|
||||||
|
for vcs in VCS:
|
||||||
|
if not vcs.is_usable():
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not allow_dirty:
|
||||||
|
try:
|
||||||
|
vcs.assert_nondirty()
|
||||||
|
except WorkingDirectoryIsDirtyException as e:
|
||||||
|
log.warn(
|
||||||
|
f"{e.message}\n\n"
|
||||||
|
f"Use --allow-dirty to override this if you know what you're doing."
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return vcs
|
||||||
|
|
||||||
|
return None
|
||||||
71
src/pycalver/version.py
Normal file
71
src/pycalver/version.py
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# (C) 2018 Manuel Barkhau (@mbarkhau)
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from . import lex_id
|
||||||
|
log = logging.getLogger("pycalver.version")
|
||||||
|
|
||||||
|
|
||||||
|
def current_calver() -> str:
|
||||||
|
return dt.datetime.utcnow().strftime("v%Y%m")
|
||||||
|
|
||||||
|
|
||||||
|
def bump(old_version: str, release: str=None) -> str:
|
||||||
|
# old_version is assumed to be a valid calver string,
|
||||||
|
# validated in pycalver.config.parse.
|
||||||
|
|
||||||
|
old_calver, rest = old_version.split(".")
|
||||||
|
old_build, old_release = rest.split("-")
|
||||||
|
|
||||||
|
new_calver = current_calver()
|
||||||
|
new_build = lex_id.next_id(old_build)
|
||||||
|
if release is None:
|
||||||
|
# preserve existing release
|
||||||
|
new_release = old_release
|
||||||
|
else:
|
||||||
|
new_release = release
|
||||||
|
|
||||||
|
new_version = new_calver + "." + new_build
|
||||||
|
if new_release:
|
||||||
|
new_version += "-" + new_release
|
||||||
|
return new_version
|
||||||
|
|
||||||
|
|
||||||
|
def incr_version(old_version: str, *, tag: str="__sentinel__") -> str:
|
||||||
|
maybe_match: MaybeMatch = VERSION_RE.search(old_version)
|
||||||
|
|
||||||
|
if maybe_match is None:
|
||||||
|
raise ValueError(f"Invalid version string: {old_version}")
|
||||||
|
|
||||||
|
prev_version_info: PyCalVerInfo = maybe_match.groupdict()
|
||||||
|
|
||||||
|
prev_calver: str = prev_version_info["calver"]
|
||||||
|
next_calver: str = current_calver()
|
||||||
|
|
||||||
|
prev_build: str = prev_version_info["build"]
|
||||||
|
if prev_calver > next_calver:
|
||||||
|
log.warning(
|
||||||
|
f"'incr_version' called with '{old_version}', " +
|
||||||
|
f"which is from the future, " +
|
||||||
|
f"maybe your system clock is out of sync."
|
||||||
|
)
|
||||||
|
next_calver = prev_calver # leave calver as is
|
||||||
|
|
||||||
|
next_build = lex_id.next_id(prev_build)
|
||||||
|
new_version = f"{next_calver}.{next_build}"
|
||||||
|
if tag != "__sentinel__":
|
||||||
|
if tag is None:
|
||||||
|
pass # tag explicitly ignored/removed
|
||||||
|
else:
|
||||||
|
new_version += "-" + tag
|
||||||
|
elif "tag" in prev_version_info:
|
||||||
|
# preserve previous tag
|
||||||
|
new_version += "-" + prev_version_info["tag"]
|
||||||
|
|
||||||
|
assert old_version < new_version, f"{old_version} {new_version}"
|
||||||
|
return new_version
|
||||||
0
test/__init__.py
Normal file
0
test/__init__.py
Normal file
0
test/test_config.py
Normal file
0
test/test_config.py
Normal file
30
test/test_version.py
Normal file
30
test/test_version.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import datetime as dt
|
||||||
|
import pycalver.version
|
||||||
|
|
||||||
|
|
||||||
|
def test_calver():
|
||||||
|
import random
|
||||||
|
|
||||||
|
first_version_str = "v201808.0001-dev"
|
||||||
|
padding = len(first_version_str) + 3
|
||||||
|
version_str = first_version_str
|
||||||
|
|
||||||
|
def _current_calver() -> str:
|
||||||
|
_current_calver.delta += dt.timedelta(days=int(random.random() * 5))
|
||||||
|
|
||||||
|
return (dt.datetime.utcnow() + _current_calver.delta).strftime("v%Y%m")
|
||||||
|
|
||||||
|
_current_calver.delta = dt.timedelta(days=1)
|
||||||
|
|
||||||
|
global current_calver
|
||||||
|
current_calver = _current_calver
|
||||||
|
|
||||||
|
for i in range(1050):
|
||||||
|
version_str = incr_version(version_str, tag=random.choice([
|
||||||
|
None, "alpha", "beta", "rc"
|
||||||
|
]))
|
||||||
|
print(f"{version_str:<{padding}}", end=" ")
|
||||||
|
if (i + 1) % 8 == 0:
|
||||||
|
print()
|
||||||
|
|
||||||
|
print()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue