From caa1c01858be241bf2a726c780c2f7e8174ecff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 21 May 2012 21:35:56 +0200 Subject: [PATCH] Remove SpecItem. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- README | 3 +- doc/changelog.rst | 5 +- doc/django.rst | 8 +- doc/index.rst | 62 +++--- doc/reference.rst | 301 ++++++++++++-------------- src/semantic_version/__init__.py | 2 +- src/semantic_version/base.py | 17 +- src/semantic_version/django_fields.py | 15 -- tests/django_test_app/models.py | 7 +- tests/test_base.py | 3 - tests/test_django.py | 31 +-- 11 files changed, 192 insertions(+), 262 deletions(-) diff --git a/README b/README index 196649e..38c5196 100644 --- a/README +++ b/README @@ -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 diff --git a/doc/changelog.rst b/doc/changelog.rst index e4476ac..7b1cb8e 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -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) ------------------ diff --git a/doc/django.rst b/doc/django.rst index 2117c21..8cfdbca 100644 --- a/doc/django.rst +++ b/doc/django.rst @@ -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. diff --git a/doc/index.rst b/doc/index.rst index cae45f5..952e30e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -82,9 +82,9 @@ Obviously, :class:`Versions ` 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. diff --git a/doc/reference.rst b/doc/reference.rst index e785aa9..a173cb9 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -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') - = > - - 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') + = > + + 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/ diff --git a/src/semantic_version/__init__.py b/src/semantic_version/__init__.py index 9075b70..99a00dc 100644 --- a/src/semantic_version/__init__.py +++ b/src/semantic_version/__init__.py @@ -5,4 +5,4 @@ __version__ = '1.3.0-alpha' -from .base import compare, match, Version, Spec, SpecItem +from .base import compare, match, Version, Spec diff --git a/src/semantic_version/base.py b/src/semantic_version/base.py index 4f177b2..861063b 100644 --- a/src/semantic_version/base.py +++ b/src/semantic_version/base.py @@ -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) diff --git a/src/semantic_version/django_fields.py b/src/semantic_version/django_fields.py index 7cc5d44..c6ef688 100644 --- a/src/semantic_version/django_fields.py +++ b/src/semantic_version/django_fields.py @@ -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."), diff --git a/tests/django_test_app/models.py b/tests/django_test_app/models.py index 8e1f2ce..53ed874 100644 --- a/tests/django_test_app/models.py +++ b/tests/django_test_app/models.py @@ -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) diff --git a/tests/test_base.py b/tests/test_base.py index 41ec6cf..22690b6 100755 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -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)) diff --git a/tests/test_django.py b/tests/test_django.py index 5768c47..b4a3c29 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -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])