Remove SpecItem.

Signed-off-by: Raphaël Barrois <raphael.barrois@polytechnique.org>
This commit is contained in:
Raphaël Barrois
2012-05-21 21:35:56 +02:00
parent 8b6a0e42d9
commit caa1c01858
11 changed files with 192 additions and 262 deletions

3
README
View File

@@ -40,8 +40,7 @@ Define a simple specification::
Define complex specifications::
>>> from semantic_version import SpecList
>>> s = SpecList('>=0.1.1,<0.2.0')
>>> s = Spec('>=0.1.1,<0.2.0')
>>> Version('0.1.2') in s
True
>>> Version('0.3.0') in s

View File

@@ -1,14 +1,15 @@
ChangeLog
=========
1.3.0 (Master)
2.0.0 (Master)
--------------
*Backwards incompatible changes:*
* Removed "loose" specification support
* Cleanup :class:`~semantic_version.Spec` to be more intuitive.
* Rename Spec to SpecItem and SpecList to Spec.
* Merge Spec and SpecList into :class:`~semantic_version.Spec`.
* Remove :class:`~semantic_version.django_fields.SpecListField`
1.2.0 (18/05/2012)
------------------

View File

@@ -3,10 +3,9 @@ Interaction with Django
.. module:: semantic_version.django_fields
The ``python-semanticversion`` package provides three custom fields for Django:
The ``python-semanticversion`` package provides two custom fields for Django:
- :class:`VersionField`: stores a :class:`semantic_version.Version` object
- :class:`SpecItemField`: stores a :class:`semantic_version.SpecItem` object
- :class:`SpecField`: stores a :class:`semantic_version.Spec` object
Those fields are :class:`django.db.models.CharField` subclasses,
@@ -22,11 +21,6 @@ with their :attr:`~django.db.models.CharField.max_length` defaulting to 200.
Boolean; whether :attr:`~semantic_version.Version.partial` versions are allowed.
.. class:: SpecItemField
Stores a :class:`semantic_version.SpecItem` as its string representation.
.. class:: SpecField
Stores a :class:`semantic_version.Spec` as its comma-separated string representation.

View File

