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:

  • MAJOR version when you make incompatible API changes,

  • MINOR version when you add functionality in a backwards compatible manner, and

  • MICRO version when you make backwards compatible bug fixes.

  • Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.MICRO format.

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

  • v1

  • r1.12

  • i1.2.13+linux_86_64

  • rev1.2.3.14

  • v1.2.3-dev

  • v1.2.3.dev23

  • v1.2.3.alpha1

  • v1.2.3.beta1

  • v1.2.3.rc1+deb25

  • 1.2.8.post2

  • 1.2.8.post2.dev4

  • v1.2.3.alpha4.post5.dev6+deb11u35

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

10.0.0-rc1

10.0.0rc1

10.0.0rc1

rc

10.0.0-pre1

10.0.0pre1

10.0.0rc1

rc

10.0.0-preview1

10.0.0preview1

10.0.0rc1

rc

10.0.0c1

10.0.0c1

10.0.0rc1

gamma → rc

10.0.0-dev

10.0.0-dev

10.0.0.dev0

dev → final

10.0.0.dev

10.0.0-dev

10.0.0.dev0

dev → final

v10.0.0-rc1

v10.0.0rc1

10.0.0rc1

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, alpha

  • pre-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.MICRO

  • YYYY.MINOR.MICRO

  • YY.MM

  • YYYY.0M

  • YYYY.MM.DD

  • YYYY.MM.DD_MICRO

  • YYYY-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 LowerBound property. Similarly, the upper bound of the version range can be read or updated by accessing the UpperBound property.

The behavior how lower and upper bound are handled can be read or modified by accessing the BoundHandling property.

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

VersionComparison

Meaning

== !=

Equal Unequal

Exactly this version, or anything but it.

< <= > >=

LessThan … GreaterThanOrEqual

The four ordering comparisons.

~=

CompatibleRelease

PEP 440’s compatible release.

^

Caret

npm’s may not change the leftmost non-zero part.

~

Tilde

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

CompatibleVersionConstraint

~=1.2.3

1.3.0 - drop the last part written, increment what becomes the last.

CaretVersionConstraint

^1.2.3

2.0.0 - increment the leftmost non-zero part that was written.

TildeVersionConstraint

~1.2.3

1.3.0 - increment the minor part, or the major one when no minor part was written.

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

PythonVersionExpression

,

PEP 440: the six ordering comparisons plus ~=. Versions parse as PythonVersion.

NPMVersionExpression

whitespace

Equality is =, never ==; there is no !=; adds ^ and ~. A comma is a syntax error.

DebianVersionExpression

,

Strict comparisons are << and >>, equality is =, and there is no !=.

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 PythonVersion and packaging disagree, packaging is right.

  • Parses everything PEP 440 allows, e.g. local versions as 1.0+local.7, which PythonVersion doesn’t parse, and the operator ===.

  • A SpecifierSet handles 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, which SemanticVersion doesn’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 as 2.x, and - in node-semver - hyphen ranges. NPMVersionExpression parses none of them.

  • node-semver answers satisfies and max_satisfying as 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; DebianVersionExpression parses its versions as SemanticVersion.

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 vers notation, as vers:npm/>=1.0.2|<2.0.0 for ^1.0.2, and back.