diff --git a/README.md b/README.md index 170b03a..53f5867 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,10 @@ # [PyCalVer: Automatic CalVer Versioning for Python Packages][repo_ref] -PyCalVer is a simple calendar based versioning system. With a single -`pycalver bump` command it will: - - - Automatically update version strings across files in your project. - - Commit those changes and tag the commit with the new version. - -Version strings generated by pycalver are 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/). +PyCalVer is for projects that only have one semantic: newer == +better. PyCalVer version strings are compatible with python +packaging software [setuptools][setuptools_ref] and +[PEP440][pep_440_ref], but can in principle be used with any +project. Project/Repo: @@ -43,25 +38,17 @@ Code Quality/CI: [](TOC) - [Introduction](#introduction) - - [Format](#format) - - [Versioning Behaviour](#versioning-behaviour) - - [Lexical Ids](#lexical-ids) + - [Semantics of PyCalVer](#semantics-of-pycalver) + - [Breaking Changes](#breaking-changes) + - [Zeno's 1.0 and the Eternal Beta](#zenos-10-and-the-eternal-beta) + - [Version String Format](#version-string-format) + - [Incrementing Behaviour](#incrementing-behaviour) - [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) + - [Bump It Up](#bump-it-up) + - [Version State](#version-state) + - [Lexical Ids](#lexical-ids) [](TOC) @@ -95,6 +82,197 @@ v202207.18133 v202207.18134 ``` +PyCalVer is inspired by: + + - ["Speculation" talk by Rich + Hicky](https://www.youtube.com/watch?v=oyLBGkS5ICk) + - [Designing a Version by Mahmoud + Hashemi](http://sedimental.org/designing_a_version.html) + - [calver.org](https://calver.org/) + - ["The cargo cult of versioning" by Kartik + Agaram](http://akkartik.name/post/versioning) + - The [bumpversion][bumpversion_ref] project, upon which + PyCalVer is partially based. + +If you are familiar with these, feel free to skip ahead to +[Usage](#usage) + + +## Semantics of PyCalVer + +> Disclaimer: This section is aspirational and of course there +> is nothing to prevent package maintainers from publishing +> packages with different semantics than what is laid out here. + +PyCalVer places a greater burden on package maintainers than +SemVer. Backward incompatibility is not encoded, because +maintainers should not make any breaking changes. This is great +for users of a package, who can worry a bit less about an update +breaking their project. If they're paranoid, they can of course +still pin to known good versions, but ideally they don't need +version specifier in their requirements.txt so they always get +the latest bug fixes and features. Ideally users can trust the +promise of a maintainer that the following semantics will always +be true: + + - Newer is compatible. + - Newer has fewer bugs. + - Newer has more features. + - Newer has similar or better performance. + +The world is not ideal of course, so how do users and +maintainers deal with changes that might violate these promises? + + +### Breaking Changes + +> Namespaces are one honking great idea +> -- let's do more of those! + +If you must make a breaking change to a package, *instead of +incrementing a number*, the recommended approach with PyCalVer +is to *create a whole new package*. Put differently, the major +version becomes part of the package/module namespace. Typically +you might add a numerical suffix, eg. `mypkg -> mypkg2`. + +The other kind of breaking change is the non-intentional kind, +otherwise known as a bug. Realize first of all, that it is +impossible for any versioning system to encode that this has +happened: Since the maintainer isn't knowingly introducing a bug +they naturally won't set a version to reflect something they +don't know about. Instead we have to deal with this issue after +the fact. + +The first thing a package maintainer can do is to minimize the +chance of inflicting buggy software on users. After any +non-trivial (maybe unsafe) change, create a new +`-alpha`/`-beta`/`-rc` release. These so called `--pre` releases +are downloaded only by the few and the brave, who are willing to +participate in testing. After any any issues are ironed with the +`--pre` releases, a `final` release can be made for more +regular/conservative users. + +Note that the default behaviour of `pip install ` +(without any version specifier) is to download the latest +`final` release. It will download a `--pre` release *only* if + + 1. there is no `final` release available yet + 2. the `--pre` flag is explicitly used, or + 3. if the requirement specifier explicitly includes the version + number of a pre release, eg. `pip install + mypkg==v201812.0007-alpha`. + +Should a release include a bug (heaven forbid and despite all +precautions), then the maintainer should publish a new release +which either fixes the or reverts the change. It is important +that the new release have a greater version than the release +that contained the issue and that it use the same release +suffix. If users downloaded a version of the package which +included the bug, they only have to do `pip install --upgrade +` and the issue will be resolved. + +Perhaps a timeline will illustrate more clearly: + +``` +v201812.0665 # last stable release +v201812.0666-beta # pre release for testers +v201901.0667 # final release after testing + +# bug is discovered which effects v201812.0666-beta and v201901.0667 + +v201901.0668-beta # fix is issued for testers +v201901.0669 # fix is issued everybody + +# Alternatively, revert before fixing + +v201901.0668 # identical code to v201812.0665 +v201901.0669-beta # reintroduce change from v201812.0666-beta + fix +v201901.0670 # final release after testing +``` + +In the absolute worst case, a change is discovered to break +backward compatibility, but the change is still considered to be +desirable. At that point, a new release should be made to revert +the change. This way: + + - users who were not exposed to the breaking change will + download the the newest release with the reverted changes. + - users who were exposed to the breaking change can update to + the latest release and get the old working code again. + +Remember that the goal is to always make things easy for users +who depend on a package. If there is any issue whatsoever, all +they should have to do is `pip install --update`. If this +doesn't work, they may have to temporarily pin to a known good +version of a dependency, at least until a fixed release is +uploaded. + +After this immediate fire has been put out, if the maintainer +considers the breaking change worth keeping, they can **create a +new package**, with a new namespace. This package will perhaps +have 99% overlap to the previous one and the old one may +eventually be abandoned. + +``` +mypkg v201812.0665 # last stable release +mypkg v201812.0666-rc # pre release for testers +mypkg v201901.0667 # final release after testing period + +# bug is discovered in v201812.0666-beta and v201901.0667 + +mypkg v201901.0668 # identical code to v201812.0665 + +# new package is created with compatibility breaking code + +mypkg2 v201901.0669 # identical code to v201901.0667 +mypkg v201901.0669 # updated readme, declaring support + # level for mypkg, pointing to mypgk2 + # and documenting how to migrate. +``` + +If this seems like overkill, consider investing time to minimize +the overhead of creating new packages. Consider also that your +projects may recursively depend on dozens of libraries which +you've never even heard of. If every maintainer introduced +breaking changes only once per year, users who depend on these +libraries would be dealing with packaging issues every month! In +other words: *Breaking things is a big deal*. A bit of extra +effort for a few maintainers seems like a fair trade compared to +the effort of many users who would be perfectly happy to use the +old code until they can find the time to migrate. + +When creating a new package, it may be worthwhile to rename not +just the package, but also its python module(s). The benefit of +this is that users can install both packages in the same +environment and import both old and new modules. The downside is +that users have to change their code, even if the breaking +change did not affect them. + + +### Zeno's 1.0 and the Eternal Beta + +With PyCalVer, the release tag (`-alpha`, `-beta`, `-rc`) says +something about the stability of a *particular release*. This is +similar ([perhaps identical][pep_101_ref]) to the meaning of +release tags used by the CPython interpreter. A release tag is +not a statement general stability of the software as a whole, it +is metadata about a particular release artifact of a package, +eg. a `.whl` file. + +There is a temptation for maintainers to avoid any commitment to +backward compatibility by forever staying in beta or in the case +of SemVer, by never incriminating the major version, leading to +the [Zeno 1.0 paradox][zeno_1_dot_0_ref]. Of course an unpaid +Open Source developer *does not owe anybody a commitment to +backward compatibility*. Especially when a project is young and +going through major changes, such a commitment may not make any +sense. For these cases you can still use PyCalVer, just so long +as there is a big fat warning at the top of your README. Another +way to signify that a project is still in early development is +to not publish a `final` release until the codebase has become +stable. + + ### Version String Format The format for PyCalVer version strings can be parsed with this @@ -149,22 +327,26 @@ assert version_info == { } ``` -### Versioning Behaviour +### Incrementing Behaviour To see how version strings are incremented, we can use -`pycalver incr`: +`pycalver test`: ```shell $ pip install pycalver ... -$ pycalver incr v201801.0033-beta +Successfully installed pycalver-201812.12 +$ pycavler --version +pycalver, version v201812.0012 +$ pycalver test v201801.0033-beta PyCalVer Version: v201809.0034-beta -PEP440 Version: 201809.34b0 +PEP440 Version : 201809.34b0 ``` This is the simple case: - - The calendar component is update to the current year and month. + - The calendar component is updated to the current year and + month. - The build number is incremented by 1. - The optional release tag is preserved as is. @@ -172,88 +354,17 @@ You can explicitly update the release tag using the `--release=` argument: ```shell -$ pycalver incr v201801.0033-alpha --release=beta +$ pycalver test v201801.0033-alpha --release=beta PyCalVer Version: v201809.0034-beta -PEP440 Version: 201809.34b0 -$ pycalver incr v201809.0034-beta --release=final +PEP440 Version : 201809.34b0 +$ pycalver test v201809.0034-beta --release=final PyCalVer Version: v201809.0035 -PEP440 Version: 201809.35 +PEP440 Version : 201809.35 ``` -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 build number padding may eventually be exhausted. In order to -preserve both lexical ordering, build numbers are incremented in -a special 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 padding 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 be the case, -even if the padding was expanded and the version number was -incremented multiple times in the same month. To illustrate the -issue, imagine we did not expand the padding and instead just -incremented numerically. - -```python -"0001" -"0002" -"0003" -... -"0999" -"1000" -... -"9999" -"10000" -``` - -Here we eventually run into a build number where the lexical -ordering is not preserved, since `"9999" < "10000" == False`. -This is a very rare corner case, but it's better to not have -to think about it. - -Just as an example of why lexical ordering is a nice property to -have, there are lots of software which read git tags, but which -have no logic to parse version strings, which can nonetheless -order the version tags correctly. +To maintain lexical ordering of version numbers, the version +number is padded with extra zeros (see [Lexical +Ids](#lexical-ids) ). ## Usage @@ -289,8 +400,8 @@ README.md = {pep440_version} ``` -This may or may not cover all version numbers across your -repository. Something like the following may illustrate +This probably won't cover all instances of version numbers across +your repository. Something like the following may illustrate additional changes you might need to make. ```ini @@ -305,368 +416,249 @@ setup.cfg = current_version = {version} setup.py = version="{pep440_version}" -src/myproject.py = +myproject/__init__.py = __version__ = "{version}" README.md = - [PyCalVer {calver}{build}-{release}] - img.shields.io/badge/PyCalVer-{calver}{build}--{release}-blue + [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`. +To see if a pattern is found, you can use the `--dry` flag, which +will leave your repository untouched and only show you a diff. ```shell -$ pycalver show -Current Version: v201809.0001-beta -PEP440 Version: 201809.1b0 +$ pycalver bump --dry --no-fetch --release rc +INFO - Old Version: v201809.0001-beta +INFO - New Version: v201809.0002-rc +--- README.md ++++ README.md +@@ -11,7 +11,7 @@ -$ pycalver bump --dry -TODO: diff output -Don't forget to do $ git push --tags + [![Supported Python Versions][pyversions_img]][pyversions_ref] +-[![PyCalVer v201809.0001-beta][version_img]][version_ref] ++[![PyCalVer v201809.0002-rc][version_img]][version_ref] + [![PyPI Releases][pypi_img]][pypi_ref] + +--- myprojcet/__init__.py ++++ myprojcet/__init__.py +@@ -1,1 +1,1 @@ +-__version__ = "v201809.0001-beta" ++__version__ = "v201809.0002-rc" + +--- setup.py ++++ setup.py +@@ -44,7 +44,7 @@ + name="myproject", +- version="201812.11b0", ++ version="201812.12rc0", + license="MIT", ``` -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. +The `pycalver:file_patterns` section of the configuration is +used both to search and also to replace version strings in your +projects files. 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 | +| placeholder | example | +|--------------------|--------------------| +| `{pep440_version}` | 201809.1b0 | +| `{version}` | v201809.0001-alpha | +| `{calver}` | v201809 | +| `{year}` | 2018 | +| `{month}` | 09 | +| `{build}` | .0001 | +| `{build_no}` | 0001 | +| `{release}` | -alpha | +| `{release_tag}` | 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. - +Note that the separator/prefix characters can be part of what is +matched and generated for a given placeholder. In other words, +assuming you have the following text in your README.md (note the +two dashes before alpha): ``` -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) --- +https://img.shields.io/badge/PyCalVer-v201809.0001--alpha-blue.svg +``` -In [2]: version_dict -{'year': '2017', 'month': '12', 'build_nr': '0027', 'tag': 'beta'} ->>> str(version) -201712.27b0 +An appropriate pattern would be: + +```ini +README.md = + /badge/PyCalVer {calver}{build}-{release}-blue.svg +``` + +Notice that neither the "v" prefix, nor the "." and "-" +separators are included in the pattern text, as they are +respectivly part of the `calver`, `build` and `release` +placeholders. Alternatively you can be more explicit. + +```ini +README.md = + /badge/PyCalVer v{year}{month}.{build_no}--{release_tag}-blue.svg +``` + +One limitation to keep in mind is that a version string cannot +span multiple lines. + + +### Bump It Up + +The current version that will be bumped is defined either as + + - Initially: The value of `pycalver.current_version` in + `setup.cfg`/`pyproject.toml`/`pycalver.toml`. This is only + used if a project does not use a supported VCS or if no + version tags have been set so far. + - Typically: The lexically largest git/mercurial tag in the + repository. + +As part of doing `pycalver bump`, your local VCS index is +updated using `git fetch --tags`/`hg pull`, to ensure that all +tags are known locally and the same version is not generated for +different commits. This is mitigates a rare corner case where +`pycalver bump` is invoked on different machines. + + +```shell +$ pycalver show +INFO - fetching tags from remote (to turn off use: -n / --no-fetch) +Current Version: v201812.0005-beta +PEP440 Version : 201812.5b0 ``` +To increment and publish a new version, you can use the +`pycalver bump` command, which will do a few things: -### 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 + 0. Check that your repo doesn't have any local changes. + 1. *Fetch* the most recent global VCS tags from origin + (--no-fetch to disable). + 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 and tag. ``` -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 +$ pycalver bump --dry +--- setup.cfg ++++ setup.cfg +@@ -65,7 +65,7 @@ + [pycalver] +-current_version = v201812.0005-beta ++current_version = v201812.0006-beta + commit = True + tag = True + push = True ... -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. +### Version State -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. +The "current version" is considered global state that needs to +be stored somewhere. Typically this might be stored in a +`VERSION` file, or some other file which is part of the +repository. This creates the possibility that different parallel +branches can have different states, which might lead to the same +version number being generated for different commits. + +To avoid this issue, pycalver treats VCS tags as the canonical / +(SSOT)[https://en.wikipedia.org/wiki/Single_source_of_truth] for +the most recent version and attempts to change this state in the +most atomic way possible. This is why some actions of the +`pycalver` command can take a while, as it is synchronizing with +the remote repository to get the most recent versions and to +push any new version tags as soon as possible. -### FAQ +### Lexical Ids -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?!". +The build number padding may eventually be exhausted. In order +to preserve lexical ordering, build numbers are incremented in a +special way. Examples will perhaps illustrate more clearly. -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 +```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 padding 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: `new_version > old_version`. This will always be the +case, even if the padding was expanded and the version number +was incremented multiple times in the same month. To illustrate +the issue, consider what would happen if we did not expand the +padding and instead just incremented numerically. + +```python +"0001" +"0002" +"0003" +... +"0999" +"1000" +... +"9999" +"10000" +``` + +Here we eventually run into a build number where the lexical +ordering is not preserved, since `"10000" < "9999"` (because the +string `"1"` is lexically smaller than `"9"`). This is a very +rare corner case, but it's better to not have to think about it. + +Just as an example of why lexical ordering is a nice property to +have, there are lots of software which read git tags, but which +have no logic to parse version strings, which can nonetheless +order the version tags correctly. +## References [repo_ref]: https://gitlab.com/mbarkhau/pycalver +[setuptools_ref]: https://setuptools.readthedocs.io/en/latest/setuptools.html#specifying-your-project-s-version + +[pep_440_ref]: https://www.python.org/dev/peps/pep-0440/ + +[zeno_1_dot_0_ref]: http://sedimental.org/designing_a_version.html#semver-and-release-blockage + +[pep_101_ref]: https://www.python.org/dev/peps/pep-0101/ + +[bumpversion_ref]: https://github.com/peritus/bumpversion + + [build_img]: https://gitlab.com/mbarkhau/pycalver/badges/master/pipeline.svg [build_ref]: https://gitlab.com/mbarkhau/pycalver/pipelines @@ -682,7 +674,7 @@ because I don't think breaking changes should ever be [style_img]: https://img.shields.io/badge/code%20style-%20sjfmt-f71.svg [style_ref]: https://gitlab.com/mbarkhau/straitjacket/ -[downloads_img]: https://pepy.tech/badge/pycalver +[downloads_img]: https://pepy.tech/badge/pycalver/month [downloads_ref]: https://pepy.tech/project/pycalver [version_img]: https://img.shields.io/badge/PyCalVer-v201812.0011--beta-blue.svg diff --git a/setup.cfg b/setup.cfg index daba43a..7abf233 100644 --- a/setup.cfg +++ b/setup.cfg @@ -87,3 +87,5 @@ src/pycalver/__main__.py = README.md = [PyCalVer {version}] https://img.shields.io/badge/PyCalVer-{calver}{build}-{release}-blue.svg + Successfully installed pycalver-{pep440_version} + pycalver, version {version}