@@ -82,9 +82,9 @@ Obviously, :class:`Versions <Version>` can be compared::
Requirement specification
-------------------------
The :class:`SpecItem` object describes a range of accepted versions::
The :class:`Spec` object describes a range of accepted versions::
>>> s = SpecItem('>=0.1.1') # At least 0.1.1
>>> s = Spec('>=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
@@ -94,61 +94,55 @@ The :class:`SpecItem` object describes a range of accepted versions::
Simpler test syntax is also available using the ``in`` keyword::
>>> s = SpecItem('==0.1.1')
>>> s = Spec('==0.1.1')
>>> Version('0.1.1-alpha1') in s
True
>>> Version('0.1.2') in s
False
Combining specifications can be expressed in two ways:
- Components separated by commas in a single string::
>>> Spec('>=0.1.1,<0.3.0')
- Components given as different arguments::
>>> Spec('>=0.1.1', '<0.3.0')
- A mix of both versions::
>>> Spec('>=0.1.1', '!=0.2.4-alpha,<0.3.0')
Including pre-release identifiers in specifications
"""""""""""""""""""""""""""""""""""""""""""""""""""
When testing a :class:`Version` against a :class:`SpecItem`, comparisons are only
performed for components defined in the :class:`SpecItem`; thus, a pre-release
When testing a :class:`Version` against a :class:`Spec`, comparisons are only
performed for components defined in the :class:`Spec`; thus, a pre-release
version (``1.0.0-alpha``), while not strictly equal to the non pre-release
version (``1.0.0``), satisfies the ``==1.0.0`` :class:`SpecItem`.
version (``1.0.0``), satisfies the ``==1.0.0`` :class:`Spec`.
Pre-release identifiers will only be compared if included in the :class:`SpecItem`
Pre-release identifiers will only be compared if included in the :class:`Spec`
definition or (for the empty pre-release number) if a single dash is appended
(``1.0.0-``)::
>>> Version('0.1.0-alpha') in SpecItem('>=0.1.0') # No pre-release identifier
>>> Version('0.1.0-alpha') in Spec('>=0.1.0') # No pre-release identifier
True
>>> Version('0.1.0-alpha') in SpecItem('>=0.1.0-') # Include pre-release in checks
>>> Version('0.1.0-alpha') in Spec('>=0.1.0-') # Include pre-release in checks
False
Including build identifiers in specifications
"""""""""""""""""""""""""""""""""""""""""""""
The same rule applies for the build identifier: comparisons will include it only
if it was included in the :class:`SpecItem` definition, or - for the unnumbered build
if it was included in the :class:`Spec` definition, or - for the unnumbered build
version - if a single + is appended to the definition(``1.0.0+``, ``1.0.0-alpha+``)::
>>> Version('1.0.0+build2') in SpecItem('<=1.0.0') # Build identifier ignored
>>> Version('1.0.0+build2') in Spec('<=1.0.0') # Build identifier ignored
True
>>> Version('1.0.0+build2') in SpecItem('<=1.0.0+') # Include build in checks
False
Combining requirements
======================
In order to express complex version specifications, use the :class:`Spec` class::
>>> # At least 0.1.1, not 0.2.0, avoid broken 0.1.5-alpha.
>>> sl = Spec('>=0.1.1,<0.2.0,!=0.1.5-alpha')
>>> sl.match(Version('0.1.1'))
True
>>> Version('0.1.1-rc1') in sl
True
>>> Version('0.1.2') in sl
True
>>> Version('0.2.0-alpha') in sl
False
>>> Version('0.1.5-alpha') in sl
False
>>> Version('0.1.5-alpha+build2') in sl
>>> Version('1.0.0+build2') in Spec('<=1.0.0+') # Include build in checks
False
@@ -156,7 +150,7 @@ Using with Django
=================
The :mod:`semantic_version.django_fields` module provides django fields to
store :class:`Version`, :class:`SpecItem` or :class:`Spec` objects.
store :class:`Version` or :class:`Spec` objects.
More documentation is available in the :doc:`django` section.

View File

@@ -189,7 +189,7 @@ Representing a version (the Version class)
:rtype: (major, minor, patch, prerelease, build)
Version specifications (the SpecItem class)
Version specifications (the Spec class)
-------------------------------------------
@@ -215,13 +215,13 @@ In order to have version specification behave naturally, the rules are the follo
This means that::
>>> Version('1.1.1-rc1') in SpecItem('<1.1.1')
>>> Version('1.1.1-rc1') in Spec('<1.1.1')
False
>>> Version('1.1.1-rc1') in SpecItem('<1.1.1-rc4')
>>> Version('1.1.1-rc1') in Spec('<1.1.1-rc4')
True
>>> Version('1.1.1-rc1+build4') in SpecItem('<=1.1.1-rc1')
>>> Version('1.1.1-rc1+build4') in Spec('<=1.1.1-rc1')
True
>>> Version('1.1.1-rc1+build4') in SpecItem('<=1.1.1-rc1+build2')
>>> Version('1.1.1-rc1+build4') in Spec('<=1.1.1-rc1+build2')
False
In order to force matches to *strictly* compare version numbers, these additional
@@ -230,169 +230,19 @@ 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 SpecItem('<1.1.1')
>>> Version('1.1.1-rc1') in Spec('<1.1.1')
False
>>> Version('1.1.1-rc1') in SpecItem('<1.1.1-')
>>> Version('1.1.1-rc1') in Spec('<1.1.1-')
True
* Setting a build separator without a build identifier (``>1.1.1+``) forces
satisfaction tests to include both prerelease and build identifiers::
>>> Version('1.1.1+build2') in SpecItem('>1.1.1')
>>> Version('1.1.1+build2') in Spec('>1.1.1')
False
>>> Version('1.1.1+build2') in SpecItem('>1.1.1+')
>>> Version('1.1.1+build2') in Spec('>1.1.1+')
True
.. class:: SpecItem(spec_string)
Stores a version specification, defined from a string::
>>> SpecItem('>=0.1.1')
<SpecItem: >= <SemVer(0, 1, 1, [], [])>>
This allows to test :class:`Version` objects against the :class:`SpecItem`::
>>> SpecItem('>=0.1.1').match(Version('0.1.1-rc1')) # pre-release satisfy conditions
True
>>> Version('0.1.1+build2') in SpecItem('>=0.1.1') # build version satisfy specifications
True
>>>
>>> # Use the '-' marker to include the pre-release component in checks
>>> SpecItem('>=0.1.1-').match(Version('0.1.1-rc1')
False
>>>
>>> # Use the '+' marker to include the build identifier in checks
>>> SpecItem('<=0.1.1-alpha+').match(Version('0.1.1-alpha+build1'))
False
.. rubric:: Attributes
.. attribute:: kind
One of :data:`KIND_LT`, :data:`KIND_LTE`, :data:`KIND_EQUAL`, :data:`KIND_GTE`,
:data:`KIND_GT` and :data:`KIND_NEQ`.
.. attribute:: spec
:class:`Version` in the :class:`SpecItem` description.
It is alway a :attr:`~Version.partial` :class:`Version`.
.. rubric:: Class methods
.. classmethod:: parse(cls, requirement_string)
Retrieve a ``(kind, version)`` tuple from a string.
:param str requirement_string: The textual description of the specification
:raises: :exc:`ValueError`: if the ``requirement_string`` is invalid.
:rtype: (``kind``, ``version``) tuple
.. rubric:: Methods
.. method:: match(self, version)
Test whether a given :class:`Version` matches this :class:`SpecItem`::
>>> SpecItem('>=0.1.1').match(Version('0.1.1-alpha'))
True
>>> SpecItem('>=0.1.1-').match(Version('0.1.1-alpha'))
False
:param version: The version to test against the spec
:type version: :class:`Version`
:rtype: ``bool``
.. method:: __contains__(self, version)
Alias of the :func:`match` method;
allows the use of the ``version in spec`` syntax::
>>> Version('1.1.1') in SpecItem('<=1.1.2')
True
.. method:: __str__(self)
Converting a :class:`SpecItem` to a string returns the initial description string::
>>> str(SpecItem('>=0.1.1'))
'>=0.1.1'
.. method:: __hash__(self)
Provides a hash based solely on the current kind and the specified version.
Allows using a :class:`SpecItem` as a dictionary key.
.. rubric:: Class attributes
.. data:: KIND_LT
The kind of 'Less than' specifications::
>>> Version('1.0.0-alpha') in SpecItem('<1.0.0')
False
.. data:: KIND_LTE
The kind of 'Less or equal to' specifications::
>>> Version('1.0.0-alpha1+build999') in SpecItem('<=1.0.0-alpha1')
True
.. data:: KIND_EQUAL
The kind of 'equal to' specifications::
>>> Version('1.0.0+build3.3') in SpecItem('==1.0.0')
True
.. data:: KIND_GTE
The kind of 'Greater or equal to' specifications::
>>> Version('1.0.0') in SpecItem('>=1.0.0')
True
.. data:: KIND_GT
The kind of 'Greater than' specifications::
>>> Version('1.0.0+build667') in SpecItem('>1.0.1')
False
.. data:: KIND_NEQ
The kind of 'Not equal to' specifications::
>>> Version('1.0.1') in SpecItem('!=1.0.1')
False
The kind of 'Almost equal to' specifications
Combining version specifications (the Spec class)
-------------------------------------------------
It may be useful to define a rule such as
"Accept any version between the first 1.0.0 (incl. pre-release) and strictly before 1.2.0; ecluding 1.1.4 which was broken.".
This is possible with the :class:`Spec` class.
.. class:: Spec(spec_string[, spec_string[, ...]])
Stores a list of :class:`SpecItem` and matches any :class:`Version` against all
@@ -484,4 +334,137 @@ This is possible with the :class:`Spec` class.
:rtype: ``(*spec)`` tuple
.. class:: SpecItem(spec_string)
.. note:: This class belong to the private python-semanticversion API.
Stores a version specification, defined from a string::
>>> SpecItem('>=0.1.1')
<SpecItem: >= <SemVer(0, 1, 1, [], [])>>
This allows to test :class:`Version` objects against the :class:`SpecItem`::
>>> SpecItem('>=0.1.1').match(Version('0.1.1-rc1')) # pre-release satisfy conditions
True
>>> Version('0.1.1+build2') in SpecItem('>=0.1.1') # build version satisfy specifications
True
>>>
>>> # Use the '-' marker to include the pre-release component in checks
>>> SpecItem('>=0.1.1-').match(Version('0.1.1-rc1')
False
>>>
>>> # Use the '+' marker to include the build identifier in checks
>>> SpecItem('<=0.1.1-alpha+').match(Version('0.1.1-alpha+build1'))
False
.. rubric:: Attributes
.. attribute:: kind
One of :data:`KIND_LT`, :data:`KIND_LTE`, :data:`KIND_EQUAL`, :data:`KIND_GTE`,
:data:`KIND_GT` and :data:`KIND_NEQ`.
.. attribute:: spec
:class:`Version` in the :class:`SpecItem` description.
It is alway a :attr:`~Version.partial` :class:`Version`.
.. rubric:: Class methods
.. classmethod:: parse(cls, requirement_string)
Retrieve a ``(kind, version)`` tuple from a string.
:param str requirement_string: The textual description of the specification
:raises: :exc:`ValueError`: if the ``requirement_string`` is invalid.
:rtype: (``kind``, ``version``) tuple
.. rubric:: Methods
.. method:: match(self, version)
Test whether a given :class:`Version` matches this :class:`SpecItem`::
>>> SpecItem('>=0.1.1').match(Version('0.1.1-alpha'))
True
>>> SpecItem('>=0.1.1-').match(Version('0.1.1-alpha'))
False
:param version: The version to test against the spec
:type version: :class:`Version`
:rtype: ``bool``
.. method:: __str__(self)
Converting a :class:`SpecItem` to a string returns the initial description string::
>>> str(SpecItem('>=0.1.1'))
'>=0.1.1'
.. method:: __hash__(self)
Provides a hash based solely on the current kind and the specified version.
Allows using a :class:`SpecItem` as a dictionary key.
.. rubric:: Class attributes
.. data:: KIND_LT
The kind of 'Less than' specifications::
>>> Version('1.0.0-alpha') in SpecItem('<1.0.0')
False
.. data:: KIND_LTE
The kind of 'Less or equal to' specifications::
>>> Version('1.0.0-alpha1+build999') in SpecItem('<=1.0.0-alpha1')
True
.. data:: KIND_EQUAL
The kind of 'equal to' specifications::
>>> Version('1.0.0+build3.3') in SpecItem('==1.0.0')
True
.. data:: KIND_GTE
The kind of 'Greater or equal to' specifications::
>>> Version('1.0.0') in SpecItem('>=1.0.0')
True
.. data:: KIND_GT
The kind of 'Greater than' specifications::
>>> Version('1.0.0+build667') in SpecItem('>1.0.1')
False
.. data:: KIND_NEQ
The kind of 'Not equal to' specifications::
>>> Version('1.0.1') in SpecItem('!=1.0.1')
False
The kind of 'Almost equal to' specifications
.. _SemVer: http://semver.org/

View File

@@ -5,4 +5,4 @@
__version__ = '1.3.0-alpha'
from .base import compare, match, Version, Spec, SpecItem
from .base import compare, match, Version, Spec

View File

@@ -88,16 +88,8 @@ class Version(object):
major, minor, patch, prerelease, build = match.groups()
major = int(major)
if minor is None:
return (major, None, None, None, None)
else:
minor = int(minor)
if patch is None:
return (major, minor, None, None, None)
else:
patch = int(patch)
minor = int(minor)
patch = int(patch)
if prerelease is None:
if partial and (build is None):
@@ -290,11 +282,6 @@ class SpecItem(object):
else: # pragma: no cover
raise ValueError('Unexpected match kind: %r' % self.kind)
def __contains__(self, version):
if isinstance(version, Version):
return self.match(version)
return False
def __str__(self):
return '%s%s' % (self.kind, self.spec)

View File

@@ -46,21 +46,6 @@ class VersionField(BaseSemVerField):
return base.Version(value, partial=self.partial)
class SpecItemField(BaseSemVerField):
default_error_messages = {
'invalid': _(u"Enter a valid version number spec in ==X.Y.Z format."),
}
description = _(u"Version specification")
def to_python(self, value):
"""Converts any value to a base.SpecItem field."""
if value is None or value == '':
return value
if isinstance(value, base.SpecItem):
return value
return base.SpecItem(value)
class SpecField(BaseSemVerField):
default_error_messages = {
'invalid': _(u"Enter a valid version number spec list in ==X.Y.Z,>=A.B.C format."),

View File

@@ -7,13 +7,10 @@ from semantic_version import django_fields as semver_fields
class VersionModel(models.Model):
version = semver_fields.VersionField(verbose_name='my version')
spec = semver_fields.SpecItemField(verbose_name='my spec')
speclist = semver_fields.SpecField(verbose_name='my spec list')
spec = semver_fields.SpecField(verbose_name='my spec')
class PartialVersionModel(models.Model):
partial = semver_fields.VersionField(partial=True, verbose_name='partial version')
optional = semver_fields.VersionField(verbose_name='optional version', blank=True, null=True)
optional_spec = semver_fields.SpecItemField(verbose_name='optional spec', blank=True, null=True)
optional_speclist = semver_fields.SpecField(verbose_name='optional spec list',
blank=True, null=True)
optional_spec = semver_fields.SpecField(verbose_name='optional spec', blank=True, null=True)

View File

@@ -249,13 +249,10 @@ class SpecItemTestCase(unittest.TestCase):
for version_text in matching:
version = base.Version(version_text)
self.assertTrue(version in spec, "%r should be in %r" % (version, spec))
self.assertTrue(spec.match(version), "%r should match %r" % (version, spec))
for version_text in failing:
version = base.Version(version_text)
self.assertFalse(version in spec,
"%r should not be in %r" % (version, spec))
self.assertFalse(spec.match(version),
"%r should not match %r" % (version, spec))

View File

@@ -35,28 +35,25 @@ if django_loaded: # pragma: no cover
@unittest.skipIf(not django_loaded, "Django not installed")
class DjangoFieldTestCase(unittest.TestCase):
def test_version(self):
obj = models.VersionModel(version='0.1.1', spec='>0.1.0', speclist='==0.1.1,!=0.1.1-alpha')
obj = models.VersionModel(version='0.1.1', spec='==0.1.1,!=0.1.1-alpha')
self.assertEqual(semantic_version.Version('0.1.1'), obj.version)
self.assertEqual(semantic_version.SpecItem('>0.1.0'), obj.spec)
self.assertEqual(semantic_version.Spec('==0.1.1,!=0.1.1-alpha'), obj.speclist)
self.assertEqual(semantic_version.Spec('==0.1.1,!=0.1.1-alpha'), obj.spec)
alt_obj = models.VersionModel(version=obj.version, spec=obj.spec, speclist=obj.speclist)
alt_obj = models.VersionModel(version=obj.version, spec=obj.spec)
self.assertEqual(semantic_version.Version('0.1.1'), alt_obj.version)
self.assertEqual(semantic_version.SpecItem('>0.1.0'), alt_obj.spec)
self.assertEqual(semantic_version.Spec('==0.1.1,!=0.1.1-alpha'), alt_obj.speclist)
self.assertEqual(semantic_version.Spec('==0.1.1,!=0.1.1-alpha'), alt_obj.spec)
self.assertEqual(obj.spec, alt_obj.spec)
self.assertEqual(obj.version, alt_obj.version)
self.assertEqual(obj.speclist, alt_obj.speclist)
def test_invalid_input(self):
self.assertRaises(ValueError, models.VersionModel,
version='0.1.1', spec='blah', speclist='==0.1.1,!=0.1.1-alpha')
version='0.1.1', spec='blah')
self.assertRaises(ValueError, models.VersionModel,
version='0.1', spec='>0.1.1', speclist='==0.1.1,!=0.1.1-alpha')
version='0.1', spec='==0.1.1,!=0.1.1-alpha')
self.assertRaises(ValueError, models.VersionModel,
version='0.1.1', spec='>0.1.1', speclist='==0,!=0.2')
version='0.1.1', spec='==0,!=0.2')
def test_partial(self):
obj = models.PartialVersionModel(partial='0.1.0')
@@ -64,27 +61,23 @@ class DjangoFieldTestCase(unittest.TestCase):
self.assertEqual(semantic_version.Version('0.1.0', partial=True), obj.partial)
self.assertIsNone(obj.optional)
self.assertIsNone(obj.optional_spec)
self.assertIsNone(obj.optional_speclist)
# Copy values to another model
alt_obj = models.PartialVersionModel(
partial=obj.partial,
optional=obj.optional,
optional_spec=obj.optional_spec,
optional_speclist=obj.optional_speclist,
)
self.assertEqual(semantic_version.Version('0.1.0', partial=True), alt_obj.partial)
self.assertEqual(obj.partial, alt_obj.partial)
self.assertIsNone(obj.optional)
self.assertIsNone(obj.optional_spec)
self.assertIsNone(obj.optional_speclist)
def test_serialization(self):
o1 = models.VersionModel(version='0.1.1', spec='<0.2.4-rc42',
speclist='==0.1.1,!=0.1.1-alpha')
o2 = models.VersionModel(version='0.4.3-rc3+build3', spec='==0.4.3',
speclist='<=0.1.1-rc2,!=0.1.1-rc1')
o1 = models.VersionModel(version='0.1.1', spec='==0.1.1,!=0.1.1-alpha')
o2 = models.VersionModel(version='0.4.3-rc3+build3',
spec='<=0.1.1-rc2,!=0.1.1-rc1')
data = serializers.serialize('json', [o1, o2])
@@ -94,9 +87,9 @@ class DjangoFieldTestCase(unittest.TestCase):
def test_serialization_partial(self):
o1 = models.PartialVersionModel(partial='0.1.1', optional='0.2.4-rc42',
optional_spec=None, optional_speclist=None)
optional_spec=None)
o2 = models.PartialVersionModel(partial='0.4.3-rc3+build3', optional='',
optional_spec='==1.1.0', optional_speclist='==0.1.1,!=0.1.1-alpha')
optional_spec='==0.1.1,!=0.1.1-alpha')
data = serializers.serialize('json', [o1, o2])