readme updates

This commit is contained in:
Manuel Barkhau 2020-10-09 22:11:55 +00:00
parent 3efb72dd3c
commit 15c560a0c5
4 changed files with 764 additions and 244 deletions

View file

@ -73,20 +73,7 @@ pycalver_deps.svg:
-o pycalver_deps.svg -o pycalver_deps.svg
README.md: src/pycalver/__main__.py makefile ## Update cli reference in README.md
README.md: src/pycalver/__main__.py scripts/update_readme_examples.py Makefile
@git add README.md @git add README.md
@printf '\n```\n$$ pycalver --help\n' > /tmp/pycalver_help.txt @$(DEV_ENV)/bin/python scripts/update_readme_examples.py
@$(DEV_ENV)/bin/pycalver --help >> /tmp/pycalver_help.txt
@printf '```\n\n' >> /tmp/pycalver_help.txt
sed -i -ne '/<!-- BEGIN pycalver --help -->/ {p; r /tmp/pycalver_help.txt' \
-e ':a; n; /<!-- END pycalver --help -->/ {p; b}; ba}; p' \
README.md
@printf '\n```\n$$ pycalver bump --help\n' > /tmp/pycalver_help.txt
@$(DEV_ENV)/bin/pycalver bump --help >> /tmp/pycalver_help.txt
@printf '```\n\n' >> /tmp/pycalver_help.txt
sed -i -ne '/<!-- BEGIN pycalver bump --help -->/ {p; r /tmp/pycalver_help.txt' \
-e ':a; n; /<!-- END pycalver bump --help -->/ {p; b}; ba}; p' \
README.md

756
README.md
View file

@ -7,8 +7,11 @@
# [PyCalVer: Automatic Calendar Versioning][url_repo] # [PyCalVer: Automatic Calendar Versioning][url_repo]
PyCalVer is a CLI-tool to search and replace all version strings in your project files ([calver][url_calver_org], [semver][url_semver_org] or otherwise). PyCalVer has support for
PyCalVer is a CLI-tool to search and replace version strings in your project files ([calver][url_calver_org], [semver][url_semver_org] or otherwise) . - Configurable version patterns
- Git, Mercurial or no VCS
- Operates only on plaintext files, so it can be used for any project, not just python projects.
[url_repo]: https://gitlab.com/mbarkhau/pycalver [url_repo]: https://gitlab.com/mbarkhau/pycalver
[url_calver_org]: https://calver.org/ [url_calver_org]: https://calver.org/
@ -69,99 +72,402 @@ Code Quality/CI:
[url_pyversions]: https://pypi.python.org/pypi/pycalver [url_pyversions]: https://pypi.python.org/pypi/pycalver
<!--
To update the TOC:
$ pip install md-toc
$ md_toc -i gitlab README.md
-->
[](TOC) [](TOC)
- [PyCalVer: Automatic Calendar Versioning](#pycalver-automatic-calendar-versioning)
- [Usage](#usage) - [Usage](#usage)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Pattern Search and Replacement](#pattern-search-and-replacement) - [Pattern Search and Replacement](#pattern-search-and-replacement)
- [Week Numbering](#week-numbering)
- [Normalization Caveats](#normalization-caveats)
- [Legacy Patterns](#legacy-patterns)
- [Pattern Usage](#pattern-usage)
- [Examples](#examples) - [Examples](#examples)
- [Version State](#version-state) - [Version State](#version-state)
- [The Current Version](#the-current-version) - [The Current Version](#the-current-version)
- [Bump It Up](#bump-it-up) - [Bump It Up](#bump-it-up)
- [Config Parameters](#config-parameters)
- [CLI Reference](#cli-reference)
- [The PyCalVer Format](#the-pycalver-format) - [The PyCalVer Format](#the-pycalver-format)
- [Parsing](#parsing) - [Parsing](#parsing)
- [Incrementing Behaviour](#incrementing-behaviour) - [Incrementing Behaviour](#incrementing-behaviour)
- [Lexical Ids](#lexical-ids)
- [Semantics of PyCalVer](#semantics-of-pycalver) - [Semantics of PyCalVer](#semantics-of-pycalver)
- [Pitch](#pitch)
- [blah](#blah)
- [Intentional Breaking Changes](#intentional-breaking-changes) - [Intentional Breaking Changes](#intentional-breaking-changes)
- [Costs and Benefits](#costs-and-benefits) - [Costs and Benefits](#costs-and-benefits)
- [Unintentional Breaking Changes](#unintentional-breaking-changes) - [Unintentional Breaking Changes](#unintentional-breaking-changes)
- [Pinning is not a Panacea](#pinning-is-not-a-panacea) - [Pinning is not a Panacea](#pinning-is-not-a-panacea)
- [Zeno's 1.0 and The Eternal Beta](#zeno-s-1-0-and-the-eternal-beta) - [Zeno's 1.0 and The Eternal Beta](#zenos-10-and-the-eternal-beta)
[](TOC) [](TOC)
## Usage ## Overview
### Search and Replace ### Search and Replace
With PyCalVer, you only have to specify one `version_pattern` which is used both to search for version strings as well as to generate the replacement when you do `pycalver bump`. Compare this e.g. to `bumpversion` where you declare separate configurations for `parse` and `serialize`. With PyCalVer, you only configure a single `version_pattern` which is then used
``` 1. Search for version strings in your project files
[bumpversion] 2. Replace these occurrences with an updated/bumped version number.
current_version = 1.alpha
parse = (?P<major>\d+)\.(?P<release>.*)
serialize =
{major}.{release}
{major}
```
A similar version schema with PyCalVer would be: Your configuration might look something like this:
``` ```
[pycalver] [pycalver]
current_version = 1.alpha current_version = "2020.9"
version_pattern = MAJOR.RELEASE version_pattern = "YYYY.MM"
```
Similarly you must specify file specific search and replace strings.
```
[bumpversion:file:requirements.txt]
search = MyProject=={current_version}
replace = MyProject=={new_version}
```
The same with PyCalVer would be:
```
[pycalver:file_patterns] [pycalver:file_patterns]
requirements.txt src/mymodule/__init__.py
MyProject=={version} __version__ = "{version}"
src/mymodule/__main__.py
@click.version_option(version="{version}")
setup.py
version="{version}",
``` ```
The string `{version}` is a placeholder which references whatever you specified in your `version_pattern`. > Throughout the examples, we use the `--date` argument. Without this argument PyCalVer will just use the current date. We use it here so that you can easily reproduce the examples.
You can also be explicit and write the expanded version yourself if you prefer:
Using this configuration, the output of `pycalver bump --dry` might look something like this:
```diff
$ pycalver bump --date 2020-10-01 --dry
INFO - fetching tags from remote (to turn off use: -n / --no-fetch)
INFO - Old Version: 2020.9
INFO - New Version: 2020.10
--- setup.py
+++ setup.py
@@ -63,7 +63,7 @@
setuptools.setup(
name="mymodule",
- version="2020.9",
+ version="2020.10",
description=description,
long_description=long_description,
--- src/mymodule/__init__.py
+++ src/mymodule/__init__.py
@@ -3,3 +3,3 @@
-__version__ = "2020.9"
+__version__ = "2020.10"
--- src/mymodule/__main__.py
+++ src/mymodule/__main__.py
@@ -101,7 +101,7 @@
@click.group()
-@click.version_option(version="2020.9")
+@click.version_option(version="2020.10")
@click.help_option()
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
``` ```
### Related Projects/Alternatives
If PyCalVer does not serve your purposes, you may wish to look at the [bump2version][url_bump2version] project, by which PyCalVer was heavily inspired. You may also wish to take a look at their list of related projects: [bump2version/RELATED.md][url_bump2version_related]
[url_bump2version] https://github.com/c4urself/bump2version/
[url_bump2version_related] https://github.com/c4urself/bump2version/blob/master/RELATED.md
## Example Usage
### Testing a version pattern
You can validate a pattern and how it is incremented using `pycalver test`.
```shell
$ pycalver test --date 2018-09-22 '2018.37' 'YYYY.WW'
New Version: 2018.38
PEP440 : 2018.38
$ pycalver test --date 2018-09-22 '2018.37' 'YYYY.MM' # expected to fail
ERROR - Incomplete match '2018.3' for version string '2018.37' with pattern 'YYYY.MM'/'(?P<year_y>[1-9][0-9]{3})\.(?P<month>1[0-2]|[1-9])'
ERROR - Version did not change: '2018.37'. Invalid version and/or pattern 'YYYY.MM'.
```
This illustrates that each pattern is internally translated to a regular expression which must match your version string. The `--verbose` flag shows a slightly more readable form.
```shell
$ pycalver test --date 2018-09-22 'v2018.37' 'YYYY.WW' --verbose
INFO - Using pattern YYYY.WW
INFO - regex = re.compile(r"""
(?P<year_y>[1-9][0-9]{3})
\.
(?P<week_w>5[0-2]|[1-4][0-9]|[0-9])
""", flags=re.VERBOSE)
ERROR - Invalid version string 'v2018.37' for pattern ...
```
In other words, you don't specify regular expressions manually, they are generated for by PyCalVer based on the parts defined in the [Parts Overview](#parts-overview).
### SemVer: `MAJOR`/`MINOR`/`PATCH`
You can do tradition SemVer without any kind of calendar component if you like.
```shell
$ pycalver test '1.2.3' 'MAJOR.MINOR.PATCH' --patch
New Version: 1.2.4
PEP440 : 1.2.4
$ pycalver test '1.2.3' 'MAJOR.MINOR.PATCH' --minor
New Version: 1.3.0
PEP440 : 1.3.0
$ pycalver test '1.2.3' 'MAJOR.MINOR.PATCH' --major
New Version: 2.0.0
PEP440 : 2.0.0
```
These are the same CLI flags as are accepted by the `pycalver bump` command.
In the context of a CalVer version, a typical use would be to include a `PATCH` part in your version pattern, so that you can create multiple releases in the same month.
```shell
$ pycalver test --date 2018-09-22 '2018.9.0' 'YYYY.MM.PATCH'
ERROR - Invalid arguments or pattern, version did not change.
ERROR - Version did not change: '2018.9.0'. Invalid version and/or pattern 'YYYY.MM.PATCH'.
INFO - Perhaps try: pycalver test --patch
$ pycalver test --date 2018-09-22 '2018.9.0' 'YYYY.MM.PATCH' --patch
New Version: 2018.9.1
PEP440 : 2018.9.1
```
The `PATCH` part will roll over back to zero when leading parts change (in this case the year and month).
```shell
$ pycalver test --date 2018-10-22 '2018.9.1' 'YYYY.MM.PATCH'
New Version: 2018.10.0
PEP440 : 2018.10.0
```
This will happen even if you use the `--patch` argument, so that your first release of the month has a `PATCH` of 0 instead of 1.
```shell
$ pycalver test --date 2018-10-22 '2018.9.1' 'YYYY.MM.PATCH' --patch
New Version: 2018.10.0
PEP440 : 2018.10.0
```
### Auto Incrementing Parts: `BUILD`/`INC0`/`INC1`
The following parts are incremented automatically, and do not use/require a CLI flag: `BUILD`/`INC0`/`INC1`. This means you can just do `pycalver bump` without any further CLI flags and special cases, which can simplify your build scripts.
```shell
$ pycalver test --date 2018-09-22 '2018.9.1' 'YYYY.MM.INC0'
New Version: 2018.9.2
PEP440 : 2018.9.2
$ pycalver test --date 2018-10-22 '2018.9.2' 'YYYY.MM.INC0'
New Version: 2018.10.0
PEP440 : 2018.10.0
$ pycalver test --date 2018-10-22 '2018.9.2' 'YYYY.MM.INC1'
New Version: 2018.10.1
PEP440 : 2018.10.1
```
If it is rare for you to make multiple releases within a given period, you can make such a part optional using the `[PART]` syntax with square braces:
```shell
$ pycalver test --date 2018-09-22 '2018.9' 'YYYY.MM[.INC0]'
New Version: 2018.9.1
PEP440 : 2018.9.1
$ pycalver test --date 2018-10-22 '2018.9.1' 'YYYY.MM[.INC0]'
New Version: 2018.10
PEP440 : 2018.10
```
If the extra `INC0` part is needed, it is added. If the date rolls over and it's no longer needed, it is omitted. Any literal text enclosed in the braces (such as a separator) will also be added or omitted as needed.
### Persistent Parts: `BUILD`/`RELEASE`/`PYTAG`
The `BUILD` and `RELEASE` parts are not reset. Instead they are carried forward.
```shell
$ pycalver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]'
New Version: 201809.1052-beta
PEP440 : 201809.1052b0
$ pycalver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' --release rc
New Version: 201809.1052-rc
PEP440 : 201809.1052rc0
```
To remove a release tag, mark it as final with `--release final`.
```shell
$ pycalver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' --release final
New Version: 201809.1052
PEP440 : 201809.1052
```
### Searching for Patterns with `grep`
Using `pycalver grep`, you can search for occurrences of a version pattern in your project files.
```shell
$ pycalver grep '__version__ = "YYYY.MM[-RELEASENUM]"' src/module/__init__.py
src/module/__init__.py
3:
4: __version__ = "2020.9-beta1"
5:
```
Note that everything in the pattern is treated as literal text, except for a valid part (in all caps).
When you write your configuration, you can avoid repeating your version pattern in every search pattern, by using these placeholders
- `{version}`
- `{pep440_version}`
Applied to the above example, you can instead use this:
```shell
$ pycalver grep --version-pattern "YYYY.MM[-RELEASENUM]" '__version__ = "{version}"' src/module/__init__.py
src/module/__init__.py
3:
4: __version__ = "2020.9-beta1"
5:
```
The corresponding configuration would look like this.
```ini
[pycalver]
current_version = "2020.9-beta1"
version_pattern = "YYYY.MM[-RELEASENUM]"
...
[pycalver:file_patterns] [pycalver:file_patterns]
requirements.txt src/module/__init__.py
MyProject==MAJOR.RELEASE __version__ = "{version}"
...
``` ```
> You may be asking at this point, "what if I want to match `MAJOR.RELEASE` as a literal string?". If your pattern produces non PEP440 version numbers, you may wish to use the placeholder `{pep440_version}` in your search pattern and specify your `--version-pattern` separately.
> Well, tough luck. Realistically speaking, this has not been an issue.
In other words, you don't specify regular expressions manually, they are generated for by PyCalVer based on the parts defined below. Everything except for a valid part (in all caps) is treated as literal text. ```shell
$ pycalver grep --version-pattern "YYYY.MM[-RELEASENUM]" 'version="{pep440_version}"' setup.py
setup.py
65: url="https://github.com/org/project",
66: version="2020.9b1",
67: description=description,
```
### Patterns/Parts The placeholder `{version}` matches `2020.9-beta1`, while the placeholder `{pep440_version}` matches `2020.9b1` (excluding the "v" prefix, the "-" separator and with a short form release tag "b1" instead of "beta1"). These two placeholders make it possible to mostly use your preferred format for version strings, but use a [PEP440][url_pep_440] compliant/normalized version string where appropriate.
> These patterns are closely based on [calver.org][url_calver_org_scheme]. [url_pep_440]: https://www.python.org/dev/peps/pep-0440/
As a further illustration of how the search and replace works, you might want use a file pattern entry to keep the year of your copyright header up to date.
```
$ python -m pycalver grep 'Copyright (c) 2018-YYYY' src/mymodule/*.py | head
src/mymodule/__init__.py
3:
4: # Copyright (c) 2018-2020 Vandelay Industries - All rights reserved.
5:
src/mymodule/config.py
3:
4: # Copyright (c) 2018-2020 Vandelay Industries - All rights reserved.
5:
```
The corresponding configuration for this pattern would look like this.
```ini
[pycalver:file_patterns]
...
src/mymodule/*.py
Copyright (c) 2018-YYYY Vandelay Industries - All rights reserved.
```
## Reference
### Command Line
<!-- BEGIN pycalver --help -->
```
$ pycalver --help
Usage: pycalver [OPTIONS] COMMAND [ARGS]...
Automatically update PyCalVer version strings in all project files.
Options:
--version Show the version and exit.
--help Show this message and exit.
-v, --verbose Control log level. -vv for debug level.
Commands:
bump Increment the current version string and update project files.
grep Search file(s) for a version pattern.
init Initialize [pycalver] configuration.
show Show current version of your project.
test Increment a version number for demo purposes.
```
<!-- END pycalver --help -->
<!-- BEGIN pycalver bump --help -->
```
$ pycalver bump --help
Usage: pycalver bump [OPTIONS]
Increment the current version string and update project files.
Options:
-v, --verbose Control log level. -vv for debug level.
-f, --fetch / -n, --no-fetch Sync tags from remote origin.
-d, --dry Display diff of changes, don't rewrite files.
--release <NAME> Override release name of current_version.
Valid options are: alpha, beta, rc, post,
final.
--allow-dirty 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.
--major Increment major component.
-m, --minor Increment minor component.
-p, --patch Increment patch component.
-r, --release-num Increment release number (rc1, rc2, rc3..).
--pin-date Leave date components unchanged.
--date <ISODATE> Set explicit date in format YYYY-0M-0D (e.g.
2020-10-09).
--help Show this message and exit.
```
<!-- END pycalver bump --help -->
### Part Overview
> Where possible, these patterns match the conventions from [calver.org][url_calver_org_scheme].
[url_calver_org_scheme]: https://calver.org/#scheme [url_calver_org_scheme]: https://calver.org/#scheme
| part | range / example(s) | comment | | part | range / example(s) | comment |
|-----------|---------------------------|--------------------------------------------| |-----------|---------------------------|--------------------------------------------|
| `YYYY` | 2019, 2020... | Full year, based on `strftime('%Y')` | | `YYYY` | 2019, 2020... | Full year, based on `strftime('%Y')` |
| `YY` | 18, 19..99, 1, 2 | Short year, based on `int(strftime('%y'))` | | `YY` | 18, 19..99, 0, 1 | Short year, based on `int(strftime('%y'))` |
| `MM` | 9, 10, 11, 12 | Month, based on `int(strftime('%m'))` | | `MM` | 9, 10, 11, 12 | Month, based on `int(strftime('%m'))` |
| `DD` | 1, 2, 3..31 | Day, based on `int(strftime('%d'))` | | `DD` | 1, 2, 3..31 | Day, based on `int(strftime('%d'))` |
| `MAJOR` | 0..9, 10..99, 100.. | `pycalver bump --major` | | `MAJOR` | 0..9, 10..99, 100.. | `pycalver bump --major` |
@ -175,13 +481,13 @@ In other words, you don't specify regular expressions manually, they are generat
| `INC1` | 1, 2... | 1-based auto incrementing number | | `INC1` | 1, 2... | 1-based auto incrementing number |
The above are the most commonly used. The following are also available, but you should be aware of the [Normalization Caveats](#normalization-caveats) if you want to use them. The following are also available, but you should review the [Normalization Caveats](#normalization-caveats) before you decide to use them.
| part | range / example(s) | comment | | part | range / example(s) | comment |
|--------|---------------------|----------------------------------------------| | ------ | ------------------- | -------------------------------------------- |
| `Q` | 1, 2, 3, 4 | Quarter | | `Q` | 1, 2, 3, 4 | Quarter |
| `0Y` | 18, 19..99, 01, 02 | Short Year `strftime('%y')`(zero-padded) | | `0Y` | 18, 19..99, 00, 01 | Short Year `strftime('%y')`(zero-padded) |
| `0M` | 09, 10, 11, 12 | Month `strftime('%m')` (zero-padded) | | `0M` | 09, 10, 11, 12 | Month `strftime('%m')` (zero-padded) |
| `0D` | 01, 02, 03..31 | Day `strftime('%d')` (zero-padded) | | `0D` | 01, 02, 03..31 | Day `strftime('%d')` (zero-padded) |
| `JJJ` | 1,2,3..366 | Day of year `int(strftime('%j'))` | | `JJJ` | 1,2,3..366 | Day of year `int(strftime('%j'))` |
@ -200,27 +506,104 @@ The above are the most commonly used. The following are also available, but you
- ² Sunday is the first day of the week. - ² Sunday is the first day of the week.
- ³ ISO 8601 week. Week 1 contains Jan 4th. - ³ ISO 8601 week. Week 1 contains Jan 4th.
> On Week Numbering
> ### Normalization Caveats
> Week numbering is a bit special, as it depends on your definition of "week":
> Package managers and installation tools will parse your version numbers. When doing so, your version number may go through a normalization process and may not be displayed as you specified it. In the case of Python, the packaging tools (such as pip, twine, setuptools) follow [PEP440 normalization rules][pep_440_normalzation_ref].
> - Does it start on a Monday or a Sunday?
> - Range from 0-52 or 1-53 ? According to these rules (among other things):
> - At the beginning/end of the year, do you have partial weeks or do
> you have a week that span mutliple years? - Any non-numerical prefix (such as `v`) is removed
> - If a week spans multiple years, what is the year number? - Leading zeros in delimited parts are truncated `XX.08` -> `XX.8`
> - Tags are converted to a short form (`-alpha` -> `a0`)
> If you use `VV`/`0V`, be aware that you cannot also use `YYYY`.
> Instead use `GGGG`. This is to avoid an edge case where your version For example:
> number would run backwards if it was created around New Year.
- Pattern: `vYY.0M.0D[-RELEASE]`
- Version: `v20.08.02-beta`
- PEP440 : `20.8.2b0`
It may not be obvious to everyone that `v20.08.02-beta` is the same `20.8.2b0` on pypi. To avoid this confusion, you should choose a pattern which is always in a normalized form or as close to it as possible.
A further consideration for the choice of your version format is that it may be processed by tools that *do not* interpret it as a version number, but treat it just like any other string. It may also be confusing to your users if they a list of version numbers, sorted lexicographically by some tool (e.g. from `git tags`) and versions are not listed in order of their release as here:
```
$ git tag
18.6b4
18.9b0
19.10b0
19.3b0
20.8b0
20.8b1
```
If you wish to avoid this, you should use a pattern which maintains lexicographical ordering.
### Pattern Examples
<!-- BEGIN pattern_examples -->
| pattern | examples | PEP440 | lexico. |
|---------------------------------|-------------------------------------|--------|---------|
| `MAJOR.MINOR.PATCH[PYTAGNUM]` | `0.13.10 0.16.10rc1` | yes | no |
| `MAJOR.MINOR[.PATCH[PYTAGNUM]]` | `1.11 0.3.0b5` | yes | no |
| `YYYY.BUILD[PYTAGNUM]` | `2020.1031 2020.1148a0` | yes | yes |
| `YYYY.BUILD[-RELEASE]` | `2021.1393-beta 2022.1279` | no | yes |
| `YYYY.INC0[PYTAGNUM]` | `2020.10 2021.12b2` | yes | no |
| `YYYY0M.PATCH[-RELEASE]` | `202005.12 202210.15-beta` | no | no¹ |
| `YYYY0M.BUILD[-RELEASE]` | `202106.1071 202106.1075-beta` | no | yes |
| `YYYY.0M` | `2020.02 2022.09` | no | yes |
| `YYYY.MM` | `2020.8 2020.10` | yes | no |
| `YYYY.WW` | `2020.8 2021.14` | yes | no |
| `YYYY.MM.PATCH[PYTAGNUM]` | `2020.3.12b0 2021.6.19b0` | yes | no |
| `YYYY.0M.PATCH[PYTAGNUM]` | `2020.10.15b0 2022.07.7b0` | no | no¹ |
| `YYYY.MM.INC0` | `2021.6.2 2022.8.9` | yes | no |
| `YYYY.MM.DD` | `2020.5.18 2021.8.2` | yes | no |
| `YYYY.0M.0D` | `2020.08.24 2022.05.03` | no | yes |
| `YY.0M.PATCH` | `21.04.2 21.11.12` | no | no² |
<!-- END pattern_examples -->
- ¹ If `PATCH > 9`
- ² For `2100` YY produces `00`...
### Rollover ### Week Numbering
TODO Week numbering is a bit special, as it depends on your definition of "week":
### Configuration
The fastest way to setup a project is to use `pycalver init`. - Does it start on a Monday or a Sunday?
- Range from 0-52 or 1-53 ?
- At the beginning/end of the year, do you have partial weeks or do you have a week that span multiple years?
- If a week spans multiple years, what is the year number?
If you use `VV`/`0V`, be aware that you cannot also use `YYYY`.
Instead use `GGGG`. This is to avoid an edge case where your version
number would run backwards if it was created around New Year.
<!-- BEGIN weeknum_example -->
```
YYYY WW UU GGGG VV
2020-12-26 (Sat): 2020 51 51 2020 52
2020-12-27 (Sun): 2020 51 52 2020 52
2020-12-28 (Mon): 2020 52 52 2020 53
2020-12-29 (Tue): 2020 52 52 2020 53
2020-12-30 (Wed): 2020 52 52 2020 53
2020-12-31 (Thu): 2020 52 52 2020 53
2021-01-01 (Fri): 2021 00 00 2020 53
2021-01-02 (Sat): 2021 00 00 2020 53
2021-01-03 (Sun): 2021 00 01 2020 53
2021-01-04 (Mon): 2021 01 01 2021 01
```
<!-- END weeknum_example -->
## Configuration
The fastest way to setup the configuration for project is to use `pycalver init`.
```shell ```shell
$ pip install pycalver $ pip install pycalver
@ -231,7 +614,6 @@ Successfully installed pycalver-202010.1041b0
$ cd myproject $ cd myproject
~/myproject/ ~/myproject/
$ pycalver init --dry $ pycalver init --dry
WARNING - File not found: pycalver.toml
Exiting because of '-d/--dry'. Would have written to pycalver.toml: Exiting because of '-d/--dry'. Would have written to pycalver.toml:
[pycalver] [pycalver]
@ -252,22 +634,15 @@ Exiting because of '-d/--dry'. Would have written to pycalver.toml:
] ]
``` ```
If you already have a `setup.cfg` file, the `init` sub-command will If you already have configuration file in your project (such as a `setup.cfg` file), then `pycalver init` will update that file instead.
write to that instead.
``` ```
~/myproject
$ ls
README.md setup.cfg setup.py
~/myproject ~/myproject
$ pycalver init $ pycalver init
WARNING - Couldn't parse setup.cfg: Missing [pycalver] section.
Updated setup.cfg Updated setup.cfg
``` ```
This will add the something like the following to your `setup.cfg` Your `setup.cfg` may now look something like this:
(depending on what files already exist in your project):
```ini ```ini
# setup.cfg # setup.cfg
@ -281,7 +656,7 @@ push = True
[pycalver:file_patterns] [pycalver:file_patterns]
setup.cfg = setup.cfg =
current_version = {version} current_version = "{version}"
setup.py = setup.py =
"{version}", "{version}",
"{pep440_version}", "{pep440_version}",
@ -290,10 +665,63 @@ README.md =
{pep440_version} {pep440_version}
``` ```
This probably won't cover every version number used in your project and you For the entries in `[pycalver:file_patterns]` you can expect two failure modes:
will have to manually add entries to `pycalver:file_patterns`. Something
like the following may illustrate additional changes you might need to - A pattern won't match a version number in the associated file.
make. - A pattern will match something it shouldn't (less likely).
To debug such issues, you can use `pycalver grep` .
```
$ pycalver grep 'Copyright (c) 2018-YYYY' src/module/*.py
src/module/__init__.py
3: #
4: # Copyright (c) 2018-2020 Vandelay Industries - All rights reserved.
5: # SPDX-License-Identifier: MIT
src/module/config.py
3: #
4: # Copyright (c) 2018-2020 Vandelay Industries - All rights reserved.
5: # SPDX-License-Identifier: MIT
```
Of course, you may not get the pattern correct right away. If your pattern is not found, `pycalver grep` will show an error message with the regular expression it uses, to help you debug the issue.
```
$ pycalver grep 'Copyright 2018-YYYY' src/pycalver/*.py
ERROR - Pattern not found: 'Copyright 2018-YYYY'
# https://regex101.com/?flavor=python&flags=gmx&regex=Copyright%5B%20%5D2018%5C-%0A%28%3FP%3Cyear_y%3E%5B1-9%5D%5B0-9%5D%7B3%7D%29
re.compile(r"""
Copyright[ ]2018\-
(?P<year_y>[1-9][0-9]{3})
""", flags=re.VERBOSE)
```
Let's say you want to keep a badge your README.md up to date.
```
$ pycalver grep --version-pattern='vYYYY0M.BUILD[-RELEASE]' 'img.shields.io/static/v1.svg?label=PyCalVer&message={version}&color=blue' README.md
61:
62: [img_version]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1040-beta&color=blue
63: [url_version]: https://pypi.org/org/package/
Found 1 match for pattern 'img.shields.io/static/v1.svg?label=PyCalVer&message=vYYYY0M.BUILD[-RELEASE]&color=blue' in README.md
```
This probably won't cover all version numbers present in your project, so you will have to manually add entries to `pycalver:file_patterns`. To determine what to add, you can use `pycalver grep` :
```
$ pycalver grep 'Copyright (c) 2018-YYYY' src/project/*.py
```
Something like the following may illustrate additional changes you might need to make.
```ini ```ini
[pycalver:file_patterns] [pycalver:file_patterns]
@ -349,11 +777,12 @@ INFO - New Version: v201902.1002-beta
If there is no match for a pattern, bump will report an error. If there is no match for a pattern, bump will report an error.
```shell ```shell
# TODO (mb 2020-08-29): update regex pattern
$ pycalver bump --dry --no-fetch $ pycalver bump --dry --no-fetch
INFO - Old Version: v201901.1001-beta INFO - Old Version: v201901.1001-beta
INFO - New Version: v201902.1002-beta INFO - New Version: v201902.1002-beta
ERROR - No match for pattern 'img.shields.io/static/v1.svg?label=PyCalVer&message={pycalver}&color=blue' ERROR - No match for pattern 'img.shields.io/static/v1.svg?label=CalVer&message={version}&color=blue'
ERROR - Pattern compiles to regex 'img\.shields\.io/static/v1\.svg\?label=PyCalVer&message=(?P<pycalver>v(?P<year>\d{4})(?P<month>(?:0[0-9]|1[0-2]))\.(?P<bid>\d{4,})(?:-(?P ERROR - Pattern compiles to regex 'img\.shields\.io/static/v1\.svg\?label=CalVer&message=(?P<year_y>\d{4})(?P<month>(?:0[0-9]|1[0-2]))\.(?P<bid>\d{4,})(?:-(?P
<tag>(?:alpha|beta|dev|rc|post|final)))?)&color=blue' <tag>(?:alpha|beta|dev|rc|post|final)))?)&color=blue'
``` ```
@ -397,6 +826,8 @@ Available placeholders are:
### Pattern Usage ### Pattern Usage
<!-- TODO (mb 2020-09-24): UPDATE USAGE -->
There are some limitations to keep in mind: There are some limitations to keep in mind:
1. A version string cannot span multiple lines. 1. A version string cannot span multiple lines.
@ -433,61 +864,7 @@ When what you probably wanted was this (with the `--final` tag omitted):
https://img.shields.io/badge/myproject-v202010.1117-blue.svg https://img.shields.io/badge/myproject-v202010.1117-blue.svg
``` ```
### Examples
The easiest way to test a pattern is with the `pycalver test` sub-command.
```shell
$ pycalver test 'v18w01' 'vYYw0W'
New Version: v19w06
PEP440 : v19w06
# TODO (mb 2020-09-24): Update regexp pattern
$ pycalver test 'v18.01' 'vYYw0W'
ERROR - Invalid version string 'v18.01' for pattern
'vYYw0W'/'v(?P<YY>\d{2})w(?P<0W>(?:[0-4]\d|5[0-2]))'
ERROR - Invalid version 'v18.01' and/or pattern 'vYYw0W'.
```
As you can see, each pattern is internally translated to a regular expression.
All version strings in your project must match either this regular expression or
the corresponding regular expression for the PEP440 version string.
The `pycalver test` sub-command accepts the same cli flags as `pycalver
bump` to update the components that are not updated automatically (eg.
based on the calendar).
```shell
$ pycalver test 'v18.1.1' 'vYY.MINOR.PATCH'
New Version: v19.1.1
PEP440 : 19.1.1
$ pycalver test 'v18.1.1' 'vYY.MINOR.PATCH' --patch
New Version: v19.1.2
PEP440 : 19.1.2
$ pycalver test 'v18.1.2' 'vYY.MINOR.PATCH' --minor
New Version: v19.2.0
PEP440 : 19.2.0
$ pycalver test 'v201811.1051-beta' 'vYYYYMM.BUILD[-RELEASE]'
New Version: v201902.1052-beta
PEP440 : 201902.1052b0
$ pycalver test 'v201811.0051-beta' 'vYYYYMM.BUILD[-RELEASE]' --release rc
New Version: v201902.1052-rc
PEP440 : 201902.1052rc0
$ pycalver test 'v201811.0051-beta' 'vYYYYMM.BUILD[-RELEASE]' --release final
New Version: v201902.1052
PEP440 : 201902.1052
```
Note that pypi/setuptools/pip will normalize version strings to a format
defined in [PEP440][pep_440_ref]. You can use a format that deviates from
this, just be aware that version strings processed by these tools will look
different.
### Version State ### Version State
@ -623,89 +1000,20 @@ INFO - git push origin v202010.1006-beta
### Config Parameters ### Config Parameters
TODO: Descriptions <!-- TODO (mb 2020-09-24): descriptions -->
| Config Parameter | Type | Description | | Config Parameter | Type | Description |
|-------------------|---------|------------------------------| | ----------------- | -------- | ---------------------------- |
| `current_version` | string | | | `current_version` | string | |
| `version_pattern` | string | | | `version_pattern` | string | |
| `commit_message` | string | ¹Template fro commit message | | `commit_message` | string | Template for commit message¹ |
| `commit` | boolean | | | `commit` | boolean | |
| `tag` | boolean | | | `tag` | boolean² | |
| `push` | boolean | | | `push` | boolean² | |
- ¹ Available placeholders:
- `{new_version}`
- `{old_version}`
- `{new_version_pep440}`
- `{old_version_pep440}`
### CLI Reference
<!-- BEGIN pycalver --help -->
```
$ pycalver --help
Usage: pycalver [OPTIONS] COMMAND [ARGS]...
Automatically update PyCalVer version strings on python projects.
Options:
--version Show the version and exit.
--help Show this message and exit.
-v, --verbose Control log level. -vv for debug level.
Commands:
bump Increment the current version string and update project files.
grep Search file(s) for a version pattern.
init Initialize [pycalver] configuration.
show Show current version of your project.
test Increment a version number for demo purposes.
```
<!-- END pycalver --help -->
<!-- BEGIN pycalver bump --help -->
```
$ pycalver bump --help
Usage: pycalver bump [OPTIONS]
Increment the current version string and update project files.
Options:
-v, --verbose Control log level. -vv for debug level.
-f, --fetch / -n, --no-fetch Sync tags from remote origin.
-d, --dry Display diff of changes, don't rewrite files.
--release <name> Override release name of current_version.
Valid options are: alpha, beta, rc, post,
final.
--allow-dirty 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.
--major Increment major component.
-m, --minor Increment minor component.
-p, --patch Increment patch component.
-r, --release-num Increment release number.
--pin-date Leave date components unchanged.
--date <iso-date> Set explicit date in format YYYY-0M-0D (e.g.
2020-10-04).
--help Show this message and exit.
```
<!-- END pycalver bump --help -->
### Related Projects/Alternatives
The bump2version project maintains a good list of alternative and related projects: [bump2version/RELATED.md][url_bump2version_related]
[url_bump2version_related] https://github.com/c4urself/bump2version/blob/master/RELATED.md
- ¹ Available placeholders: `{new_version}`, `{old_version}`, `{new_version_pep440}`, `{old_version_pep440}`
- ² Requires `commit = True`
## The PyCalVer Format ## The PyCalVer Format
@ -767,7 +1075,7 @@ import re
PYCALVER_PATTERN = r""" PYCALVER_PATTERN = r"""
\b \b
(?P<pycalver> (?P<pycalver>
(?P<vYYYYMM> (?P<vYYYY0M>
v # "v" version prefix v # "v" version prefix
(?P<year>\d{4}) (?P<year>\d{4})
(?P<month>\d{2}) (?P<month>\d{2})
@ -789,7 +1097,7 @@ version_match = PYCALVER_REGEX.match(version_str)
assert version_match.groupdict() == { assert version_match.groupdict() == {
"pycalver" : "v201712.0001-alpha", "pycalver" : "v201712.0001-alpha",
"vYYYYMM" : "v201712", "vYYYY0M" : "v201712",
"year" : "2017", "year" : "2017",
"month" : "12", "month" : "12",
"build" : ".0001", "build" : ".0001",
@ -803,7 +1111,7 @@ version_match = PYCALVER_REGEX.match(version_str)
assert version_match.groupdict() == { assert version_match.groupdict() == {
"pycalver" : "v201712.0033", "pycalver" : "v201712.0033",
"vYYYYMM" : "v201712", "vYYYY0M" : "v201712",
"year" : "2017", "year" : "2017",
"month" : "12", "month" : "12",
"build" : ".0033", "build" : ".0033",
@ -851,15 +1159,21 @@ This means that the expression `older_id < newer_id` will always be true, whethe
## Semantics of PyCalVer ## Semantics of PyCalVer
> Disclaimer: This section is of course only aspirational. Nothing will
> stop a package maintainer from publishing updates that violate the
> semantics presented here.
This sorting even works correctly in JavaScript! ### Pitch
- dates are good information
- how old is the software
- is the software maintained
- is my dependency outdated
- can I trust an update?
### blah
> Disclaimer: This section can of course only be aspirational. There is nothing
> to prevent package maintainers from publishing packages with different
> semantics than what is presented here.
PyCalVer places a greater burden on package maintainers than SemVer. PyCalVer places a greater burden on package maintainers than SemVer.
Backward incompatibility is not encoded in the version string, because Backward incompatibility is not encoded in the version string, because

View file

@ -46,3 +46,6 @@ graphviz
# run failed tests first # run failed tests first
pytest-cache pytest-cache
# to update the readme examples
rich

View file

@ -0,0 +1,216 @@
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 pycalver 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[-RELEASE]" , ""),
("YYYY.INC0[PYTAGNUM]" , ""),
("YYYY0M.PATCH[-RELEASE]" , "¹"),
("YYYY0M.BUILD[-RELEASE]" , ""),
("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, "pycalver --help")
new_content = update_md_code_output(new_content, "pycalver bump --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)
# @printf '\n```\n$$ pycalver --help\n' > /tmp/pycalver_help.txt
# @$(DEV_ENV)/bin/pycalver --help >> /tmp/pycalver_help.txt
# @printf '```\n\n' >> /tmp/pycalver_help.txt
# sed -i -ne '/<!-- BEGIN pycalver --help -->/ {p; r /tmp/pycalver_help.txt' \
# -e ':a; n; /<!-- END pycalver --help -->/ {p; b}; ba}; p' \
# README.md
# @printf '\n```\n$$ pycalver bump --help\n' > /tmp/pycalver_help.txt
# @$(DEV_ENV)/bin/pycalver bump --help >> /tmp/pycalver_help.txt
# @printf '```\n\n' >> /tmp/pycalver_help.txt
# sed -i -ne '/<!-- BEGIN pycalver bump --help -->/ {p; r /tmp/pycalver_help.txt' \
# -e ':a; n; /<!-- END pycalver bump --help -->/ {p; b}; ba}; p' \
# README.md