From f67ece3b0aaac69f4d331b213090ed78e6ba46d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 8 Nov 2021 14:23:34 +0100 Subject: [PATCH 01/21] Remove ambiguous floats in .yml files `3.8` is interpreted as a floating point number, but we view it as a string identifier; quote all python version numbers. --- .github/workflows/test.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 67e1fbf..5101fd2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,33 +14,33 @@ jobs: matrix: include: # Django 1.11: 3.4, 3.7 - - python-version: 2.7 + - python-version: "2.7" django-family: 111 - - python-version: 3.4 + - python-version: "3.4" django-family: 111 - - python-version: 3.7 + - python-version: "3.7" django-family: 111 - - python-version: pypy3 + - python-version: "pypy3" django-family: 111 # Django 2.2: 3.5, 3.7, 3.8 - - python-version: 3.5 + - python-version: "3.5" django-family: 22 - - python-version: 3.7 + - python-version: "3.7" django-family: 22 - - python-version: 3.8 + - python-version: "3.8" django-family: 22 - - python-version: pypy3 + - python-version: "pypy3" django-family: 22 # Django 3.1: Python 3.6, 3.8, 3.9 - - python-version: 3.6 + - python-version: "3.6" django-family: 31 - - python-version: 3.8 + - python-version: "3.8" django-family: 31 - - python-version: 3.9 + - python-version: "3.9" django-family: 31 - - python-version: pypy3 + - python-version: "pypy3" django-family: 31 env: From 63720c70dd538234e75914e2afb80b94d5e88a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 8 Nov 2021 14:17:52 +0100 Subject: [PATCH 02/21] Extend test matrix for Python 3.10 / Django 3.2 Django >=3.2.9 is compatible with Python 3.10 --- .github/workflows/test.yml | 12 +++++++----- setup.cfg | 1 + tox.ini | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5101fd2..7eb4638 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,15 +33,17 @@ jobs: - python-version: "pypy3" django-family: 22 - # Django 3.1: Python 3.6, 3.8, 3.9 + # Django 3.2: Python 3.6, 3.8, 3.9, 3.10 - python-version: "3.6" - django-family: 31 + django-family: 32 - python-version: "3.8" - django-family: 31 + django-family: 32 - python-version: "3.9" - django-family: 31 + django-family: 32 + - python-version: "3.10" + django-family: 32 - python-version: "pypy3" - django-family: 31 + django-family: 32 env: TOXENV: django${{ matrix.django-family }} diff --git a/setup.cfg b/setup.cfg index 42360d4..e7fd5e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Topic :: Software Development :: Libraries :: Python Modules [options] diff --git a/tox.ini b/tox.ini index f1cb1be..31e6e34 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ envlist = py{27,34,35,36,37}-django111 py{35,36,37,38}-django22 - py{36,37,38,39}-django31 - pypy3-django{111,22,31} + py{36,37,38,39,310}-django32 + pypy3-django{111,22,32} lint toxworkdir = {env:TOX_WORKDIR:.tox} @@ -13,7 +13,7 @@ extras = dev deps = django111: Django>=1.11,<1.12 django22: Django>=2.2,<2.3 - django31: Django>=3.1,<3.2 + django32: Django>=3.2,<3.3 whitelist_externals = make commands = make test From 8abf0030e215804eb84e63a9233010abc02e05a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 8 Nov 2021 14:18:29 +0100 Subject: [PATCH 03/21] Add support for Django 4.0 The gettext_lazy function has a different name between Django 2.x and 4.x; use the right one according to the version. Closes #113, #121 --- .github/workflows/test.yml | 8 ++++++++ ChangeLog | 4 ++-- semantic_version/django_fields.py | 8 +++++++- tox.ini | 2 ++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7eb4638..02cb641 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,14 @@ jobs: - python-version: "pypy3" django-family: 32 + # Django 4.0: Python 3.8, 3.9, 3.10 + - python-version: "3.8" + django-family: 40 + - python-version: "3.9" + django-family: 40 + - python-version: "3.10" + django-family: 40 + env: TOXENV: django${{ matrix.django-family }} diff --git a/ChangeLog b/ChangeLog index c8453e5..211be6b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,8 +6,8 @@ ChangeLog *New:* - * Add support for Django 3.1 - * Add support for Python 3.7 / 3.8 / 3.9 + * Add support for Django 3.1, 3.2, 4.0 + * Add support for Python 3.7 / 3.8 / 3.9 / 3.10 2.8.5 (2020-04-29) diff --git a/semantic_version/django_fields.py b/semantic_version/django_fields.py index caa7a8c..e5bd7eb 100644 --- a/semantic_version/django_fields.py +++ b/semantic_version/django_fields.py @@ -4,8 +4,14 @@ import warnings +import django from django.db import models -from django.utils.translation import ugettext_lazy as _ + +if django.VERSION >= (3, 0): + # See https://docs.djangoproject.com/en/dev/releases/3.0/#features-deprecated-in-3-0 + from django.utils.translation import gettext_lazy as _ +else: + from django.utils.translation import ugettext_lazy as _ from . import base diff --git a/tox.ini b/tox.ini index 31e6e34..26cbebe 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = py{27,34,35,36,37}-django111 py{35,36,37,38}-django22 py{36,37,38,39,310}-django32 + py{38,39,310}-django40 pypy3-django{111,22,32} lint @@ -14,6 +15,7 @@ deps = django111: Django>=1.11,<1.12 django22: Django>=2.2,<2.3 django32: Django>=3.2,<3.3 + django40: Django>=4.0a1,<4.1 whitelist_externals = make commands = make test From 81a4730778fba6b5c76242d3c8da6dace7e2ec0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 8 Nov 2021 14:37:11 +0100 Subject: [PATCH 04/21] Remove Python 3.4 from testing matrix No longer available on GitHub. --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 02cb641..3c9918b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,10 @@ jobs: fail-fast: false matrix: include: - # Django 1.11: 3.4, 3.7 + # Django 1.11: 3.5, 3.7 - python-version: "2.7" django-family: 111 - - python-version: "3.4" + - python-version: "3.5" django-family: 111 - python-version: "3.7" django-family: 111 From 7e59a4b2e82abe4338e307b9fe49b072c9537a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 6 Feb 2022 19:40:53 +0100 Subject: [PATCH 05/21] Improve documentation - The README is now a standalone document, also included as an "introduction" page; - A new "guide" section provides more details on most features; - A couple of typos were fixed. The main goal was to make the README file perfectly suitable for rendering on PyPI, while keeping its content available on the standard documentation on ReadTheDocs. --- README.rst | 205 +++++++++---------------- docs/django.rst | 2 +- docs/guide.rst | 346 ++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 31 +++- docs/introduction.rst | 1 + docs/reference.rst | 34 +++-- 6 files changed, 467 insertions(+), 152 deletions(-) create mode 100644 docs/guide.rst mode change 120000 => 100644 docs/index.rst create mode 120000 docs/introduction.rst diff --git a/README.rst b/README.rst index 03d1691..4735cce 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -python-semanticversion -====================== +Introduction +============ This small python library provides a few tools to handle `SemVer`_ in Python. It follows strictly the 2.0.0 version of the SemVer scheme. @@ -57,20 +57,18 @@ Import it in your code: import semantic_version -.. currentmodule:: semantic_version - This module provides classes to handle semantic versions: -- :class:`Version` represents a version number (``0.1.1-alpha+build.2012-05-15``) -- :class:`BaseSpec`-derived classes represent requirement specifications (``>=0.1.1,<0.3.0``): +- ``Version`` represents a version number (``0.1.1-alpha+build.2012-05-15``) +- ``BaseSpec``-derived classes represent requirement specifications (``>=0.1.1,<0.3.0``): - - :class:`SimpleSpec` describes a natural description syntax - - :class:`NpmSpec` is used for NPM-style range descriptions. + - ``SimpleSpec`` describes a natural description syntax + - ``NpmSpec`` is used for NPM-style range descriptions. Versions -------- -Defining a :class:`Version` is quite simple: +Defining a ``Version`` is quite simple: .. code-block:: pycon @@ -90,7 +88,7 @@ Defining a :class:`Version` is quite simple: >>> list(v) [0, 1, 1, [], []] -If the provided version string is invalid, a :exc:`ValueError` will be raised: +If the provided version string is invalid, a ``ValueError`` will be raised: .. code-block:: pycon @@ -104,7 +102,39 @@ If the provided version string is invalid, a :exc:`ValueError` will be raised: ValueError: Invalid version string: '0.1' -Obviously, :class:`Versions ` can be compared: +One may also create a ``Version`` with named components: + +.. code-block:: pycon + + >>> semantic_version.Version(major=0, minor=1, patch=2) + Version('0.1.2') + +In that case, ``major``, ``minor`` and ``patch`` are mandatory, and must be integers. +``prerelease`` and ``build``, if provided, must be tuples of strings: + +.. code-block:: pycon + + >>> semantic_version.Version(major=0, minor=1, patch=2, prerelease=('alpha', '2')) + Version('0.1.2-alpha.2') + + +Some user-supplied input might not match the semantic version scheme. +For such cases, the ``Version.coerce`` method will try to convert any +version-like string into a valid semver version: + +.. code-block:: pycon + + >>> Version.coerce('0') + Version('0.0.0') + >>> Version.coerce('0.1.2.3.4') + Version('0.1.2+3.4') + >>> Version.coerce('0.1.2a3') + Version('0.1.2-a3') + +Working with versions +""""""""""""""""""""" + +Obviously, versions can be compared: .. code-block:: pycon @@ -133,38 +163,31 @@ You can also get a new version that represents a bump in one of the version leve >>> str(new_v) '1.1.2' -It is also possible to check whether a given string is a proper semantic version string: - - -.. code-block:: pycon - - >>> semantic_version.validate('0.1.3') - True - >>> semantic_version.validate('0a2') - False -Finally, one may create a :class:`Version` with named components instead: - -.. code-block:: pycon - - >>> semantic_version.Version(major=0, minor=1, patch=2) - Version('0.1.2') - -In that case, ``major``, ``minor`` and ``patch`` are mandatory, and must be integers. -``prerelease`` and ``build``, if provided, must be tuples of strings: +Requirement specification +------------------------- -.. code-block:: pycon +python-semanticversion provides a couple of ways to describe a range of accepted +versions: - >>> semantic_version.Version(major=0, minor=1, patch=2, prerelease=('alpha', '2')) - Version('0.1.2-alpha.2') +- The ``SimpleSpec`` class provides a simple, easily understood scheme -- + somewhat inspired from PyPI range notations; +- The ``NpmSpec`` class supports the whole NPM range specification scheme: + .. code-block:: pycon -Requirement specification -------------------------- + >>> Version('0.1.2') in NpmSpec('0.1.0-alpha.2 .. 0.2.4') + True + >>> Version('0.1.2') in NpmSpec('>=0.1.1 <0.1.3 || 2.x') + True + >>> Version('2.3.4') in NpmSpec('>=0.1.1 <0.1.3 || 2.x') + True -The :class:`SimpleSpec` object describes a range of accepted versions: +The ``SimpleSpec`` scheme +""""""""""""""""""""""""" +Basic usage is simply a comparator and a base version: .. code-block:: pycon @@ -176,6 +199,12 @@ The :class:`SimpleSpec` object describes a range of accepted versions: >>> s.match(Version('0.1.0')) False +Combining specifications can be expressed as follows: + + .. code-block:: pycon + + >>> SimpleSpec('>=0.1.1,<0.3.0') + Simpler test syntax is also available using the ``in`` keyword: .. code-block:: pycon @@ -189,17 +218,16 @@ Simpler test syntax is also available using the ``in`` keyword: False -Combining specifications can be expressed as follows: - - .. code-block:: pycon +Refer to the full documentation at +https://python-semanticversion.readthedocs.io/en/latest/ for more details on the +``SimpleSpec`` scheme. - >>> SimpleSpec('>=0.1.1,<0.3.0') Using a specification """"""""""""""""""""" -The :func:`SimpleSpec.filter` method filters an iterable of :class:`Version`: +The ``SimpleSpec.filter`` method filters an iterable of ``Version``: .. code-block:: pycon @@ -222,82 +250,6 @@ It is also possible to select the 'best' version from such iterables: Version('0.3.0') -Coercing an arbitrary version string -"""""""""""""""""""""""""""""""""""" - -Some user-supplied input might not match the semantic version scheme. -For such cases, the :meth:`Version.coerce` method will try to convert any -version-like string into a valid semver version: - -.. code-block:: pycon - - >>> Version.coerce('0') - Version('0.0.0') - >>> Version.coerce('0.1.2.3.4') - Version('0.1.2+3.4') - >>> Version.coerce('0.1.2a3') - Version('0.1.2-a3') - - -Including pre-release identifiers in specifications -""""""""""""""""""""""""""""""""""""""""""""""""""" - -When testing a :class:`Version` against a :class:`SimpleSpec`, comparisons are -adjusted for common user expectations; thus, a pre-release version (``1.0.0-alpha``) -will not satisfy the ``==1.0.0`` :class:`SimpleSpec`. - -Pre-release identifiers will only be compared if included in the :class:`BaseSpec` -definition or (for the empty pre-release number) if a single dash is appended -(``1.0.0-``): - - -.. code-block:: pycon - - >>> Version('0.1.0-alpha') in SimpleSpec('<0.1.0') # No pre-release identifier - False - >>> Version('0.1.0-alpha') in SimpleSpec('<0.1.0-') # Include pre-release in checks - True - - -Including build metadata in specifications -"""""""""""""""""""""""""""""""""""""""""" - -Build metadata has no ordering; thus, the only meaningful comparison including -build metadata is equality. - - -.. code-block:: pycon - - >>> Version('1.0.0+build2') in SimpleSpec('<=1.0.0') # Build metadata ignored - True - >>> Version('1.0.0+build1') in SimpleSpec('==1.0.0+build2') # Include build in checks - False - - -NPM-based ranges ----------------- - -The :class:`NpmSpec` class handles NPM-style ranges: - -.. code-block:: pycon - - >>> Version('1.2.3') in NpmSpec('1.2.2 - 1.4') - True - >>> Version('1.2.3') in NpmSpec('<1.x || >=1.2.3') - True - -Refer to https://docs.npmjs.com/misc/semver.html for a detailed description of NPM -range syntax. - - -Using with Django -================= - -The :mod:`semantic_version.django_fields` module provides django fields to -store :class:`Version` or :class:`BaseSpec` objects. - -More documentation is available in the :doc:`django` section. - Contributing ============ @@ -323,26 +275,5 @@ When submitting patches or pull requests, you should respect the following rules # -*- encoding: utf-8 -*- # Copyright (c) The python-semanticversion project - -Contents -======== - -.. toctree:: - :maxdepth: 2 - - reference - django - changelog - credits - - .. _SemVer: http://semver.org/ .. _PyPI: http://pypi.python.org/ - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/docs/django.rst b/docs/django.rst index 34a0fe3..befa50a 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -37,4 +37,4 @@ with their :attr:`~django.db.models.CharField.max_length` defaulting to 200. The syntax to use for the field; defaults to ``'simple'``. - .. versionaddedd:: 2.7 + .. versionadded:: 2.7 diff --git a/docs/guide.rst b/docs/guide.rst new file mode 100644 index 0000000..7780a32 --- /dev/null +++ b/docs/guide.rst @@ -0,0 +1,346 @@ +Guide +===== + +.. currentmodule:: semantic_version + +This module covers the 2.0.0 version of the SemVer scheme, with additional +extensions: + +- Coercing any version string into a SemVer version, through + :meth:`Version.coerce`; +- Comparing versions; +- Computing next versions; +- Modelling version range specifcations, and choosing the best match -- for both + its custom logic, and NPM semantics (custom range specification schemes can + be added). + + +Version basics +-------------- + +Building :class:`Version` instances +""""""""""""""""""""""""""""""""""" + +The core of the module is the :class:`Version` class; it is usually instantiated +from a version string: + +.. code-block:: pycon + + >>> import semantic_version as semver + >>> v = semver.Version("0.1.1") + +The version's components are available through its attributes: + +* :attr:`~Version.major`, :attr:`~Version.minor`, :attr:`~Version.patch` are + integers: + + .. code-block:: pycon + + >>> v.major + 0 + >>> v.minor + 1 + >>> v.patch + 1 + + +* The :attr:`~Version.prerelease` and :attr:`~Version.build` attributes are + iterables of text elements: + + .. code-block:: pycon + + >>> v2 = semver.Version("0.1.1-dev+23.git2") + >>> v2.prerelease + ["dev"] + >>> v2.build + ["23", "git2"] + + +One may also build a :class:`Version` from named components directly: + +.. code-block:: pycon + + >>> semantic_version.Version(major=0, minor=1, patch=2) + Version('0.1.2') + + +In that case, ``major``, ``minor`` and ``patch`` are mandatory, and must be integers. +``prerelease`` and ``build``, if provided, must be tuples of strings: + +.. code-block:: pycon + + >>> semantic_version.Version(major=0, minor=1, patch=2, prerelease=('alpha', '2')) + Version('0.1.2-alpha.2') + + +If the provided version string is invalid, a :exc:`ValueError` will be raised: + +.. code-block:: pycon + + >>> semver.Version('0.1') + Traceback (most recent call last): + File "", line 1, in + File "/Users/rbarrois/dev/semantic_version/src/semantic_version/base.py", line 64, in __init__ + major, minor, patch, prerelease, build = self.parse(version_string, partial) + File "/Users/rbarrois/dev/semantic_version/src/semantic_version/base.py", line 86, in parse + raise ValueError('Invalid version string: %r' % version_string) + ValueError: Invalid version string: '0.1' + + +Working with non-SemVer version strings +""""""""""""""""""""""""""""""""""""""" + +Some user-supplied input might not match the semantic version scheme. +For such cases, the ``Version.coerce`` method will try to convert any +version-like string into a valid semver version: + +.. code-block:: pycon + + >>> semver.Version.coerce('0') + Version('0.0.0') + >>> semver.Version.coerce('0.1.2.3.4') + Version('0.1.2+3.4') + >>> semver.Version.coerce('0.1.2a3') + Version('0.1.2-a3') + + +Comparing versions +"""""""""""""""""" + +Versions can be compared, following the SemVer scheme: + +.. code-block:: pycon + + >>> semver.Version("0.1.0") < semver.Version("0.1.1") + True + >>> max( + ... semver.Version("0.1.0"), + ... semver.Version("0.2.2"), + ... semver.Version("0.1.1"), + ... semver.Version("0.2.2-rc1"), + ... ) + Version("0.2.2") + + +.. note:: + + As defined in SemVer, build metadata is ignored in comparisons, + but not in equalities: + + .. code-block:: pycon + + >>> semver.Version("0.1.2") <= semver.Version("0.1.2+git2") + True + >>> semver.Version("0.1.2") >= semver.Version("0.1.2+git2") + True + >>> semver.Version("0.1.2") == semver.Version("0.1.2+git2") + False + + +Iterating versions +"""""""""""""""""" + +One can get a new version that represents a bump in one of the version levels +through the :meth:`Version.next_major`, :meth:`Version.next_minor` or +:meth:`Version.next_patch` functions: + +.. code-block:: pycon + + >>> v = semver.Version('0.1.1+build') + >>> new_v = v.next_major() + >>> str(new_v) + '1.0.0' + >>> v = semver.Version('1.1.1+build') + >>> new_v = v.next_minor() + >>> str(new_v) + '1.2.0' + >>> v = semver.Version('1.1.1+build') + >>> new_v = v.next_patch() + >>> str(new_v) + '1.1.2' + +.. note:: + + * If the version includes :attr:`~Version.build` or + :attr:`~Version.prerelease` metadata, that value will be empty in the + next version; + * The next patch following a version with a pre-release identifier + is the same version with its prerelease and build identifiers removed: + ``Version("0.1.1-rc1").next_patch() == Version("0.1.1")`` + * Pre-release and build naming schemes are often custom and specific + to a project's internal design; thus, the library can't provide a + ``next_xxx`` method for those fields. + +One may also truncate versions through the :meth:`Version.truncate` method, +removing components beyond the selected level: + +.. code-block:: pycon + + >>> v = semver.Version("0.1.2-dev+git3") + >>> v.truncate("prerelease") + Version("0.1.2-dev") + >>> v.truncate("minor") + Version("0.1.0") + + +Range specifications +-------------------- + +Comparing version numbers isn't always enough; in many situations, one needs to +define a *range of acceptable versions*. + +That notion is not defined in SemVer; moreover, several systems exists, with +their own notations. + +The ``semantic_version`` package provides a couple of implementations for these +notions: + +- :class:`SimpleSpec` is a simple implementation, with reasonable expectations; +- :class:`NpmSpec` sticks to the NPM specification. + +Further schemes can be built in a similar manner, relying on the :class:`BaseSpec` +class for basics. + +Core API +"""""""" + +The core API is provided by the :class:`BaseSpec` class. + +.. note:: + + These examples use :class:`SimpleSpec` in order to be easily reproduced + by users, but only exhibit the standard parts of the interface. + +It is possible to check whether a given :class:`Version` matches a +:class:`BaseSpec` through :meth:`~BaseSpec.match`: + +.. code-block:: pycon + + >>> s = semver.SimpleSpec(">=0.1.1") + >>> s.match(Version("0.1.1")) + True + >>> s.match(Version("0.1.0")) + False + +This feature is also available through the ``in`` keyword: + +.. code-block:: pycon + + >>> s = semver.SimpleSpec(">=0.1.1") + >>> Version("0.1.1") in s + True + >>> Version("0.1.0") in s + False + +A specification can filter compatible values from an iterable of versions +with :meth:`~BaseSpec.filter`: + +.. code-block:: pycon + + >>> s = semver.SimpleSpec(">=0.2.1") + >>> versions = [ + ... Version("0.1.0"), + ... Version("0.2.0"), + ... Version("0.3.0"), + ... Version("0.4.0"), + ... ] + >>> list(s.filter(versions)) + [Version("0.3.0"), Version("0.4.0")] + +It can also select the "best" version from such an iterable through +:meth:`~BaseSpec.select`: + +.. code-block:: pycon + + >>> s = semver.SimpleSpec(">=0.2.1") + >>> versions = [ + ... Version("0.1.0"), + ... Version("0.2.0"), + ... Version("0.3.0"), + ... Version("0.4.0"), + ... ] + >>> s.select(versions) + Version("0.4.0") + + +The :class:`SimpleSpec` scheme +"""""""""""""""""""""""""""""" + +The :class:`SimpleSpec` provides a hopefully intuitive version range +specification scheme: + +- A specification expression is composed of comma-separated clauses; +- Each clause can be: + + - An equality match (``==`` or ``!=``); + - A comparison (``>``, ``>=``, ``<`` , ``<=``); + - A compatible release clause, PyPI style (``~=2.2`` for ``>=2.2.0,<3.0.0``); + - An NPM style clause: + + - ``~1.2.3`` for ``>=1.2.3,<1.3.0``; + - ``^1.3.4`` for ``>=1.3.4,<2.0.0``; + +- The range in each clause may include a wildcard: + + * ``==0.1.*`` maps to ``>=0.1.0,<0.2.0``; + * ``==1.*`` or ``==1.*.*`` map to ``>=1.0.0,<2.0.0`` + + +.. rubric:: Special matching rules + +When testing a :class:`Version` against a :class:`SimpleSpec`, comparisons are +adjusted for common user expectations; thus, a pre-release version (``1.0.0-alpha``) +will not satisfy the ``==1.0.0`` :class:`SimpleSpec`. + +Pre-release identifiers will only be compared if included in the :class:`BaseSpec` +definition or (for the empty pre-release number) if a single dash is appended +(``1.0.0-``): + + +.. code-block:: pycon + + >>> Version('0.1.0-alpha') in SimpleSpec('<0.1.0') # No pre-release identifier + False + >>> Version('0.1.0-alpha') in SimpleSpec('<0.1.0-') # Include pre-release in checks + True + + +Build metadata has no ordering; thus, the only meaningful comparison including +build metadata is equality: + + +.. code-block:: pycon + + >>> Version('1.0.0+build2') in SimpleSpec('<=1.0.0') # Build metadata ignored + True + >>> Version('1.0.0+build1') in SimpleSpec('==1.0.0+build2') # Include build in checks + False + +.. note:: + + The full documentation is provided in the reference section + for the :class:`SimpleSpec` class. + + +The :class:`NpmSpec` scheme +""""""""""""""""""""""""""" + +The :class:`NpmSpec` class implements the full NPM specification (from +https://docs.npmjs.com/misc/semver.html): + +.. code-block:: pycon + + >>> semver.Version("0.1.2") in semver.NpmSpec("0.1.0-alpha.2 .. 0.2.4") + True + >>> semver.Version('0.1.2') in semver.NpmSpec('>=0.1.1 <0.1.3 || 2.x') + True + >>> semver.Version('2.3.4') in semver.NpmSpec('>=0.1.1 <0.1.3 || 2.x') + True + +Using with Django +----------------- + +The :mod:`semantic_version.django_fields` module provides django fields to +store :class:`Version` or :class:`BaseSpec` objects. + +More documentation is available in the :doc:`django` section. diff --git a/docs/index.rst b/docs/index.rst deleted file mode 120000 index 89a0106..0000000 --- a/docs/index.rst +++ /dev/null @@ -1 +0,0 @@ -../README.rst \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..135ee68 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,30 @@ +====================== +python-semanticversion +====================== + +.. include:: introduction.rst + + +Contents +======== + +.. toctree:: + :maxdepth: 2 + + introduction + guide + reference + django + changelog + credits + + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/introduction.rst b/docs/introduction.rst new file mode 120000 index 0000000..89a0106 --- /dev/null +++ b/docs/introduction.rst @@ -0,0 +1 @@ +../README.rst \ No newline at end of file diff --git a/docs/reference.rst b/docs/reference.rst index 951bd9a..93a29eb 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -66,6 +66,7 @@ Representing a version (the Version class) ------------------------------------------ .. class:: Version(version_string[, partial=False]) + :noindex: Object representation of a `SemVer`_-compliant version. @@ -90,15 +91,6 @@ Representing a version (the Version class) .. rubric:: Attributes - .. attribute:: partial - - ``bool``, whether this is a 'partial' or a complete version number. - Partial version number may lack :attr:`minor` or :attr:`patch` version numbers. - - .. deprecated:: 2.7 - The ability to define a partial version will be removed in version 3.0. - Use :class:`SimpleSpec` instead: ``SimpleSpec('1.x.x')``. - .. attribute:: major ``int``, the major version number @@ -140,6 +132,15 @@ Representing a version (the Version class) Note that the :attr:`~Version.build` isn't included in the precedence_key computatin. + .. attribute:: partial + + ``bool``, whether this is a 'partial' or a complete version number. + Partial version number may lack :attr:`minor` or :attr:`patch` version numbers. + + .. deprecated:: 2.7 + The ability to define a partial version will be removed in version 3.0. + Use :class:`SimpleSpec` instead: ``SimpleSpec('1.x.x')``. + .. rubric:: Methods @@ -173,7 +174,7 @@ Representing a version (the Version class) >>> Version('1.1.0-alpha').next_minor() Version('1.1.0') - .. method:: next_patch(self): + .. method:: next_patch(self) Return the next patch version, i.e the smallest version strictly greater than the current one with empty :attr:`prerelease` and :attr:`build`. @@ -231,6 +232,7 @@ Representing a version (the Version class) - For non-:attr:`partial` versions, compare using the `SemVer`_ scheme - If any compared object is :attr:`partial`: + - Begin comparison using the `SemVer`_ scheme - If a component (:attr:`minor`, :attr:`patch`, :attr:`prerelease` or :attr:`build`) was absent from the :attr:`partial` :class:`Version` -- represented with :obj:`None` @@ -472,7 +474,7 @@ Each of those ``Spec`` classes provides a shared set of methods to work with ver * A clause of ``>0.1.2-rc.3`` will match versions strictly above ``0.1.2-rc.3``, including matching prereleases of ``0.1.2``: ``0.1.2-rc.10`` is included; * A clause of ``>=XXX`` will match versions that match ``>XXX`` or ``==XXX`` - ..rubric:: Wildcards + .. rubric:: Wildcards * A clause of ``==0.1.*`` is equivalent to ``>=0.1.0,<0.2.0`` * A clause of ``>=0.1.*`` is equivalent to ``>=0.1.0`` @@ -554,7 +556,7 @@ Each of those ``Spec`` classes provides a shared set of methods to work with ver , )> - Its keeps a list of :class:`SpecItem` objects, based on the initial expression + It keeps a list of :class:`SpecItem` objects, based on the initial expression components. .. method:: __iter__(self) @@ -705,7 +707,13 @@ Each of those ``Spec`` classes provides a shared set of methods to work with ver >>> Version('1.0.1') in Spec('!=1.0.1') False - The kind of 'Almost equal to' specifications + .. data:: KIND_COMPATIBLE + + The kind of `compatible release clauses`_ + specifications:: + + >>> Version('1.1.2') in Spec('~=1.1.0') + True From 4fcc1475e0161bdf9fa3737023068afae15c62f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 6 Feb 2022 19:43:31 +0100 Subject: [PATCH 06/21] docs: Update CI location --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4735cce..8686366 100644 --- a/README.rst +++ b/README.rst @@ -29,7 +29,7 @@ Links - Package on `PyPI`_: http://pypi.python.org/pypi/semantic_version/ - Doc on `ReadTheDocs `_: https://python-semanticversion.readthedocs.io/ - Source on `GitHub `_: http://github.com/rbarrois/python-semanticversion/ -- Build on `Travis CI `_: http://travis-ci.org/rbarrois/python-semanticversion/ +- Build on Github Actions: https://github.com/rbarrois/python-semanticversion/actions - Semantic Version specification: `SemVer`_ From 2713cf34f37214f2b3acfa79e8909d0d03dcabac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 6 Feb 2022 19:44:18 +0100 Subject: [PATCH 07/21] doc: Remove remaining Sphinx markup from README --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 8686366..0170dee 100644 --- a/README.rst +++ b/README.rst @@ -266,9 +266,9 @@ When submitting patches or pull requests, you should respect the following rules - Coding conventions are based on :pep:`8` - The whole test suite must pass after adding the changes - The test coverage for a new feature must be 100% -- New features and methods should be documented in the :doc:`reference` section - and included in the :doc:`changelog` -- Include your name in the :ref:`contributors` section +- New features and methods should be documented in the ``reference`` section + and included in the ``changelog`` +- Include your name in the ``contributors`` section .. note:: All files should contain the following header:: From 5f2bf3539ea877540f52ac47d5317ee1b17ba761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 6 Feb 2022 19:45:11 +0100 Subject: [PATCH 08/21] Preparing release 2.9.0 --- ChangeLog | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 211be6b..55343d3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,7 @@ ChangeLog ========= -2.8.6 (unreleased) +2.9.0 (2022-02-06) ------------------ *New:* diff --git a/setup.cfg b/setup.cfg index e7fd5e9..7dfce30 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = semantic_version -version = 2.8.6.dev0 +version = 2.9.0 description = A library implementing the 'SemVer' scheme. long_description = file: README.rst # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data From c61278bd35f2059ad3c7fc196a4b06eae34f3b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 6 Feb 2022 19:45:25 +0100 Subject: [PATCH 09/21] Back to development: 2.9.1 --- ChangeLog | 6 ++++++ setup.cfg | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 55343d3..da05e60 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,12 @@ ChangeLog ========= +2.9.1 (unreleased) +------------------ + +- Nothing changed yet. + + 2.9.0 (2022-02-06) ------------------ diff --git a/setup.cfg b/setup.cfg index 7dfce30..4bac2c1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = semantic_version -version = 2.9.0 +version = 2.9.1.dev0 description = A library implementing the 'SemVer' scheme. long_description = file: README.rst # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data From 8a7162fc01b33964688a5be41df3865bceb875c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 6 Feb 2022 19:52:05 +0100 Subject: [PATCH 10/21] docs: Update reference to NPM range specification --- ChangeLog | 2 +- docs/guide.rst | 2 +- docs/reference.rst | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index da05e60..0d30c7e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -100,7 +100,7 @@ Backwards compatibility has been kept, but users should adjust their code for th * Allow creation of a ``Version`` directly from parsed components, as keyword arguments (``Version(major=1, minor=2, patch=3)``) * Add ``Version.truncate()`` to build a truncated copy of a ``Version`` - * Add ``NpmSpec(...)``, following strict NPM matching rules (https://docs.npmjs.com/misc/semver) + * Add ``NpmSpec(...)``, following strict NPM matching rules (https://github.com/npm/node-semver#ranges) * Add ``Spec.parse('xxx', syntax='')`` for simpler multi-syntax support * Add ``Version().precedence_key``, for use in ``sort(versions, key=lambda v: v.precedence_key)`` calls. The contents of this attribute is an implementation detail. diff --git a/docs/guide.rst b/docs/guide.rst index 7780a32..9a7c5cd 100644 --- a/docs/guide.rst +++ b/docs/guide.rst @@ -326,7 +326,7 @@ The :class:`NpmSpec` scheme """"""""""""""""""""""""""" The :class:`NpmSpec` class implements the full NPM specification (from -https://docs.npmjs.com/misc/semver.html): +https://github.com/npm/node-semver#ranges): .. code-block:: pycon diff --git a/docs/reference.rst b/docs/reference.rst index 93a29eb..6d1101b 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -336,7 +336,7 @@ In order to solve this problem, each `SemVer`_-based package management platform python-semanticversion provides a couple of implementations of those range definition syntaxes: - ``'simple'`` (through :class:`SimpleSpec`): A python-semanticversion specific syntax, which supports simple / intuitive patterns, and some NPM-inspired extensions; -- ``'npm'`` (through :class:`NpmSpec`): The NPM syntax, based on https://docs.npmjs.com/misc/semver.html +- ``'npm'`` (through :class:`NpmSpec`): The NPM syntax, based on https://github.com/npm/node-semver#ranges - More might be added in the future. Each of those ``Spec`` classes provides a shared set of methods to work with versions: @@ -518,7 +518,7 @@ Each of those ``Spec`` classes provides a shared set of methods to work with ver .. versionadded:: 2.7 - A NPM-compliant version matching engine, based on the https://docs.npmjs.com/misc/semver.html specification. + A NPM-compliant version matching engine, based on the https://github.com/npm/node-semver#ranges specification. .. code-block:: pycon From 47be07eb4a632850b28cc584b4caa54ed02cd924 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Wed, 17 Nov 2021 14:47:39 +0100 Subject: [PATCH 11/21] Update the Version.parse() to match the code The docstring was lying by saying a Version object was returned. Rather this function returns a tuple of version parts. Signed-off-by: Philippe Ombredanne --- semantic_version/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/semantic_version/base.py b/semantic_version/base.py index 871ccb0..82a9af0 100644 --- a/semantic_version/base.py +++ b/semantic_version/base.py @@ -290,7 +290,8 @@ def coerce(cls, version_string, partial=False): @classmethod def parse(cls, version_string, partial=False, coerce=False): - """Parse a version string into a Version() object. + """Parse a version string into a tuple of components: + (major, minor, patch, prerelease, build). Args: version_string (str), the version string to parse From 7dcc42d2a828adbbeb6f8a23cdca40a3c61782bc Mon Sep 17 00:00:00 2001 From: Alex Hogen Date: Wed, 9 Mar 2022 16:38:29 -0800 Subject: [PATCH 12/21] Fix pip install name in README --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 0170dee..c50163a 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ It follows strictly the 2.0.0 version of the SemVer scheme. Links ----- -- Package on `PyPI`_: http://pypi.python.org/pypi/semantic_version/ +- Package on `PyPI`_: https://pypi.org/project/semantic-version/ - Doc on `ReadTheDocs `_: https://python-semanticversion.readthedocs.io/ - Source on `GitHub `_: http://github.com/rbarrois/python-semanticversion/ - Build on Github Actions: https://github.com/rbarrois/python-semanticversion/actions @@ -40,7 +40,7 @@ Install the package from `PyPI`_, using pip: .. code-block:: sh - pip install semantic_version + pip install semantic-version Or from GitHub: From 57c78e7307792879dce33734c11e7774383b9d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 May 2022 15:28:23 +0200 Subject: [PATCH 13/21] Guarantee a stable ordering with build metadata Sorting any permutation of Version objects should always yield the same result, even if those hold some build metadata. To that end, the "precedence_key" is now used exclusively for sorting; direct comparisons between Version objects still ignores the "build" metadata, using a different precedence key. For performance improvements, both precedence keys are cached. Closes: #132 --- ChangeLog | 10 +++++++--- docs/reference.rst | 11 ++++++++++- semantic_version/base.py | 42 +++++++++++++++++++++++++++++++++------- tests/test_base.py | 14 ++++++++++++++ 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/ChangeLog b/ChangeLog index 0d30c7e..110b054 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,10 +1,14 @@ ChangeLog ========= -2.9.1 (unreleased) ------------------- +2.10.0 (unreleased) +------------------- + +*New:* -- Nothing changed yet. + * `132 `_: + Ensure sorting a collection of versions is always stable, even with + build metadata. 2.9.0 (2022-02-06) diff --git a/docs/reference.rst b/docs/reference.rst index 6d1101b..b2946d9 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -130,7 +130,16 @@ Representing a version (the Version class) The actual value of the attribute is considered an implementation detail; the only guarantee is that ordering versions by their precedence_key will comply with semver precedence rules. - Note that the :attr:`~Version.build` isn't included in the precedence_key computatin. + + .. warning:: + + .. versionchanged:: 2.10.0 + + The :attr:`~Version.build` is included in the precedence_key computation, but + only for ordering stability. + The only guarantee is that, for a given release of python-semanticversion, two versions' + :attr:`~Version.precedence_key` will always compare in the same direction if they include + build metadata; that ordering is an implementation detail and shouldn't be relied upon. .. attribute:: partial diff --git a/semantic_version/base.py b/semantic_version/base.py index 82a9af0..777c27a 100644 --- a/semantic_version/base.py +++ b/semantic_version/base.py @@ -118,6 +118,12 @@ def __init__( self.partial = partial + # Cached precedence keys + # _cmp_precedence_key is used for semver-precedence comparison + self._cmp_precedence_key = self._build_precedence_key(with_build=False) + # _sort_precedence_key is used for self.precedence_key, esp. for sorted(...) + self._sort_precedence_key = self._build_precedence_key(with_build=True) + @classmethod def _coerce(cls, value, allow_none=False): if value is None and allow_none: @@ -408,11 +414,15 @@ def __hash__(self): # at least a field being `None`. return hash((self.major, self.minor, self.patch, self.prerelease, self.build)) - @property - def precedence_key(self): + def _build_precedence_key(self, with_build=False): + """Build a precedence key. + + The "build" component should only be used when sorting an iterable + of versions. + """ if self.prerelease: prerelease_key = tuple( - NumericIdentifier(part) if re.match(r'^[0-9]+$', part) else AlphaIdentifier(part) + NumericIdentifier(part) if part.isdigit() else AlphaIdentifier(part) for part in self.prerelease ) else: @@ -420,13 +430,31 @@ def precedence_key(self): MaxIdentifier(), ) + if not with_build: + return ( + self.major, + self.minor, + self.patch, + prerelease_key, + ) + + build_key = tuple( + NumericIdentifier(part) if part.isdigit() else AlphaIdentifier(part) + for part in self.build or () + ) + return ( self.major, self.minor, self.patch, prerelease_key, + build_key, ) + @property + def precedence_key(self): + return self._sort_precedence_key + def __cmp__(self, other): if not isinstance(other, self.__class__): return NotImplemented @@ -458,22 +486,22 @@ def __ne__(self, other): def __lt__(self, other): if not isinstance(other, self.__class__): return NotImplemented - return self.precedence_key < other.precedence_key + return self._cmp_precedence_key < other._cmp_precedence_key def __le__(self, other): if not isinstance(other, self.__class__): return NotImplemented - return self.precedence_key <= other.precedence_key + return self._cmp_precedence_key <= other._cmp_precedence_key def __gt__(self, other): if not isinstance(other, self.__class__): return NotImplemented - return self.precedence_key > other.precedence_key + return self._cmp_precedence_key > other._cmp_precedence_key def __ge__(self, other): if not isinstance(other, self.__class__): return NotImplemented - return self.precedence_key >= other.precedence_key + return self._cmp_precedence_key >= other._cmp_precedence_key class SpecItem(object): diff --git a/tests/test_base.py b/tests/test_base.py index 4a844c3..e6a3733 100755 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -229,6 +229,20 @@ def test_invalid_comparisons(self): self.assertTrue(v != '0.1.0') self.assertFalse(v == '0.1.0') + def test_stable_ordering(self): + a = [ + base.Version('0.1.0'), + base.Version('0.1.0+a'), + base.Version('0.1.0+a.1'), + base.Version('0.1.1-a1'), + ] + b = [a[1], a[3], a[0], a[2]] + + self.assertEqual( + sorted(a, key=lambda v: v.precedence_key), + sorted(b, key=lambda v: v.precedence_key), + ) + def test_bump_clean_versions(self): # We Test each property explicitly as the == comparator for versions # does not distinguish between prerelease or builds for equality. From e49b5b065b845cd7798c0219e0fa8986c75f6a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 May 2022 15:35:14 +0200 Subject: [PATCH 14/21] Preparing release 2.10.0 --- ChangeLog | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 110b054..586ea8e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,7 @@ ChangeLog ========= -2.10.0 (unreleased) +2.10.0 (2022-05-26) ------------------- *New:* diff --git a/setup.cfg b/setup.cfg index 4bac2c1..7fcd9e3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = semantic_version -version = 2.9.1.dev0 +version = 2.10.0 description = A library implementing the 'SemVer' scheme. long_description = file: README.rst # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data From c2ec5114a7865c5fccd745329ecdc34d912d9b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 May 2022 15:35:31 +0200 Subject: [PATCH 15/21] Back to development: 2.10.1 --- ChangeLog | 6 ++++++ setup.cfg | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 586ea8e..d58715d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,12 @@ ChangeLog ========= +2.10.1 (unreleased) +------------------- + +- Nothing changed yet. + + 2.10.0 (2022-05-26) ------------------- diff --git a/setup.cfg b/setup.cfg index 7fcd9e3..3e38471 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = semantic_version -version = 2.10.0 +version = 2.10.1.dev0 description = A library implementing the 'SemVer' scheme. long_description = file: README.rst # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data From e00fb6da0b0a2cec8fdb8ca89ba78b086157f6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 28 Feb 2023 11:01:01 +0100 Subject: [PATCH 16/21] Test and fix Version.truncate() Calling `Version.truncate("build")` should return a fresh instance, as caught in #141 Closes: #141, #142 --- ChangeLog | 5 ++++- semantic_version/base.py | 9 ++++++++- tests/test_base.py | 10 ++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index d58715d..6c0f593 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,7 +4,10 @@ ChangeLog 2.10.1 (unreleased) ------------------- -- Nothing changed yet. +*Bugfix:* + + * `141 `_: + Ensure we return a new instance for ``Version.truncate("build")``. 2.10.0 (2022-05-26) diff --git a/semantic_version/base.py b/semantic_version/base.py index 777c27a..1c10155 100644 --- a/semantic_version/base.py +++ b/semantic_version/base.py @@ -181,7 +181,14 @@ def next_patch(self): def truncate(self, level='patch'): """Return a new Version object, truncated up to the selected level.""" if level == 'build': - return self + return Version( + major=self.major, + minor=self.minor, + patch=self.patch, + prerelease=self.prerelease, + build=self.build, + partial=self.partial, + ) elif level == 'prerelease': return Version( major=self.major, diff --git a/tests/test_base.py b/tests/test_base.py index e6a3733..73d1b08 100755 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -395,6 +395,16 @@ def test_bump_prerelease_versions(self): self.assertEqual(v.prerelease, ()) self.assertEqual(v.build, ()) + def test_truncate(self): + v = base.Version("3.2.1-pre+build") + self.assertEqual(v.truncate("build"), v) + self.assertIsNot(v.truncate("build"), v) + self.assertEqual(v.truncate("prerelease"), base.Version("3.2.1-pre")) + self.assertEqual(v.truncate("patch"), base.Version("3.2.1")) + self.assertEqual(v.truncate(), base.Version("3.2.1")) + self.assertEqual(v.truncate("minor"), base.Version("3.2.0")) + self.assertEqual(v.truncate("major"), base.Version("3.0.0")) + class SpecItemTestCase(unittest.TestCase): if sys.version_info[0] <= 2: From adaed0da56ac5d1fecb08d772cd1b5546044d793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 28 Feb 2023 11:12:52 +0100 Subject: [PATCH 17/21] Update the support matrix - Add Django 4.1 - Add Python 3.11 - Drop old versions of Python --- .github/workflows/test.yml | 34 ++++++---------------------------- setup.cfg | 1 + tox.ini | 12 ++++-------- 3 files changed, 11 insertions(+), 36 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3c9918b..2165bc7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,29 +13,7 @@ jobs: fail-fast: false matrix: include: - # Django 1.11: 3.5, 3.7 - - python-version: "2.7" - django-family: 111 - - python-version: "3.5" - django-family: 111 - - python-version: "3.7" - django-family: 111 - - python-version: "pypy3" - django-family: 111 - - # Django 2.2: 3.5, 3.7, 3.8 - - python-version: "3.5" - django-family: 22 - - python-version: "3.7" - django-family: 22 - - python-version: "3.8" - django-family: 22 - - python-version: "pypy3" - django-family: 22 - - # Django 3.2: Python 3.6, 3.8, 3.9, 3.10 - - python-version: "3.6" - django-family: 32 + # Django 3.2: Python 3.8, 3.9, 3.10 - python-version: "3.8" django-family: 32 - python-version: "3.9" @@ -45,13 +23,13 @@ jobs: - python-version: "pypy3" django-family: 32 - # Django 4.0: Python 3.8, 3.9, 3.10 - - python-version: "3.8" - django-family: 40 + # Django 4.1: Python 3.9, 3.10, 3.11 - python-version: "3.9" - django-family: 40 + django-family: 41 - python-version: "3.10" - django-family: 40 + django-family: 41 + - python-version: "3.11" + django-family: 41 env: TOXENV: django${{ matrix.django-family }} diff --git a/setup.cfg b/setup.cfg index 3e38471..46a2673 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,7 @@ classifiers = Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Topic :: Software Development :: Libraries :: Python Modules [options] diff --git a/tox.ini b/tox.ini index 26cbebe..77d6d1a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,8 @@ [tox] envlist = - py{27,34,35,36,37}-django111 - py{35,36,37,38}-django22 - py{36,37,38,39,310}-django32 - py{38,39,310}-django40 - pypy3-django{111,22,32} + py{37,38,39,310}-django32 + py{38,39,310,311}-django41 + pypy3-django{32} lint toxworkdir = {env:TOX_WORKDIR:.tox} @@ -12,10 +10,8 @@ toxworkdir = {env:TOX_WORKDIR:.tox} [testenv] extras = dev deps = - django111: Django>=1.11,<1.12 - django22: Django>=2.2,<2.3 django32: Django>=3.2,<3.3 - django40: Django>=4.0a1,<4.1 + django41: Django>=4.1,<4.2 whitelist_externals = make commands = make test From 61dc8432720b5345384d2b8dbd98df0748ff4eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 28 Feb 2023 11:16:35 +0100 Subject: [PATCH 18/21] Update github actions versions --- .github/workflows/check.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index dc16848..2fe3b5e 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,10 +19,10 @@ jobs: TOXENV: ${{ matrix.tox-environment }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 - name: Install dependencies run: python -m pip install tox diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2165bc7..f69954a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,10 +35,10 @@ jobs: TOXENV: django${{ matrix.django-family }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} From 26f77e3d8b4eab5c7bab58880afd990b61be897f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 28 Feb 2023 11:26:26 +0100 Subject: [PATCH 19/21] Update tox.ini for newer tox versions Keep compatibility with older versions' naming scheme as well. --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 77d6d1a..369cf12 100644 --- a/tox.ini +++ b/tox.ini @@ -13,11 +13,13 @@ deps = django32: Django>=3.2,<3.3 django41: Django>=4.1,<4.2 +allowlist_externals = make whitelist_externals = make commands = make test [testenv:lint] extras = dev +allowlist_externals = make whitelist_externals = make commands = make lint From 9acf55af9d6361a3eac576f9df076507165e8cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 28 Feb 2023 11:30:19 +0100 Subject: [PATCH 20/21] Remove pypy3 from Github Actions matrix --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f69954a..50eee10 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,8 +20,6 @@ jobs: django-family: 32 - python-version: "3.10" django-family: 32 - - python-version: "pypy3" - django-family: 32 # Django 4.1: Python 3.9, 3.10, 3.11 - python-version: "3.9" From 2cbbee3154d9011cee873ae3a020cd17c669f6df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 28 Feb 2023 11:51:59 +0100 Subject: [PATCH 21/21] Simplify subclassing Version Fixes: #112 --- ChangeLog | 6 ++++++ semantic_version/base.py | 24 ++++++++++++------------ tests/test_base.py | 9 +++++++++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6c0f593..2bf2e2c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,6 +4,12 @@ ChangeLog 2.10.1 (unreleased) ------------------- +*Minor:* + + * `112 `_: + Functions returning a new ``Version`` instance reuse the current class, + helping with subclassing. + *Bugfix:* * `141 `_: diff --git a/semantic_version/base.py b/semantic_version/base.py index 1c10155..6be5624 100644 --- a/semantic_version/base.py +++ b/semantic_version/base.py @@ -132,14 +132,14 @@ def _coerce(cls, value, allow_none=False): def next_major(self): if self.prerelease and self.minor == self.patch == 0: - return Version( + return self.__class__( major=self.major, minor=0, patch=0, partial=self.partial, ) else: - return Version( + return self.__class__( major=self.major + 1, minor=0, patch=0, @@ -148,14 +148,14 @@ def next_major(self): def next_minor(self): if self.prerelease and self.patch == 0: - return Version( + return self.__class__( major=self.major, minor=self.minor, patch=0, partial=self.partial, ) else: - return Version( + return self.__class__( major=self.major, minor=self.minor + 1, patch=0, @@ -164,14 +164,14 @@ def next_minor(self): def next_patch(self): if self.prerelease: - return Version( + return self.__class__( major=self.major, minor=self.minor, patch=self.patch, partial=self.partial, ) else: - return Version( + return self.__class__( major=self.major, minor=self.minor, patch=self.patch + 1, @@ -181,7 +181,7 @@ def next_patch(self): def truncate(self, level='patch'): """Return a new Version object, truncated up to the selected level.""" if level == 'build': - return Version( + return self.__class__( major=self.major, minor=self.minor, patch=self.patch, @@ -190,7 +190,7 @@ def truncate(self, level='patch'): partial=self.partial, ) elif level == 'prerelease': - return Version( + return self.__class__( major=self.major, minor=self.minor, patch=self.patch, @@ -198,21 +198,21 @@ def truncate(self, level='patch'): partial=self.partial, ) elif level == 'patch': - return Version( + return self.__class__( major=self.major, minor=self.minor, patch=self.patch, partial=self.partial, ) elif level == 'minor': - return Version( + return self.__class__( major=self.major, minor=self.minor, patch=None if self.partial else 0, partial=self.partial, ) elif level == 'major': - return Version( + return self.__class__( major=self.major, minor=None if self.partial else 0, patch=None if self.partial else 0, @@ -266,7 +266,7 @@ def coerce(cls, version_string, partial=False): ) if match.end() == len(version_string): - return Version(version, partial=partial) + return cls(version, partial=partial) rest = version_string[match.end():] diff --git a/tests/test_base.py b/tests/test_base.py index 73d1b08..4136045 100755 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -405,6 +405,15 @@ def test_truncate(self): self.assertEqual(v.truncate("minor"), base.Version("3.2.0")) self.assertEqual(v.truncate("major"), base.Version("3.0.0")) + def test_subclass(self): + """Custom subclasses of Version returns instances of themselves.""" + class MyVersion(base.Version): + pass + + v = MyVersion("3.2.1-pre") + subv = v.truncate() + self.assertEqual(type(subv), MyVersion) + class SpecItemTestCase(unittest.TestCase): if sys.version_info[0] <= 2: