mirror of
https://github.com/TECHNOFAB11/bumpver.git
synced 2025-12-12 22:40:09 +01:00
688 lines
20 KiB
Markdown
688 lines
20 KiB
Markdown
# PyCalVer: Automatic CalVer Versioning for Python Packages
|
|
|
|
PyCalVer is a simple versioning system. It version strings
|
|
everywhere in your project, it generates git/mercurial tags and
|
|
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/).
|
|
|
|
[![Build Status][build_img]][build_ref]
|
|
[![Code Coverage][codecov_img]][codecov_ref]
|
|
[![MIT License][license_img]][license_ref]
|
|
[![Code Style: sjfmt][style_img]][style_ref]
|
|
[![Type Checked with mypy][mypy_img]][mypy_ref]
|
|
[![PyCalVer v201809.0002-beta][version_img]][version_ref]
|
|
[![PyPI Version][pypi_img]][pypi_ref]
|
|
[![PyPI Downloads][downloads_img]][downloads_ref]
|
|
[![PyPI Wheel][wheel_img]][wheel_ref]
|
|
[![Supported Python Versions][pyversions_img]][pyversions_ref]
|
|
|
|
|
|
| Name | role | since | until |
|
|
|----------------------------|------------|---------|-------|
|
|
| Manuel Barkhau (@mbarkhau) | maintainer | 2018-09 | - |
|
|
|
|
|
|
<!--
|
|
$ pip install md-toc
|
|
$ md_toc -i gitlab README.md
|
|
-->
|
|
|
|
[](TOC)
|
|
|
|
- [Introduction](#introduction)
|
|
- [Format](#format)
|
|
- [Versioning Behaviour](#versioning-behaviour)
|
|
- [Lexical Ids](#lexical-ids)
|
|
- [Usage](#usage)
|
|
- [Configuration](#configuration)
|
|
- [Bump It Up](#bump-it-up)
|
|
- [Pattern Search and Replacement](#pattern-search-and-replacement)
|
|
- [Rational](#rational)
|
|
- [Other Versioning Software](#other-versioning-software)
|
|
- [Some Details](#some-details)
|
|
- [Realities of Version Numbers](#realities-of-version-numbers)
|
|
- [Should I use PyCalVer for my Project?](#should-i-use-pycalver-for-my-project)
|
|
- [Marketing/Vanity](#marketing-vanity)
|
|
- [Rational](#rational-1)
|
|
- [Breaking Things is a Big Deal](#breaking-things-is-a-big-deal)
|
|
- [A Word on Marketing](#a-word-on-marketing)
|
|
- [Commitment to Compatibility](#commitment-to-compatibility)
|
|
- [The Life of a Library](#the-life-of-a-library)
|
|
- [FAQ](#faq)
|
|
|
|
[](TOC)
|
|
|
|
|
|
## Introduction
|
|
|
|
The PyCalVer package provides the `pycalver` command and module
|
|
to generate version strings. These use the following format:
|
|
`v{calendar_version}.{build_number}[-{release_tag}]`
|
|
|
|
Some examples:
|
|
|
|
```
|
|
v201711.0001-alpha
|
|
v201712.0027-beta
|
|
v201801.0031
|
|
v201801.0032-post
|
|
...
|
|
v202207.18133
|
|
v202207.18134
|
|
```
|
|
|
|
The `pycalver bump` command parses your files for such strings
|
|
and rewrites them with an incremented version string.
|
|
|
|
### Format
|
|
|
|
The format for PyCalVer version strings can be parsed with this
|
|
regular expression:
|
|
|
|
```python
|
|
import re
|
|
|
|
# https://regex101.com/r/fnj60p/10
|
|
PYCALVER_PATTERN = r"""
|
|
\b
|
|
(?P<version>
|
|
(?P<calver>
|
|
v # "v" version prefix
|
|
(?P<year>\d{4})
|
|
(?P<month>\d{2})
|
|
)
|
|
(?P<build>
|
|
\. # "." build nr prefix
|
|
\d{4,}
|
|
)
|
|
(?P<release>
|
|
\- # "-" release prefix
|
|
(?:alpha|beta|dev|rc|post)
|
|
)?
|
|
)(?:\s|$)
|
|
"""
|
|
PYCALVER_RE = re.compile(PYCALVER_PATTERN, 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,
|
|
}
|
|
```
|
|
|
|
### Versioning Behaviour
|
|
|
|
To illustrate how PyCalVer increments version strings, we can use
|
|
`pycalver incr`:
|
|
|
|
```shell
|
|
$ pip install pycalver
|
|
...
|
|
$ pycalver incr v201801.0033-beta
|
|
PyCalVer Version: v201809.0034-beta
|
|
PEP440 Version: 201809.34b0
|
|
```
|
|
|
|
This is the simple case:
|
|
|
|
- The calendar component is update to the current year and month.
|
|
- The build number is incremented by 1.
|
|
- The optional release tag is preserved as is.
|
|
|
|
Here is how to explicitly update the release tag:
|
|
|
|
```shell
|
|
$ pycalver incr v201801.0033-beta --release=alpha
|
|
PyCalVer Version: v201809.0034-alpha
|
|
PEP440 Version: 201809.34a0
|
|
$ pycalver incr v201801.0033-beta --release=final
|
|
PyCalVer Version: v201809.0034
|
|
PEP440 Version: 201809.34
|
|
```
|
|
|
|
The version number is padded with extra zeros, to maintain the
|
|
lexical ordering of version numbers. What happens when the
|
|
padding is exhausted?
|
|
|
|
```shell
|
|
$ pycalver incr v201809.0999
|
|
PyCalVer Version: v201809.11000
|
|
PEP440 Version: 201809.11000
|
|
```
|
|
|
|
This is because the build number is generated as a sequence of
|
|
lexical ids.
|
|
|
|
|
|
### Lexical Ids
|
|
|
|
The padded build number will occasionally have to be expanded. In
|
|
order to preserve both lexical ordering as well numerical
|
|
ordering, build numbers are incremented in a peculiar way.
|
|
Examples will perhaps illustrate more clearly.
|
|
|
|
```python
|
|
"0001"
|
|
"0002"
|
|
"0003"
|
|
...
|
|
"0999"
|
|
"11000"
|
|
"11001"
|
|
...
|
|
"19998"
|
|
"19999"
|
|
"220000"
|
|
"220001"
|
|
```
|
|
|
|
What is happening here is that the left-most digit is incremented
|
|
early/preemptively. Whenever the left-most digit would change,
|
|
the width of the id is expanded using this simple formula:
|
|
|
|
```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
|
|
```
|
|
|
|
This behaviour ensures that the following semantic is always
|
|
preserved: `old_version < new_version`. This will even be the
|
|
case if the version number was incremented twice in the same
|
|
month.
|
|
|
|
|
|
## Usage
|
|
|
|
### Configuration
|
|
|
|
The fastest way to setup a project is to invoke
|
|
`pycalver init`.
|
|
|
|
|
|
```shell
|
|
$ cd my-project
|
|
~/my-project$ pycalver init
|
|
Updated setup.cfg
|
|
```
|
|
|
|
This will add the something like the following to your
|
|
`setup.cfg`:
|
|
|
|
```ini
|
|
[pycalver]
|
|
commit = True
|
|
tag = True
|
|
|
|
[pycalver:file:setup.cfg]
|
|
patterns =
|
|
current_version = {version}
|
|
|
|
[pycalver:file:setup.py]
|
|
patterns =
|
|
"{version}",
|
|
"{pep440_version}",
|
|
|
|
[pycalver:file:README.md]
|
|
patterns =
|
|
{version}
|
|
{pep440_version}
|
|
```
|
|
|
|
This may or may not cover all version numbers across your
|
|
repository. Something like the following may illustrate
|
|
additional changes you might need to make.
|
|
|
|
```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.md]
|
|
patterns =
|
|
[PyCalVer {calver}{build}-{release}]
|
|
img.shields.io/badge/PyCalVer-{calver}{build}--{release}-blue
|
|
```
|
|
|
|
You can omit `patterns` if the default patterns are sufficient.
|
|
These are:
|
|
|
|
```ini
|
|
patterns =
|
|
{version}
|
|
{pep440_version}
|
|
```
|
|
|
|
This allows for a shorter (albeit less explicit) configuration:
|
|
|
|
```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.md]
|
|
patterns =
|
|
[PyCalVer {calver}{build}-{release}]
|
|
img.shields.io/badge/PyCalVer-{calver}{build}--{release}-blue
|
|
```
|
|
|
|
### Bump It Up
|
|
|
|
To increment and publish a new version, you can use the
|
|
`pycalver bump` command:
|
|
|
|
```shell
|
|
$ pycalver bump
|
|
```
|
|
|
|
This will do a few things
|
|
|
|
0. Check that you don't have any non-committed local changes.
|
|
1. Fetch the most recent global vcs tags from origin.
|
|
2. Generate a new version, incremented from on the most recent
|
|
tag on any branch.
|
|
3. Update version strings in all configured files.
|
|
4. Commit the updated version strings.
|
|
5. Tag the new commit.
|
|
6. Push the new commit to origin.
|
|
|
|
The current version is defined either as
|
|
|
|
- The lexically largest git/mercurial tag in the repository.
|
|
- The value of `pycalver.current_version` in setup.cfg
|
|
|
|
The git/mercurial tags are used to minimize the chance that the
|
|
same version will be generated for different revisions. As part
|
|
of doing `pycalver bump`, your local repository is updated
|
|
using `git fetch --tags`/`hg pull`, to ensure that all tags are
|
|
known locally
|
|
git push --follow-tags
|
|
|
|
the version To
|
|
avoid this completely you n
|
|
The value in setup.cfg is only used on projects that don't use
|
|
revision control.
|
|
If your project does not use git or mercurial for version
|
|
control, then the current_version will be set by `pycalver init`.
|
|
|
|
|
|
```shell
|
|
$ pycalver show
|
|
Current Version: v201809.0001-beta
|
|
PEP440 Version: 201809.1b0
|
|
|
|
$ pycalver bump --dry
|
|
TODO: diff output
|
|
Don't forget to do $ git push --tags
|
|
```
|
|
|
|
TODO: commits and tags
|
|
|
|
|
|
### Pattern Search and Replacement
|
|
|
|
`patterns` is used both to search for version strings and to
|
|
generate the replacement strings. The following placeholders are
|
|
available for use, everything else in a pattern is treated as
|
|
literal text.
|
|
|
|
|
|
| placeholder | example |
|
|
|------------------|--------------------|
|
|
| `pep440_version` | 201809.1b0 |
|
|
| `version` | v201809.0001-alpha |
|
|
| `calver` | v201809 |
|
|
| `year` | 2018 |
|
|
| `month` | 09 |
|
|
| `build` | .0001 |
|
|
| `release` | -alpha |
|
|
|
|
|
|
Note that the separator/prefix characters are part of what is
|
|
matched and generated for a given placeholder, and they should
|
|
not be included in your patterns.
|
|
|
|
A further restriction is, that a version string cannot span
|
|
multiple lines in your source file.
|
|
|
|
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.
|
|
|
|
|
|
## Rational
|
|
|
|
### Other Versioning Software
|
|
|
|
This project is very similar to
|
|
[bumpversion](https://github.com/peritus/bumpversion), upon which
|
|
it is partially based. So why another library?
|
|
PyCalVer version strings can be
|
|
generated automatically, usage is a bit more simple.
|
|
|
|
|
|
### 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.
|
|
- Lexical 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
|
|
```
|
|
|
|
|
|
|
|
### Realities of Version 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 opinion match yours, but makes it useless for
|
|
everybody else.
|
|
|
|
The less 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
|
|
compatibility issues generated by your project can be a heavy
|
|
burden on thousands of users; users who learn of the existence
|
|
of your library for the first time in the form of a stack-trace.
|
|
PyCalVer is for projects that are committed to and can maintain
|
|
backward compatibility. 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 tenable. 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 compatibility, the better. SemVer can be overly specific
|
|
for some kinds of projects. If you are writing a library
|
|
and you have a commitment to backward compatibility
|
|
|
|
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 Compatibility
|
|
|
|
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 compatibility 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 bandwidth 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
|
|
temporarily 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 lexically 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 library. 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 software 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
|
|
|
|
|
|
|
|
[build_img]: https://gitlab.com/mbarkhau/pycalver/badges/master/pipeline.svg
|
|
[build_ref]: https://gitlab.com/mbarkhau/pycalver/pipelines
|
|
|
|
[codecov_img]: https://gitlab.com/mbarkhau/pycalver/badges/master/coverage.svg
|
|
[codecov_ref]: https://mbarkhau.gitlab.io/pycalver/cov
|
|
|
|
[license_img]: https://img.shields.io/badge/License-MIT-blue.svg
|
|
[license_ref]: https://gitlab.com/mbarkhau/pycalver/blob/master/LICENSE
|
|
|
|
[mypy_img]: https://img.shields.io/badge/mypy-100%25-green.svg
|
|
[mypy_ref]: http://mypy-lang.org/
|
|
|
|
[style_img]: https://img.shields.io/badge/code%20style-%20sjfmt-f71.svg
|
|
[style_ref]: https://gitlab.com/mbarkhau/straitjacket/
|
|
|
|
[pypi_img]: https://img.shields.io/pypi/v/pycalver.svg
|
|
[pypi_ref]: https://gitlab.com/mbarkhau/pycalver/blob/master/CHANGELOG.rst
|
|
|
|
[downloads_img]: https://pepy.tech/badge/pycalver
|
|
[downloads_ref]: https://pepy.tech/project/pycalver
|
|
|
|
[version_img]: https://img.shields.io/badge/PyCalVer-v201809.0002--beta-blue.svg
|
|
[version_ref]: https://pypi.org/project/pycalver/
|
|
|
|
[wheel_img]: https://img.shields.io/pypi/wheel/pycalver.svg
|
|
[wheel_ref]: https://pypi.org/project/pycalver/#files
|
|
|
|
[pyversions_img]: https://img.shields.io/pypi/pyversions/pycalver.svg
|
|
[pyversions_ref]: https://pypi.python.org/pypi/pycalver
|
|
|