Versioning
The pyTooling.Versioning package provides auxiliary classes to implement Semantic Versioning
(following SemVer rules) and Calendar Versioning (following
CalVer rules). The latter one has multiple variants due to the meaning of the version’s parts
like: year-month version or year-week version.
Versions can be grouped by version sets and version ranges.
Semantic Versioning
The SemanticVersion class represents of a version number like v3.7.12. It consists of
a major, minor and micro number. The micro number is also known as patch number. The minor and micro numbers are
optional, but usually used by most semantic version numbering schemes. In addition, optional parts can be added like a
prefix, a postfix or a build number.
Hint
Given a version number MAJOR.MINOR.MICRO, increment the:
MAJORversion when you make incompatible API changes,MINORversion when you add functionality in a backwards compatible manner, andMICROversion when you make backwards compatible bug fixes.Additional labels for pre-release and build metadata are available as extensions to the
MAJOR.MINOR.MICROformat.
Summary taken from semver.org.
Direct Instantiation
A semantic version can be constructed from parts like major, minor and micro numbers.
# Construct from numbers
version = SemanticVersion(1, 5, 2)
Construction from String
Alternatively, a semantic version can be created from a string containing a semantic version number by using the
class-method Parse(). The string is parsed and a semantic version gets
returned.
# Construct from string
version = SemanticVersion.Parse("0.22.8")
Usage
# Compare versions
if version2 > version1:
# Compare versions
if version2 >= "1.4.8":
Features
- Prefix string
Represents the prefix like:
v(version),r(revision),i(internal version/release),ver(version),rev(revision).v1.2.3
- Major number
Represents the major version number in semantic version.
v1.2.3
- Minor number
Represents the minor version number in semantic version.
v1.2.3
- Micro number
Represents the micro or patch version number in semantic version.
v1.2.3
- Build number
Represents the build number.
v1.2.3.4
- Release Level / Release number
Distinguishes if a version is in alpha, beta, release candidate or final release level.
v1.2.3.alpha4
v1.2.3.beta4
v1.2.3.rc4- Post number
tbd
v1.2.3.post4
- Development number
tbd
v1.2.3.dev4
- Postfix string
v1.2.3+deb11u5
- Comparison operators
Operators for
==,!=,<,<=,>,>=,>>.- String formatting
The version number can be formatted as a string with a fixed formatting pattern based on present version parts as well as a user-defined formatting via
__format__()
Examples
|
|
|
Condensed Class Definition
@export
class SemanticVersion(Version):
@classmethod
def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[[SemanticVersion], bool]] = None) -> Version:
pass
@readonly
def Parts(self) -> Parts:
pass
@readonly
def Prefix(self) -> str:
pass
@readonly
def Major(self) -> int:
pass
@readonly
def Minor(self) -> int:
pass
@readonly
def Micro(self) -> int:
pass
@readonly
def Patch(self) -> int:
pass
@readonly
def ReleaseLevel(self) -> ReleaseLevel:
pass
@readonly
def ReleaseNumber(self) -> int:
pass
@readonly
def ReleaseLevelSpelling(self) -> str:
pass
@readonly
def Post(self) -> int:
pass
@readonly
def Dev(self) -> int:
pass
@readonly
def Build(self) -> int:
pass
@readonly
def Postfix(self) -> str:
pass
@readonly
def Hash(self) -> str:
pass
@readonly
def Flags(self) -> Flags:
pass
def __eq__(self, other: Union[SemanticVersion, str, int, None]) -> bool:
pass
def __ne__(self, other: Union[SemanticVersion, str, int, None]) -> bool:
pass
def __lt__(self, other: Union[SemanticVersion, str, int, None]) -> bool:
pass
def __le__(self, other: Union[SemanticVersion, str, int, None]) -> bool:
pass
def __gt__(self, other: Union[SemanticVersion, str, int, None]) -> bool:
pass
def __ge__(self, other: Union[SemanticVersion, str, int, None]) -> bool:
pass
def __imod__(self, other: Union[SemanticVersion, str, int, None]) -> bool:
pass
def __format__(self, formatSpec: str) -> str:
pass
def __repr__(self) -> str:
pass
def __str__(self) -> str:
pass
Variants
Examples
3.13.0
3.13.0a4
3.13.0b2
3.13.0rc2
10.0.0.dev0
Parse() also accepts the spellings PEP 440 normalizes. A
parsed version keeps its spelling and compares equal to its normalized form, which
Normalize() returns, as does Parse(..., normalize=True):
Parsed |
Written |
Normalized |
Release level |
|---|---|---|---|
|
|
|
rc |
|
|
|
rc |
|
|
|
rc |
|
|
|
gamma → rc |
|
|
|
dev → final |
|
|
|
dev → final |
|
|
|
rc |
SemanticVersion reads pre and preview as a release candidate, too,
but keeps its own meaning of c (gamma) and -dev (a release level): there, 10.0.0c1 isn’t
10.0.0rc1.
A release level without a number has the number 0, as PEP 440 normalizes 1.0a to 1.0a0 - so
1.0.0-alpha is an alpha release, not a final release with the postfix alpha. A fourth numeric
component is the build number, and 1.2.3.4 is written back as such.
Condensed Class Definition
@export
class PythonVersion(SemanticVersion):
@classmethod
def Parse(
cls,
versionString: Nullable[str],
validator: Nullable[Callable[[SemanticVersion], bool]] = None,
*,
normalize: bool = False
) -> PythonVersion:
pass
def Normalize(self) -> PythonVersion:
pass
@classmethod
def FromSysVersionInfo(cls) -> PythonVersion:
pass
Calendar Versioning
The CalendarVersion class represents of a version number like 2021.10.
Direct Instantiation
Alternatively, a calendar version can be constructed from parts like major, minor and micro numbers. The unified naming of parts can be used to map years to major numbers, months to minor numbers, etc.
# Construct from numbers
version = CalendarVersion(2024, 5)
Construction from String
A calendar version can be created from a string containing a calendar version number by using the class-method
Parse(). The string is parsed and a calendar version gets returned.
# Construct from string
version = CalendarVersion.Parse("2024.05")
Usage
# Compare versions
if version2 > version1:
# Compare versions
if version2 >= "2023.02":
Features
- Major number
Represents the major version number in semantic version.
- Minor number
Represents the minor version number in semantic version.
- Micro number
Represents the micro or patch version number in semantic version.
- Build number
Represents the build number.
- Prefix string
Represents the prefix like:
v(version),r(revision),i(internal version/release),ver(version),rev(revision).- Comparison operators
Operators for
==,!=,<,<=,>,>=,>>.
Missing Features
release-level: additional labels like
dev,rc,pl,alphapre-version and post-version
Condensed Class Definition
@export
class CalendarVersion(Version):
@classmethod
def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[[CalendarVersion], bool]] = None) -> CalendarVersion:
pass
@readonly
def Parts(self) -> Parts:
pass
@readonly
def Major(self) -> int:
pass
@readonly
def Minor(self) -> int:
pass
@readonly
def Micro(self) -> int:
pass
@readonly
def Patch(self) -> int:
pass
@readonly
def Build(self) -> int:
pass
@readonly
def Flags(self) -> Flags:
pass
@readonly
def Prefix(self) -> str:
pass
@readonly
def Postfix(self) -> str:
pass
def __eq__(self, other: Union[CalendarVersion, str, int, None]) -> bool:
pass
def __ne__(self, other: Union[CalendarVersion, str, int, None]) -> bool:
pass
def __lt__(self, other: Union[CalendarVersion, str, int, None]) -> bool:
pass
def __le__(self, other: Union[CalendarVersion, str, int, None]) -> bool:
pass
def __gt__(self, other: Union[CalendarVersion, str, int, None]) -> bool:
pass
def __ge__(self, other: Union[CalendarVersion, str, int, None]) -> bool:
pass
def __imod__(self, other: Union[CalendarVersion, str, int, None]) -> bool:
pass
def __format__(self, formatSpec: str) -> str:
pass
def __repr__(self) -> str:
pass
def __str__(self) -> str:
pass
Variants
Hint
Calendar versions have multiple format variants:
YY.MINOR.MICROYYYY.MINOR.MICROYY.MMYYYY.0MYYYY.MM.DDYYYY.MM.DD_MICROYYYY-MM-DD
Formats taken from calver.org.
Direct Instantiation
A year-month version can be constructed from year and month numbers.
# Construct from numbers
version = YearMonthVersion(2024, 5)
Construction from String
A semantic version can also be created from a string containing a year-month version number by using the
class-method Parse(). The string is parsed and a year-month
version gets returned.
# Construct from string
version = YearMonthVersion.Parse("2024.05")
Examples
OSVVM: 2024.07
Ubuntu: 2024.10
Condensed Class Definition
@export
class YearMonthVersion(CalendarVersion):
@classmethod
def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[[YearMonthVersion], bool]] = None) -> YearMonthVersion:
pass
@readonly
def Year(self) -> int:
pass
@readonly
def Month(self) -> int:
pass
Direct Instantiation
A year-week version can be constructed from year and month numbers.
# Construct from numbers
version = YearWeekVersion(2024, 5)
Construction from String
A semantic version can also be created from a string containing a year-week version number by using the
class-method Parse(). The string is parsed and a year-week
version gets returned.
# Construct from string
version = YearWeekVersion.Parse("2024.05")
Examples
Production date codes
Condensed Class Definition
@export
class YearWeekVersion(CalendarVersion):
@classmethod
def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[[YearWeekVersion], bool]] = None) -> YearWeekVersion:
pass
@readonly
def Year(self) -> int:
pass
@readonly
def Week(self) -> int:
pass
Direct Instantiation
A year-release version can be constructed from year and month numbers.
# Construct from numbers
version = YearReleaseVersion(2024, 2)
Construction from String
A semantic version can also be created from a string containing a year-release version number by using the
class-method Parse(). The string is parsed and a year-release
version gets returned.
# Construct from string
version = YearReleaseVersion.Parse("2024.2")
Examples
Vivado: 2024.1
Condensed Class Definition
@export
class YearReleaseVersion(CalendarVersion):
@classmethod
def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[[YearReleaseVersion], bool]] = None) -> YearReleaseVersion:
pass
@readonly
def Year(self) -> int:
pass
@readonly
def Release(self) -> int:
pass
Direct Instantiation
A year-month-day version can be constructed from year, month and day numbers.
# Construct from numbers
version = YearMonthDayVersion(2024, 10, 5)
Construction from String
A semantic version can also be created from a string containing a year-month-day version number by using the
class-method Parse(). The string is parsed and a
year-month-day version gets returned.
# Construct from string
version = YearMonthDayVersion.Parse("2024.10.05")
Examples
Furo: 2024.04.27
Condensed Class Definition
@export
class YearMonthDayVersion(CalendarVersion):
@classmethod
def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[[YearMonthDayVersion], bool]] = None) -> YearMonthDayVersion:
pass
@readonly
def Year(self) -> int:
pass
@readonly
def Month(self) -> int:
pass
@readonly
def Day(self) -> int:
pass
VersionRange
A VersionRange defines a range of versions reaching from a lower to an upper bound.
It equivalently supports semantic and calendar
versions or derived subclasses thereof. When initializing a version range, an optional
RangeBoundHandling flag specifies if the bounds are inclusive (default) or
exclusive.
Features
- Access bounds and bound handling behavior
The lower bound of the version range can be read or updated by accessing the
LowerBoundproperty. Similarly, the upper bound of the version range can be read or updated by accessing theUpperBoundproperty.The behavior how lower and upper bound are handled can be read or modified by accessing the
BoundHandlingproperty.- Comparison of two version ranges
A version range can be compare to another version range using comparison operators:
<,<=,>,>=.- Comparison of a version range and a version
A version can be compared with a version range and vise versa using comparison operators:
<,<=,>,>=.The behavior is influenced by the bound handling behavior.
- Contains checks
A version can be checked if it’s contained in a version range using contains operators:
in,not in.The behavior is influenced by the bound handling behavior.
- Intersection
Two version ranges can be intersected using the
&operator creating a new version range.In case of an empty intersection result, an exception is raised.
Condensed Class Definition
@export
class VersionRange(Generic[V], metaclass=ExtendedType, slots=True):
def __init__(self, lowerBound: V, upperBound: V, boundHandling: RangeBoundHandling = RangeBoundHandling.BothBoundsInclusive) -> None:
@property
def LowerBound(self) -> V:
pass
@property
def UpperBound(self) -> V:
pass
@property
def BoundHandling(self) -> RangeBoundHandling:
pass
def __and__(self, other: Any) -> VersionRange[T]:
pass
def __lt__(self, other: Any) -> bool:
pass
def __le__(self, other: Any) -> bool:
pass
def __gt__(self, other: Any) -> bool:
pass
def __ge__(self, other: Any) -> bool:
pass
def __contains__(self, version: Version) -> bool:
pass
from pyTooling.Versioning import SemanticVersion, VersionRange
versionRange = VersionRange(
lowerBound=SemanticVersion(1, 0, 0),
upperBound=SemanticVersion(1, 9, 0)
)
testVersion = SemanticVersion(1, 4, 3)
if testVersion in versionRange:
pass
from pyTooling.Versioning import SemanticVersion, VersionRange
versionRange = VersionRange(
lowerBound=YearWeekVersion(2023, 34),
upperBound=YearWeekVersion(2023, 51),
boundHandling=RangeBoundHandling.UpperBoundExclusive)
)
testVersion = YearWeekVersion(2023, 51)
if testVersion not in versionRange:
pass
VersionSet
A VersionSet defines an ordered set (actually a list) of versions. It equivalently
supports semantic and calendar versions or
derived subclasses thereof.
Features
- Accessing versions in the set
The versions within a version set can be accessed via index operation (
__getitem__) or iterating (__iter__) the version set.The number of elements is accessible via length operation (
__len__).- Comparison of two version sets
A version set can be compare to another version set using comparison operators:
<,<=,>,>=.- Comparison of a version set and a version
A version can be compared with a version set and vise versa using comparison operators:
<,<=,>,>=.- Contains checks
A version can be checked if it’s contained in a version set using contains operators:
in,not in.- Intersection
Two version set can be intersected using the
&operator creating a new version set.In case of an empty intersection result, an exception is raised.
- Union
Two version sets can be united using the
|operator creating a new version set.
Condensed Class Definition
@export
class VersionSet(Generic[V], metaclass=ExtendedType, slots=True):
def __init__(self, versions: Union[Version, Iterable[V]]):
pass
def __and__(self, other: VersionSet[V]) -> VersionSet[T]:
pass
def __or__(self, other: VersionSet[V]) -> VersionSet[T]:
pass
def __lt__(self, other: Any) -> bool:
pass
def __le__(self, other: Any) -> bool:
pass
def __gt__(self, other: Any) -> bool:
pass
def __ge__(self, other: Any) -> bool:
pass
def __contains__(self, version: V) -> bool:
pass
def __len__(self) -> int:
pass
def __iter__(self) -> Iterator[V]:
pass
def __getitem__(self, index: int) -> V:
pass
from pyTooling.Versioning import SemanticVersion, VersionSet
versionSet = VersionSet((
YearMonthVersion(2024, 4),
YearMonthVersion(2025, 1),
YearMonthVersion(2019, 3)
))
testVersion = YearMonthVersion(2019, 3)
if testVersion in versionSet:
pass
from pyTooling.Versioning import SemanticVersion, VersionSet
versionSet = VersionSet((
YearMonthVersion(2024, 4),
YearMonthVersion(2025, 1),
YearMonthVersion(2019, 3)
))
for version in versionSet:
pass
Version Constraints and Expressions
A VersionRange says which versions are acceptable; a version expression is how a
packaging ecosystem writes that down - >=1.2.0,<2.0.0 in a requirements file, ^1.2.3 in a
package.json, (>= 1.2.0) in a debian/control.
VersionExpression
A VersionExpression is a conjunction of constraints: every one of them has to be
satisfied, which is what separating them means in every ecosystem that has the notion.
from pyTooling.Versioning import VersionExpression, SemanticVersion
expression = VersionExpression.Parse(">=1.2.0,<2.0.0")
SemanticVersion.Parse("1.5.0") in expression # True
SemanticVersion.Parse("2.0.0") in expression # False
An expression with no constraints matches every version, and
MatchesAnyVersion reports it - so no version restriction is a value
its callers can carry rather than a case they have to special-case.
Constraints gives the individual
VersionConstraint objects, and
ToVersionRange() collapses the whole expression into the single
VersionRange it describes - which is the bridge between how a dependency is written
and how it is reasoned about.
VersionConstraint
One VersionConstraint is one comparison: a
VersionComparison and the version it compares against.
Written |
|
Meaning |
|---|---|---|
|
|
Exactly this version, or anything but it. |
|
|
The four ordering comparisons. |
|
|
PEP 440’s compatible release. |
|
|
npm’s may not change the leftmost non-zero part. |
|
|
npm’s may not change the minor part. |
The last three are shorthands for a range, and they are what
RangeVersionConstraint implements: at least the version written, and below a bound
derived from it. The derived bound is readable as
UpperBound, and each subclass derives it differently:
Class |
Example |
Upper bound |
|---|---|---|
|
|
|
|
|
|
|
|
Attention
~= and ~ are not the same operator: PEP 440’s ~= depends on how many parts were written, while
npm’s ~ always works on the minor part. They agree for 1.2.3 and disagree for 1.2.
Dialects
The operators above are not spelled the same everywhere, so an expression is parsed by the class belonging to the ecosystem it was written in.
Class |
Separator |
Differences |
|---|---|---|
|
PEP 440: the six ordering comparisons plus |
|
whitespace |
Equality is |
|
|
Strict comparisons are |
Attention
DebianVersionExpression deliberately rejects the obsolete < and >.
dpkg still accepts them with a warning, because they historically meant <= and >= - reading
them as the strict operators would silently invert their meaning.
Epoch
An epoch outranks every other part of a version number, and exists for the case a project’s versioning scheme
changed so that the new numbers sort below the old ones. It is readable as
Epoch, and it is present only when the parsed string stated one.
The separator differs by scheme: SemanticVersion writes 1:1.2.3, while
PythonVersion writes 1!1.2.3 as PEP 440 prescribes.
Validators
A version parsed from an untrusted string can carry any number, which is a problem when it has to fit a fixed-width field later. A validator is a callable given to the parser, and it rejects a version instead of letting it through.
Two factories build one:
WordSizeValidator()- bounds each part by a number of bits, for a version that has to fit a hardware register or a packed struct;MaxValueValidator()- bounds each part by an explicit maximum.
Both take one limit for every part (bits / max) or a limit per part (majorBits, minorBits,
microBits, …), so the common case is one argument.
A rejected version raises VersionValidatorError, whose Version property is the
version that was rejected.
Competing Solutions
pyTooling.Versioning puts several version schemes - semantic versions, PEP 440 versions and four calendar
schemes - and three dialects of version expressions - PEP 440, npm and Debian - behind one API of
Version, VersionRange and
VersionSet. Each package below covers one scheme more completely, except for univers,
which covers many schemes by building on scheme-specific packages. For calendar versions, the packages on PyPI -
calver, bumpver - write or bump a
version number in a project’s files; none of them parses and compares one.
packaging
Source: packaging, on PyPI as packaging.
Disadvantages
PEP 440 only: no semantic versions, calendar versions, or npm and Debian expressions.
Advantages
The reference implementation of PEP 440, which pip uses. Where
PythonVersionand packaging disagree, packaging is right.Parses everything PEP 440 allows, e.g. local versions as
1.0+local.7, whichPythonVersiondoesn’t parse, and the operator===.A
SpecifierSethandles pre-releases as pip does, and filters an iterable of versions.
semver
Source: python-semver, on PyPI as semver.
Disadvantages
Semantic versions only.
match()checks one comparison, as>=1.0.0, not an expression of several.
Advantages
The whole grammar of SemVer 2.0.0, including dot-separated pre-release and build parts as
1.2.3-pre.2+build.4, whichSemanticVersiondoesn’t parse.Functions to bump a version’s parts.
semantic-version and node-semver
Source: python-semanticversion, on PyPI as
semantic-version (last release 2022), and
python-node-semver, a port of npm’s node-semver, on PyPI as
node-semver.
Disadvantages
Semantic versions and npm’s range notation only.
Advantages
npm’s whole range notation: the alternative
||, x-ranges as2.x, and - in node-semver - hyphen ranges.NPMVersionExpressionparses none of them.node-semver answers
satisfiesandmax_satisfyingas npm does, and has a loose mode for malformed versions.
python-debian
Source: python-debian, on PyPI as python-debian.
Disadvantages
Debian versions only.
Advantages
Orders Debian versions as dpkg does: epoch, upstream version and revision, with
~sorting before everything. pyTooling has no Debian version class;DebianVersionExpressionparses its versions asSemanticVersion.
univers
Source: univers, on PyPI as univers.
Disadvantages
Depends on attrs, packaging, semantic-version and semver.
Standoff
Like this package, one model for the versions and ranges of several ecosystems.
Advantages
Many more schemes, among them npm, PyPI, RubyGems, Debian, Maven, RPM, Go and Composer.
Converts a range in an ecosystem’s own notation into the common
versnotation, asvers:npm/>=1.0.2|<2.0.0for^1.0.2, and back.