From 7688b54c4a6005a75301a2e6a477f402311a6a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 24 Aug 2019 00:08:16 +0200 Subject: Add deprecations for Spec/SpecItem. The internal features from those classes will be removed in future versions: - The `Spec` class is incompatible with the support of multiple syntaxes - The `SpecItem` class was an implementation detail, but doesn't support complex `Range` combinations. --- ChangeLog | 6 ++ README.rst | 28 ++--- docs/django.rst | 2 +- docs/reference.rst | 249 ++++++++++++++++++++++++------------------- semantic_version/__init__.py | 2 +- semantic_version/base.py | 55 +++++++--- 6 files changed, 204 insertions(+), 138 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8ef6048..31ee8aa 100644 --- a/ChangeLog +++ b/ChangeLog @@ -10,11 +10,17 @@ ChangeLog (``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 ``Spec.parse('xxx', syntax='')`` for simpler multi-syntax support *Bugfix:* * Fix inconsistent behaviour regarding versions with a prerelease specification. +*Deprecated:* + + * Deprecate the ``Spec`` class (Removed in 3.1); use the ``SimpleSpec`` class instead + * Deprecate the internal ``SpecItem`` class (Removed in 3.0). + *Removed:* * Remove support for Python2 (End of life 4 months after this release) diff --git a/README.rst b/README.rst index 257b2a4..2adef1b 100644 --- a/README.rst +++ b/README.rst @@ -64,7 +64,7 @@ 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``): - - :class:`NativeSpec` describes a natural description syntax + - :class:`SimpleSpec` describes a natural description syntax - :class:`NpmSpec` is used for NPM-style range descriptions. Versions @@ -171,12 +171,12 @@ In that case, ``major``, ``minor`` and ``patch`` are mandatory, and must be inte Requirement specification ------------------------- -The :class:`NativeSpec` object describes a range of accepted versions: +The :class:`SimpleSpec` object describes a range of accepted versions: .. code-block:: pycon - >>> s = NativeSpec('>=0.1.1') # At least 0.1.1 + >>> s = SimpleSpec('>=0.1.1') # At least 0.1.1 >>> s.match(Version('0.1.1')) True >>> s.match(Version('0.1.1-alpha1')) # pre-release satisfy version spec @@ -188,7 +188,7 @@ Simpler test syntax is also available using the ``in`` keyword: .. code-block:: pycon - >>> s = NativeSpec('==0.1.1') + >>> s = SimpleSpec('==0.1.1') >>> Version('0.1.1-alpha1') in s True >>> Version('0.1.2') in s @@ -199,17 +199,17 @@ Combining specifications can be expressed as follows: .. code-block:: pycon - >>> NativeSpec('>=0.1.1,<0.3.0') + >>> SimpleSpec('>=0.1.1,<0.3.0') Using a specification """"""""""""""""""""" -The :func:`NativeSpec.filter` method filters an iterable of :class:`Version`: +The :func:`SimpleSpec.filter` method filters an iterable of :class:`Version`: .. code-block:: pycon - >>> s = NativeSpec('>=0.1.0,<0.4.0') + >>> s = SimpleSpec('>=0.1.0,<0.4.0') >>> versions = (Version('0.%d.0' % i) for i in range(6)) >>> for v in s.filter(versions): ... print v @@ -222,7 +222,7 @@ It is also possible to select the 'best' version from such iterables: .. code-block:: pycon - >>> s = NativeSpec('>=0.1.0,<0.4.0') + >>> s = SimpleSpec('>=0.1.0,<0.4.0') >>> versions = (Version('0.%d.0' % i) for i in range(6)) >>> s.select(versions) Version('0.3.0') @@ -248,9 +248,9 @@ version-like string into a valid semver version: Including pre-release identifiers in specifications """"""""""""""""""""""""""""""""""""""""""""""""""" -When testing a :class:`Version` against a :class:`NativeSpec`, comparisons are +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:`NativeSpec`. +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 @@ -259,9 +259,9 @@ definition or (for the empty pre-release number) if a single dash is appended .. code-block:: pycon - >>> Version('0.1.0-alpha') in NativeSpec('<0.1.0') # No pre-release identifier + >>> Version('0.1.0-alpha') in SimpleSpec('<0.1.0') # No pre-release identifier False - >>> Version('0.1.0-alpha') in NativeSpec('<0.1.0-') # Include pre-release in checks + >>> Version('0.1.0-alpha') in SimpleSpec('<0.1.0-') # Include pre-release in checks True @@ -274,9 +274,9 @@ build metadata is equality. .. code-block:: pycon - >>> Version('1.0.0+build2') in NativeSpec('<=1.0.0') # Build metadata ignored + >>> Version('1.0.0+build2') in SimpleSpec('<=1.0.0') # Build metadata ignored True - >>> Version('1.0.0+build1') in NativeSpec('==1.0.0+build2') # Include build in checks + >>> Version('1.0.0+build1') in SimpleSpec('==1.0.0+build2') # Include build in checks False diff --git a/docs/django.rst b/docs/django.rst index ab98e67..382b332 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -32,6 +32,6 @@ with their :attr:`~django.db.models.CharField.max_length` defaulting to 200. .. attribute:: syntax - The syntax to use for the field; defaults to ``'native'``. + The syntax to use for the field; defaults to ``'simple'``. .. versionaddedd:: 2.7 diff --git a/docs/reference.rst b/docs/reference.rst index feaffcf..62a2b3b 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -319,110 +319,21 @@ Version specifications (the Spec class) Version specifications describe a 'range' of accepted versions: older than, equal, similar to, … -The main issue with representing version specifications is that the usual syntax -does not map well onto `SemVer`_ precedence rules: +python-semanticversion supports different syntaxes for describing version range: -* A specification of ``<1.3.4`` is not expected to allow ``1.3.4-rc2``, but strict `SemVer`_ comparisons allow it ; - prereleases has the issue of excluding ``1.3.3+build3`` ; -* It may be necessary to exclude either all variations on a patch-level release - (``!=1.3.3``) or specifically one build-level release (``1.3.3+build.434``). +- ``'simple'`` (through :class:`SimpleSpec`): A python-semantic specific syntax, which supports common patterns, and some NPM-inspired extensions; +- ``'npm'`` (through :class:`NpmSpec`): The NPM syntax, based on https://docs.npmjs.com/misc/semver.html -In order to have version specification behave naturally, the rules are the following: +.. class:: BaseSpec(spec_string) -* If no pre-release number was included in the specification, versions with a pre-release - numbers are excluded from matching that specification. -* If no build metadata was included in the specification, build metadata is ignored - when deciding whether a version satisfies a specification. - -This means that:: - - >>> Version('1.1.1-rc1') in Spec('<1.1.1') - False - >>> Version('1.1.1-rc1') in Spec('<1.1.1-rc4') - True - >>> Version('1.1.1-rc1+build4') in Spec('<=1.1.1-rc1') - True - >>> Version('1.1.1-rc1+build4') in Spec('==1.1.1-rc1+build2') - False - - -.. note:: python-semanticversion also accepts ``"*"`` as a version spec, - that matches all (valid) version strings. - -.. note:: python-semanticversion supports PyPI-style `compatible release clauses`_: - - * ``~=2.2`` means "Any release between 2.2.0 and 3.0.0" - * ``~=1.4.5`` means "Any release between 1.4.5 and 1.5.0" - -.. note:: python-semanticversion includes support for NPM-style specs: - - * ``~1.2.3`` means "Any release between 1.2.3 and 1.3.0" - * ``^1.3.4`` means "Any release between 1.3.4 and 2.0.0" - -In order to force matches to *strictly* compare version numbers, these additional -rules apply: - -* Setting a pre-release separator without a pre-release identifier (``<=1.1.1-``) - forces match to take into account pre-release version:: - - >>> Version('1.1.1-rc1') in Spec('<1.1.1') - False - >>> Version('1.1.1-rc1') in Spec('<1.1.1-') - True - -* Setting a build metadata separator without build metadata (``<=1.1.1+``) - forces matches "up to the build metadata"; use this to include/exclude a - release lacking build metadata while excluding/including all other builds - of that release:: - - >>> Version('1.1.1') in Spec('==1.1.1+') - True - >>> Version('1.1.1+2') in Spec('==1.1.1+') - False - - -.. warning:: As stated in the `SemVer`_ specification, the ordering of build metadata is *undefined*. - Thus, a :class:`Spec` string can only mention build metadata to include or exclude a specific version: - - * ``==1.1.1+b1234`` includes this specific build - * ``!=1.1.1+b1234`` excludes it (but would match ``1.1.1+b1235`` - * ``<1.1.1+b1`` is invalid - - - -.. class:: Spec(spec_string[, spec_string[, ...]]) - - Stores a list of :class:`SpecItem` and matches any :class:`Version` against all - contained :class:`specs `. - - It is built from a comma-separated list of version specifications:: - - >>> Spec('>=1.0.0,<1.2.0,!=1.1.4') - = Version('1.0.0', partial=True)>, - , - - )> - - Version specifications may also be passed in separated arguments:: - - >>> Spec('>=1.0.0', '<1.2.0', '!=1.1.4,!=1.1.13') - = Version('1.0.0', partial=True)>, - , - , - , - )> + Converts an expression describing a range of versions into a set of clauses, + and matches any :class:`Version` against those clauses. .. rubric:: Attributes - - .. attribute:: specs - - Tuple of :class:`SpecItem`, the included specifications. - + This class has no public attributes. .. rubric:: Methods @@ -484,15 +395,6 @@ rules apply: >>> str(Spec('>=0.1.1,!=0.1.2')) '>=0.1.1,!=0.1.2' - .. method:: __iter__(self) - - Returns an iterator over the contained specs:: - - >>> for spec in Spec('>=0.1.1,!=0.1.2'): - ... print spec - >=0.1.1 - !=0.1.2 - .. method:: __hash__(self) Provides a hash based solely on the hash of contained specs. @@ -503,18 +405,99 @@ rules apply: .. rubric:: Class methods - .. classmethod:: parse(self, specs_string) + .. classmethod:: parse(self, expression, syntax='simple') - Retrieve a ``(*specs)`` tuple from a string. + Retrieve a :class:`BaseSpec` object tuple from a string. :param str requirement_string: The textual description of the specifications + :param str syntax: The identifier of the syntax to use for parsing :raises: :exc:`ValueError`: if the ``requirement_string`` is invalid. - :rtype: ``(*spec)`` tuple + :rtype: :class:`BaseSpec` subclass + + .. versionchanged:: 2.7 + This method used to return a tuple of :class:`SpecItem` objects. + + +.. class:: SimpleSpec(spec_string) + + .. versionadded:: 2.7 + Previously reachable through :class:`Spec`. + + Applies the python-semanticversion range specification: + + The main issue with representing version specifications is that the usual syntax + does not map well onto `SemVer`_ precedence rules: + + * A specification of ``<1.3.4`` is not expected to allow ``1.3.4-rc2``, but strict `SemVer`_ comparisons allow it ; + prereleases has the issue of excluding ``1.3.3+build3`` ; + * It may be necessary to exclude either all variations on a patch-level release + (``!=1.3.3``) or specifically one build-level release (``1.3.3+build.434``). + + + In order to have version specification behave naturally, the rules are the following: + * If no pre-release number was included in the specification, versions with a pre-release + numbers are excluded from matching that specification. + * If no build metadata was included in the specification, build metadata is ignored + when deciding whether a version satisfies a specification. + + This means that:: + + >>> Version('1.1.1-rc1') in Spec('<1.1.1') + False + >>> Version('1.1.1-rc1') in Spec('<1.1.1-rc4') + True + >>> Version('1.1.1-rc1+build4') in Spec('<=1.1.1-rc1') + True + >>> Version('1.1.1-rc1+build4') in Spec('==1.1.1-rc1+build2') + False + + + .. note:: python-semanticversion also accepts ``"*"`` as a version spec, + that matches all (valid) version strings. + + .. note:: python-semanticversion supports PyPI-style `compatible release clauses`_: + + * ``~=2.2`` means "Any release between 2.2.0 and 3.0.0" + * ``~=1.4.5`` means "Any release between 1.4.5 and 1.5.0" + + .. note:: python-semanticversion includes support for NPM-style specs: + + * ``~1.2.3`` means "Any release between 1.2.3 and 1.3.0" + * ``^1.3.4`` means "Any release between 1.3.4 and 2.0.0" + + In order to force matches to *strictly* compare version numbers, these additional + rules apply: + + * Setting a pre-release separator without a pre-release identifier (``<=1.1.1-``) + forces match to take into account pre-release version:: + + >>> Version('1.1.1-rc1') in Spec('<1.1.1') + False + >>> Version('1.1.1-rc1') in Spec('<1.1.1-') + True + + * Setting a build metadata separator without build metadata (``<=1.1.1+``) + forces matches "up to the build metadata"; use this to include/exclude a + release lacking build metadata while excluding/including all other builds + of that release:: + + >>> Version('1.1.1') in Spec('==1.1.1+') + True + >>> Version('1.1.1+2') in Spec('==1.1.1+') + False + + + .. warning:: As stated in the `SemVer`_ specification, the ordering of build metadata is *undefined*. + Thus, a :class:`Spec` string can only mention build metadata to include or exclude a specific version: + + * ``==1.1.1+b1234`` includes this specific build + * ``!=1.1.1+b1234`` excludes it (but would match ``1.1.1+b1235`` + * ``<1.1.1+b1`` is invalid .. class:: NpmSpec(spec_string) - .. versionadded:: 2.8 + .. versionadded:: 2.7 A NPM-compliant version matching engine, based on the https://docs.npmjs.com/misc/semver.html specification. @@ -528,8 +511,56 @@ rules apply: True +.. class:: Spec(spec_string) + + .. deprecated:: 2.7 + The alias from :class:`Spec` to :class:`SimpleSpec` will be removed in 3.1. + + Alias to :class:`LegacySpec`, for backwards compatibility. + + +.. class:: LegacySpec(spec_string) + + .. deprecated:: 2.7 + The :class:`LegacySpec` class will be removed in 3.0; use :class:`SimpleSpec` instead. + + A :class:`LegacySpec` class has the exact same behaviour as :class:`SimpleSpec`, with + backwards-compatible features: + + It accepts version specifications passed in as separated arguments:: + + >>> Spec('>=1.0.0', '<1.2.0', '!=1.1.4,!=1.1.13') + = Version('1.0.0', partial=True)>, + , + , + , + )> + + Its keeps a list of :class:`SpecItem` objects, based on the initial expression + components. + + .. method:: __iter__(self) + + Returns an iterator over the contained specs:: + + >>> for spec in Spec('>=0.1.1,!=0.1.2'): + ... print spec + >=0.1.1 + !=0.1.2 + + .. rubric:: Attributes + + .. attribute:: specs + + Tuple of :class:`SpecItem`, the included specifications. + + .. class:: SpecItem(spec_string) + .. deprecated:: 2.7 + This class will be removed in 3.0. + .. note:: This class belong to the private python-semanticversion API. Stores a version specification, defined from a string:: diff --git a/semantic_version/__init__.py b/semantic_version/__init__.py index e04e0ba..6092e59 100644 --- a/semantic_version/__init__.py +++ b/semantic_version/__init__.py @@ -3,7 +3,7 @@ # This code is distributed under the two-clause BSD License. -from .base import compare, match, validate, NativeSpec, NpmSpec, Spec, SpecItem, Version +from .base import compare, match, validate, SimpleSpec, NpmSpec, Spec, SpecItem, Version __author__ = "Raphaël Barrois " diff --git a/semantic_version/base.py b/semantic_version/base.py index 9b517ca..2b1a9b3 100644 --- a/semantic_version/base.py +++ b/semantic_version/base.py @@ -4,6 +4,7 @@ import functools import re +import warnings def _to_int(value): @@ -534,7 +535,13 @@ class SpecItem: re_spec = re.compile(r'^(<|<=||=|==|>=|>|!=|\^|~|~=)(\d.*)$') - def __init__(self, requirement_string): + def __init__(self, requirement_string, _warn=True): + if _warn: + warnings.warn( + "The `SpecItem` class will be removed in 3.0.", + DeprecationWarning, + stacklevel=2, + ) kind, spec = self.parse(requirement_string) self.kind = kind self.spec = spec @@ -568,11 +575,11 @@ class SpecItem: @classmethod def from_matcher(cls, matcher): if matcher == Always(): - return cls('*') + return cls('*', _warn=False) elif matcher == Never(): - return cls('<0.0.0-') + return cls('<0.0.0-', _warn=False) elif isinstance(matcher, Range): - return cls('%s%s' % (matcher.operator, matcher.target)) + return cls('%s%s' % (matcher.operator, matcher.target), _warn=False) def match(self, version): return self._clause.match(version) @@ -1002,18 +1009,10 @@ class Range(Matcher): @BaseSpec.register_syntax -class Spec(BaseSpec): +class SimpleSpec(BaseSpec): SYNTAX = 'simple' - def __init__(self, expression, *legacy_extras): - expression = ','.join((expression,) + legacy_extras) - super().__init__(expression) - - def __iter__(self): - for clause in self.clause: - yield SpecItem.from_matcher(clause) - @classmethod def _parse_to_clause(cls, expression): return cls.Parser.parse(expression) @@ -1183,6 +1182,36 @@ class Spec(BaseSpec): return Range(Range.OP_LTE, target) +class LegacySpec(SimpleSpec): + def __init__(self, *expressions): + warnings.warn( + "The Spec() class will be removed in 3.1; use SimpleSpec() instead.", + PendingDeprecationWarning, + stacklevel=2, + ) + + if len(expressions) > 1: + warnings.warn( + "Passing 2+ arguments to SimpleSpec will be removed in 3.0; concatenate them with ',' instead.", + DeprecationWarning, + stacklevel=2, + ) + expression = ','.join(expressions) + super().__init__(expression) + + def __iter__(self): + warnings.warn( + "Iterating over the components of a SimpleSpec object will be removed in 3.0.", + DeprecationWarning, + stacklevel=2, + ) + for clause in self.clause: + yield SpecItem.from_matcher(clause) + + +Spec = LegacySpec + + @BaseSpec.register_syntax class NpmSpec(BaseSpec): SYNTAX = 'npm' -- cgit v1.2.1