[prev in list] [next in list] [prev in thread] [next in thread] 

List:       mailman-cvs
Subject:    [Mailman-checkins] [Git][mailman/mailman][master] 2 commits: Refactor API contexts.
From:       Barry Warsaw <gitlab () mg ! gitlab ! com>
Date:       2016-01-13 16:22:42
Message-ID: 569679d22d217_34c347dcd601950 () worker15 ! cluster ! gitlab ! com ! mail
[Download RAW message or body]

[Attachment #2 (multipart/alternative)]


Barry Warsaw pushed to branch master at mailman / Mailman


Commits:
d75a7ebb by Barry Warsaw at 2016-01-13T00:17:49-05:00
Refactor API contexts.

Rather than sprinkle API version string tests all over the place, create
an IAPI interface which encapsulates the differences between API 3.0 and
3.1, and arrange for this to be used to convert to and from UUIDs.

- - - - -
98c074f1 by Barry Warsaw at 2016-01-13T11:16:38-05:00
Refactor API differences into a separate class.

We now have an IAPI interface which defines methods to convert to/from
UUIDs to their REST representations, and to calculate the API-homed full
URL path to a resource.  Add implementations API30 and API31 to handle
the two different implementations so far.  This also simplifies the
various path_to() calls.

Also: Add support for diff_cover to tox.ini to check that all
differences against the master branch have full test coverage.

- - - - -


21 changed files:

- .gitignore
- coverage.ini
- src/mailman/commands/cli_info.py
- + src/mailman/core/api.py
- + src/mailman/interfaces/api.py
- src/mailman/rest/addresses.py
- src/mailman/rest/docs/helpers.rst
- src/mailman/rest/domains.py
- src/mailman/rest/helpers.py
- src/mailman/rest/lists.py
- src/mailman/rest/members.py
- src/mailman/rest/post_moderation.py
- src/mailman/rest/preferences.py
- src/mailman/rest/queues.py
- src/mailman/rest/root.py
- src/mailman/rest/tests/test_addresses.py
- src/mailman/rest/tests/test_validator.py
- src/mailman/rest/users.py
- src/mailman/rest/validator.py
- src/mailman/rest/wsgiapp.py
- tox.ini


Changes:

=====================================
.gitignore
=====================================
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@ var
 htmlcov
 __pycache__
 .tox
+coverage.xml
+diffcov.html


=====================================
coverage.ini
=====================================
--- a/coverage.ini
+++ b/coverage.ini
@@ -5,6 +5,7 @@ omit =
      setup*
      */showme.py
     .tox/coverage/lib/python3.5/site-packages/*
+    .tox/diffcov/lib/python3.5/site-packages/*
     */test_*.py
 
 [report]


=====================================
src/mailman/commands/cli_info.py
=====================================
--- a/src/mailman/commands/cli_info.py
+++ b/src/mailman/commands/cli_info.py
@@ -26,9 +26,9 @@ import sys
 
 from lazr.config import as_boolean
 from mailman.config import config
+from mailman.core.api import API30, API31
 from mailman.core.i18n import _
 from mailman.interfaces.command import ICLISubCommand
-from mailman.rest.helpers import path_to
 from mailman.version import MAILMAN_VERSION_FULL
 from zope.interface import implementer
 
@@ -68,9 +68,8 @@ class Info:
         print('devmode:',
               'ENABLED' if as_boolean(config.devmode.enabled) else 'DISABLED',
               file=output)
-        print('REST root url:',
-              path_to('/', config.webservice.api_version),
-              file=output)
+        api = (API30 if config.webservice.api_version == '3.0' else API31)
+        print('REST root url:', api.path_to('/'), file=output)
         print('REST credentials: {0}:{1}'.format(
             config.webservice.admin_user, config.webservice.admin_pass),
             file=output)


=====================================
src/mailman/core/api.py
=====================================
--- /dev/null
+++ b/src/mailman/core/api.py
@@ -0,0 +1,82 @@
+# Copyright (C) 2016 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman.  If not, see <http://www.gnu.org/licenses/>.
+
+"""REST web service API contexts."""
+
+__all__ = [
+    'API30',
+    'API31',
+    ]
+
+
+from lazr.config import as_boolean
+from mailman.config import config
+from mailman.interfaces.api import IAPI
+from uuid import UUID
+from zope.interface import implementer
+
+
+@implementer(IAPI)
+class API30:
+    version = '3.0'
+
+    @classmethod
+    def path_to(cls, resource):
+        """See `IAPI`."""
+        return '{}://{}:{}/{}/{}'.format(
+            ('https' if as_boolean(config.webservice.use_https) else 'http'),
+            config.webservice.hostname,
+            config.webservice.port,
+            cls.version,
+            (resource[1:] if resource.startswith('/') else resource),
+            )
+
+    @staticmethod
+    def from_uuid(uuid):
+        """See `IAPI`."""
+        return uuid.int
+
+    @staticmethod
+    def to_uuid(uuid_repr):
+        """See `IAPI`."""
+        return UUID(int=int(uuid_repr))
+
+
+@implementer(IAPI)
+class API31:
+    version = '3.1'
+
+    @classmethod
+    def path_to(cls, resource):
+        """See `IAPI`."""
+        return '{}://{}:{}/{}/{}'.format(
+            ('https' if as_boolean(config.webservice.use_https) else 'http'),
+            config.webservice.hostname,
+            config.webservice.port,
+            cls.version,
+            (resource[1:] if resource.startswith('/') else resource),
+            )
+
+    @staticmethod
+    def from_uuid(uuid):
+        """See `IAPI`."""
+        return uuid.hex
+
+    @staticmethod
+    def to_uuid(uuid_repr):
+        """See `IAPI`."""
+        return UUID(hex=uuid_repr)


=====================================
src/mailman/interfaces/api.py
=====================================
--- /dev/null
+++ b/src/mailman/interfaces/api.py
@@ -0,0 +1,65 @@
+# Copyright (C) 2016 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman.  If not, see <http://www.gnu.org/licenses/>.
+
+"""REST web service API context."""
+
+__all__ = [
+    'IAPI',
+    ]
+
+
+from zope.interface import Attribute, Interface
+
+
+class IAPI(Interface):
+    """The REST web service context."""
+
+    version = Attribute("""The REST API version.""")
+
+    def path_to(resource):
+        """Return the full REST URL to the given resource.
+
+        :param resource: Resource path string without the leading scheme,
+            host, port, or API version information.
+        :type resource: str
+        :return: Full URL path to the resource, with the scheme, host, port
+            and API version prepended.
+        :rtype: str
+        """
+
+    def from_uuid(uuid):
+        """Return the string representation of a UUID.
+
+        :param uuid: The UUID to convert.
+        :type uuid: UUID
+        :return: The string representation of the UUID, as appropriate for the
+            API version.  In 3.0 this is the representation of an integer,
+            while in 3.1 it is the hex representation.
+        :rtype: str
+        """
+
+    def to_uuid(uuid):
+        """Return the UUID from the string representation.
+
+        :param uuid: The string representation of the UUID.
+        :type uuid: str
+        :return: The UUID converted from the string representation, as
+            appropriate for the API version.  In 3.0, uuid is interpreted as
+            the integer representation of a UUID, while in 3.1 it is the hex
+            representation of the UUID.
+        :rtype: UUID
+        """


=====================================
src/mailman/rest/addresses.py
=====================================
--- a/src/mailman/rest/addresses.py
+++ b/src/mailman/rest/addresses.py
@@ -51,7 +51,7 @@ class _AddressBase(CollectionMixin):
             email=address.email,
             original_email=address.original_email,
             registered_on=address.registered_on,
-            self_link=self.path_to('addresses/{}'.format(address.email)),
+            self_link=self.api.path_to('addresses/{}'.format(address.email)),
             )
         # Add optional attributes.  These can be None or the empty string.
         if address.display_name:
@@ -59,9 +59,8 @@ class _AddressBase(CollectionMixin):
         if address.verified_on:
             representation['verified_on'] = address.verified_on
         if address.user:
-            uid = getattr(address.user.user_id,
-                          'int' if self.api_version == '3.0' else 'hex')
-            representation['user'] = self.path_to('users/{}'.format(uid))
+            uid = self.api.from_uuid(address.user.user_id)
+            representation['user'] = self.api.path_to('users/{}'.format(uid))
         return representation
 
     def _get_collection(self, request):
@@ -214,14 +213,15 @@ class UserAddresses(_AddressBase):
             address = user_manager.get_address(validator(request)['email'])
             if address.user is None:
                 address.user = self._user
-                location = self.path_to('addresses/{}'.format(address.email))
+                location = self.api.path_to(
+                    'addresses/{}'.format(address.email))
                 created(response, location)
             else:
                 bad_request(response, 'Address belongs to other user.')
         else:
             # Link the address to the current user and return it.
             address.user = self._user
-            location = self.path_to('addresses/{}'.format(address.email))
+            location = self.api.path_to('addresses/{}'.format(address.email))
             created(response, location)
 
 


=====================================
src/mailman/rest/docs/helpers.rst
=====================================
--- a/src/mailman/rest/docs/helpers.rst
+++ b/src/mailman/rest/docs/helpers.rst
@@ -5,34 +5,6 @@ REST API helpers
 There are a number of helpers that make building out the REST API easier.
 
 
-Resource paths
-==============
-
-For example, most resources don't have to worry about where they are rooted.
-They only need to know where they are relative to the root URI, and this
-function can return them the full path to the resource.  We have to pass in
-the REST API version because there is no request in flight.
-
-    >>> from mailman.rest.helpers import path_to
-    >>> print(path_to('system', '3.0'))
-    http://localhost:9001/3.0/system
-
-Parameters like the ``scheme``, ``host``, and ``port`` can be set in the
-configuration file.
-::
-
-    >>> config.push('helpers', """
-    ... [webservice]
-    ... hostname: geddy
-    ... port: 2112
-    ... use_https: yes
-    ... """)
-    >>> cleanups.append((config.pop, 'helpers'))
-
-    >>> print(path_to('system', '4.2'))
-    https://geddy:2112/4.2/system
-
-
 Etags
 =====
 


=====================================
src/mailman/rest/domains.py
=====================================
--- a/src/mailman/rest/domains.py
+++ b/src/mailman/rest/domains.py
@@ -44,7 +44,7 @@ class _DomainBase(CollectionMixin):
             base_url=domain.base_url,
             description=domain.description,
             mail_host=domain.mail_host,
-            self_link=self.path_to('domains/{}'.format(domain.mail_host)),
+            self_link=self.api.path_to('domains/{}'.format(domain.mail_host)),
             url_host=domain.url_host,
             )
 
@@ -123,7 +123,7 @@ class AllDomains(_DomainBase):
         except BadDomainSpecificationError as error:
             bad_request(response, str(error))
         else:
-            location = self.path_to('domains/{}'.format(domain.mail_host))
+            location = self.api.path_to('domains/{}'.format(domain.mail_host))
             created(response, location)
 
     def on_get(self, request, response):


=====================================
src/mailman/rest/helpers.py
=====================================
--- a/src/mailman/rest/helpers.py
+++ b/src/mailman/rest/helpers.py
@@ -32,7 +32,6 @@ __all__ = [
     'no_content',
     'not_found',
     'okay',
-    'path_to',
     ]
 
 
@@ -48,27 +47,6 @@ from pprint import pformat
 
 
 
-def path_to(resource, api_version):
-    """Return the url path to a resource.
-
-    :param resource: The canonical path to the resource, relative to the
-        system base URI.
-    :type resource: string
-    :param api_version: API version to report.
-    :type api_version: string
-    :return: The full path to the resource.
-    :rtype: bytes
-    """
-    return '{0}://{1}:{2}/{3}/{4}'.format(
-        ('https' if as_boolean(config.webservice.use_https) else 'http'),
-        config.webservice.hostname,
-        config.webservice.port,
-        api_version,
-        (resource[1:] if resource.startswith('/') else resource),
-        )
-
-
-
 class ExtendedEncoder(json.JSONEncoder):
     """An extended JSON encoder which knows about other data types."""
 
@@ -185,9 +163,6 @@ class CollectionMixin:
             result['entries'] = entries
         return result
 
-    def path_to(self, resource):
-        return path_to(resource, self.api_version)
-
 
 
 class GetterSetter:


=====================================
src/mailman/rest/lists.py
=====================================
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -110,7 +110,7 @@ class _ListBase(CollectionMixin):
             mail_host=mlist.mail_host,
             member_count=mlist.members.member_count,
             volume=mlist.volume,
-            self_link=self.path_to('lists/{}'.format(mlist.list_id)),
+            self_link=self.api.path_to('lists/{}'.format(mlist.list_id)),
             )
 
     def _get_collection(self, request):
@@ -155,7 +155,7 @@ class AList(_ListBase):
             email, self._mlist.list_id, role)
         if member is None:
             return NotFound(), []
-        return AMember(request.context['api_version'], member.member_id)
+        return AMember(request.context['api'], member.member_id)
 
     @child(roster_matcher)
     def roster(self, request, segments, role):
@@ -216,7 +216,7 @@ class AllLists(_ListBase):
             reason = 'Domain does not exist: {}'.format(error.domain)
             bad_request(response, reason.encode('utf-8'))
         else:
-            location = self.path_to('lists/{0}'.format(mlist.list_id))
+            location = self.api.path_to('lists/{0}'.format(mlist.list_id))
             created(response, location)
 
     def on_get(self, request, response):


=====================================
src/mailman/rest/members.py
=====================================
--- a/src/mailman/rest/members.py
+++ b/src/mailman/rest/members.py
@@ -27,7 +27,7 @@ __all__ = [
 
 from mailman.app.membership import add_member, delete_member
 from mailman.interfaces.action import Action
-from mailman.interfaces.address import IAddress, InvalidEmailAddressError
+from mailman.interfaces.address import IAddress
 from mailman.interfaces.listmanager import IListManager
 from mailman.interfaces.member import (
     AlreadySubscribedError, DeliveryMode, MemberRole, MembershipError,
@@ -43,7 +43,6 @@ from mailman.rest.helpers import (
 from mailman.rest.preferences import Preferences, ReadOnlyPreferences
 from mailman.rest.validator import (
     Validator, enum_validator, subscriber_validator)
-from operator import attrgetter
 from uuid import UUID
 from zope.component import getUtility
 
@@ -52,10 +51,6 @@ from zope.component import getUtility
 class _MemberBase(CollectionMixin):
     """Shared base class for member representations."""
 
-    def _get_uuid(self, member):
-        return getattr(member.member_id,
-                       'int' if self.api_version == '3.0' else 'hex')
-
     def _resource_as_dict(self, member):
         """See `CollectionMixin`."""
         enum, dot, role = str(member.role).partition('.')
@@ -66,23 +61,23 @@ class _MemberBase(CollectionMixin):
         # member_id are UUIDs.  In API 3.0 we use the integer equivalent of
         # the UID in the URL, but in API 3.1 we use the hex equivalent.  See
         # issue #121 for details.
-        member_id = self._get_uuid(member)
+        member_id = self.api.from_uuid(member.member_id)
         response = dict(
-            address=self.path_to('addresses/{}'.format(member.address.email)),
+            address=self.api.path_to(
+                'addresses/{}'.format(member.address.email)),
             delivery_mode=member.delivery_mode,
             email=member.address.email,
             list_id=member.list_id,
             member_id=member_id,
             moderation_action=member.moderation_action,
             role=role,
-            self_link=self.path_to('members/{}'.format(member_id)),
+            self_link=self.api.path_to('members/{}'.format(member_id)),
             )
         # Add the user link if there is one.
         user = member.user
         if user is not None:
-            user_id = getattr(user.user_id,
-                              'int' if self.api_version == '3.0' else 'hex')
-            response['user'] = self.path_to('users/{}'.format(user_id))
+            user_id = self.api.from_uuid(user.user_id)
+            response['user'] = self.api.path_to('users/{}'.format(user_id))
         return response
 
     def _get_collection(self, request):
@@ -112,16 +107,13 @@ class MemberCollection(_MemberBase):
 class AMember(_MemberBase):
     """A member."""
 
-    def __init__(self, api_version, member_id_string):
+    def __init__(self, api, member_id_string):
         # The member_id_string is the string representation of the member's
         # UUID.  In API 3.0, the argument is the string representation of the
         # int representation of the UUID.  In API 3.1 it's the hex.
-        self.api_version = api_version
+        self.api = api
         try:
-            if api_version == '3.0':
-                member_id = UUID(int=int(member_id_string))
-            else:
-                member_id = UUID(hex=member_id_string)
+            member_id = api.to_uuid(member_id_string)
         except ValueError:
             # The string argument could not be converted to a UUID.
             self._member = None
@@ -143,7 +135,7 @@ class AMember(_MemberBase):
             return NotFound(), []
         if self._member is None:
             return NotFound(), []
-        member_id = self._get_uuid(self._member)
+        member_id = self.api.from_uuid(self._member.member_id)
         child = Preferences(
             self._member.preferences, 'members/{}'.format(member_id))
         return child, []
@@ -157,7 +149,8 @@ class AMember(_MemberBase):
             return NotFound(), []
         child = ReadOnlyPreferences(
             self._member,
-            'members/{}/all'.format(self._get_uuid(self._member)))
+            'members/{}/all'.format(
+                self.api.from_uuid(self._member.member_id)))
         return child, []
 
     def on_delete(self, request, response):
@@ -220,7 +213,7 @@ class AllMembers(_MemberBase):
         try:
             validator = Validator(
                 list_id=str,
-                subscriber=subscriber_validator(self.api_version),
+                subscriber=subscriber_validator(self.api),
                 display_name=str,
                 delivery_mode=enum_validator(DeliveryMode),
                 role=enum_validator(MemberRole),
@@ -292,8 +285,8 @@ class AllMembers(_MemberBase):
                 # and return the location to the new member.  Member ids are
                 # UUIDs and need to be converted to URLs because JSON doesn't
                 # directly support UUIDs.
-                member_id = self._get_uuid(member)
-                location = self.path_to('members/{}'.format(member_id))
+                member_id = self.api.from_uuid(member.member_id)
+                location = self.api.path_to('members/{}'.format(member_id))
                 created(response, location)
                 return
             # The member could not be directly subscribed because there are
@@ -339,8 +332,8 @@ class AllMembers(_MemberBase):
         # and return the location to the new member.  Member ids are
         # UUIDs and need to be converted to URLs because JSON doesn't
         # directly support UUIDs.
-        member_id = self._get_uuid(member)
-        location = self.path_to('members/{}'.format(member_id))
+        member_id = self.api.from_uuid(member.member_id)
+        location = self.api.path_to('members/{}'.format(member_id))
         created(response, location)
 
     def on_get(self, request, response):
@@ -353,10 +346,10 @@ class AllMembers(_MemberBase):
 class _FoundMembers(MemberCollection):
     """The found members collection."""
 
-    def __init__(self, members, api_version):
+    def __init__(self, members, api):
         super().__init__()
         self._members = members
-        self.api_version = api_version
+        self.api = api
 
     def _get_collection(self, request):
         """See `CollectionMixin`."""
@@ -380,5 +373,5 @@ class FindMembers(_MemberBase):
             bad_request(response, str(error))
         else:
             members = service.find_members(**data)
-            resource = _FoundMembers(members, self.api_version)
+            resource = _FoundMembers(members, self.api)
             okay(response, etag(resource._make_collection(request)))


=====================================
src/mailman/rest/post_moderation.py
=====================================
--- a/src/mailman/rest/post_moderation.py
+++ b/src/mailman/rest/post_moderation.py
@@ -28,8 +28,7 @@ from mailman.interfaces.action import Action
 from mailman.interfaces.messages import IMessageStore
 from mailman.interfaces.requests import IListRequests, RequestType
 from mailman.rest.helpers import (
-    CollectionMixin, bad_request, child, etag, no_content, not_found, okay,
-    path_to)
+    CollectionMixin, bad_request, child, etag, no_content, not_found, okay)
 from mailman.rest.validator import Validator, enum_validator
 from zope.component import getUtility
 
@@ -62,9 +61,8 @@ class _ModerationBase:
         # that's fine too.
         resource.pop('id', None)
         # Add a self_link.
-        resource['self_link'] = path_to(
-            'lists/{}/held/{}'.format(self._mlist.list_id, request_id),
-            self.api_version)
+        resource['self_link'] = self.api.path_to(
+            'lists/{}/held/{}'.format(self._mlist.list_id, request_id))
         return resource
 
 


=====================================
src/mailman/rest/preferences.py
=====================================
--- a/src/mailman/rest/preferences.py
+++ b/src/mailman/rest/preferences.py
@@ -26,7 +26,7 @@ __all__ = [
 from lazr.config import as_boolean
 from mailman.interfaces.member import DeliveryMode, DeliveryStatus
 from mailman.rest.helpers import (
-    GetterSetter, bad_request, etag, no_content, not_found, okay, path_to)
+    GetterSetter, bad_request, etag, no_content, not_found, okay)
 from mailman.rest.validator import (
     Validator, enum_validator, language_validator)
 
@@ -64,9 +64,8 @@ class ReadOnlyPreferences:
         if preferred_language is not None:
             resource['preferred_language'] = preferred_language.code
         # Add the self link.
-        resource['self_link'] = path_to(
-            '{}/preferences'.format(self._base_url),
-            self.api_version)
+        resource['self_link'] = self.api.path_to(
+            '{}/preferences'.format(self._base_url))
         okay(response, etag(resource))
 
 


=====================================
src/mailman/rest/queues.py
=====================================
--- a/src/mailman/rest/queues.py
+++ b/src/mailman/rest/queues.py
@@ -46,7 +46,7 @@ class _QueuesBase(CollectionMixin):
             directory=switchboard.queue_directory,
             count=len(files),
             files=files,
-            self_link=self.path_to('queues/{}'.format(name)),
+            self_link=self.api.path_to('queues/{}'.format(name)),
             )
 
     def _get_collection(self, request):
@@ -89,7 +89,7 @@ class AQueue(_QueuesBase):
             bad_request(response, str(error))
             return
         else:
-            location = self.path_to(
+            location = self.api.path_to(
                 'queues/{}/{}'.format(self._name, filebase))
             created(response, location)
 
@@ -122,5 +122,5 @@ class AllQueues(_QueuesBase):
     def on_get(self, request, response):
         """<api>/queues"""
         resource = self._make_collection(request)
-        resource['self_link'] = self.path_to('queues')
+        resource['self_link'] = self.api.path_to('queues')
         okay(response, etag(resource))


=====================================
src/mailman/rest/root.py
=====================================
--- a/src/mailman/rest/root.py
+++ b/src/mailman/rest/root.py
@@ -26,6 +26,7 @@ import falcon
 
 from base64 import b64decode
 from mailman.config import config
+from mailman.core.api import API30, API31
 from mailman.core.constants import system_preferences
 from mailman.core.system import system
 from mailman.interfaces.listmanager import IListManager
@@ -33,7 +34,7 @@ from mailman.model.uid import UID
 from mailman.rest.addresses import AllAddresses, AnAddress
 from mailman.rest.domains import ADomain, AllDomains
 from mailman.rest.helpers import (
-    BadRequest, NotFound, child, etag, no_content, not_found, okay, path_to)
+    BadRequest, NotFound, child, etag, no_content, not_found, okay)
 from mailman.rest.lists import AList, AllLists, Styles
 from mailman.rest.members import AMember, AllMembers, FindMembers
 from mailman.rest.preferences import ReadOnlyPreferences
@@ -59,7 +60,7 @@ class Root:
     @child('3.0')
     def api_version_30(self, request, segments):
         # API version 3.0 was introduced in Mailman 3.0.
-        request.context['api_version'] = '3.0'
+        request.context['api'] = API30
         return self._check_authorization(request, segments)
 
     @child('3.1')
@@ -68,7 +69,7 @@ class Root:
         # incompatible difference is that uuids are represented as hex strings
         # instead of 128 bit integers.  The latter is not compatible with all
         # versions of JavaScript.
-        request.context['api_version'] = '3.1'
+        request.context['api'] = API31
         return self._check_authorization(request, segments)
 
     def _check_authorization(self, request, segments):
@@ -102,8 +103,8 @@ class Versions:
         resource = dict(
             mailman_version=system.mailman_version,
             python_version=system.python_version,
-            api_version=self.api_version,
-            self_link=path_to('system/versions', self.api_version),
+            api_version=self.api.version,
+            self_link=self.api.path_to('system/versions'),
             )
         okay(response, etag(resource))
 
@@ -179,12 +180,12 @@ class TopLevel:
         """
         if len(segments) == 0:
             resource = AllAddresses()
-            resource.api_version = request.context['api_version']
+            resource.api = request.context['api']
             return resource
         else:
             email = segments.pop(0)
             resource = AnAddress(email)
-            resource.api_version = request.context['api_version']
+            resource.api = request.context['api']
             return resource, segments
 
     @child()
@@ -218,32 +219,32 @@ class TopLevel:
     @child()
     def members(self, request, segments):
         """/<api>/members"""
-        api_version = request.context['api_version']
+        api = request.context['api']
         if len(segments) == 0:
             resource = AllMembers()
-            resource.api_version = api_version
+            resource.api = api
             return resource
         # Either the next segment is the string "find" or a member id.  They
         # cannot collide.
         segment = segments.pop(0)
         if segment == 'find':
             resource = FindMembers()
-            resource.api_version = api_version
+            resource.api = api
         else:
-            resource = AMember(api_version, segment)
+            resource = AMember(api, segment)
         return resource, segments
 
     @child()
     def users(self, request, segments):
         """/<api>/users"""
-        api_version = request.context['api_version']
+        api = request.context['api']
         if len(segments) == 0:
             resource = AllUsers()
-            resource.api_version = api_version
+            resource.api = api
             return resource
         else:
             user_id = segments.pop(0)
-            return AUser(api_version, user_id), segments
+            return AUser(api, user_id), segments
 
     @child()
     def owners(self, request, segments):
@@ -252,7 +253,7 @@ class TopLevel:
             return BadRequest(), []
         else:
             resource = ServerOwners()
-            resource.api_version = request.context['api_version']
+            resource.api = request.context['api']
             return resource, segments
 
     @child()


=====================================
src/mailman/rest/tests/test_addresses.py
=====================================
--- a/src/mailman/rest/tests/test_addresses.py
+++ b/src/mailman/rest/tests/test_addresses.py
@@ -369,6 +369,18 @@ class TestAddresses(unittest.TestCase):
                 })
         self.assertEqual(cm.exception.code, 403)
 
+    def test_user_subresource_post_no_such_user(self):
+        # Try to link an address to a nonexistent user.
+        with transaction():
+            getUtility(IUserManager).create_address('anne@example.com')
+        with self.assertRaises(HTTPError) as cm:
+            call_api(
+                'http://localhost:9001/3.0/addresses/anne@example.com/user', {
+                    'user_id': 2,
+                    })
+        self.assertEqual(cm.exception.code, 400)
+        self.assertEqual(cm.exception.reason, b'No user with ID 2')
+
     def test_user_subresource_unlink(self):
         # By DELETEing the usr subresource, you can unlink a user from an
         # address.
@@ -542,7 +554,7 @@ class TestAPI31Addresses(unittest.TestCase):
                     })
         self.assertEqual(cm.exception.code, 400)
         self.assertEqual(cm.exception.reason,
-                         b'badly formed hexadecimal UUID string')
+                         b'Cannot convert parameters: user_id')
 
     def test_user_subresource_put(self):
         # By PUTing to the 'user' resource, you can change the user that an
@@ -577,4 +589,4 @@ class TestAPI31Addresses(unittest.TestCase):
                     }, method='PUT')
         self.assertEqual(cm.exception.code, 400)
         self.assertEqual(cm.exception.reason,
-                         b'badly formed hexadecimal UUID string')
+                         b'Cannot convert parameters: user_id')


=====================================
src/mailman/rest/tests/test_validator.py
=====================================
--- a/src/mailman/rest/tests/test_validator.py
+++ b/src/mailman/rest/tests/test_validator.py
@@ -25,6 +25,7 @@ __all__ = [
 import unittest
 
 from mailman.interfaces.usermanager import IUserManager
+from mailman.core.api import API30, API31
 from mailman.rest.validator import (
     list_of_strings_validator, subscriber_validator)
 from mailman.testing.layers import RESTLayer
@@ -53,35 +54,35 @@ class TestValidators(unittest.TestCase):
     def test_subscriber_validator_int_uuid(self):
         # Convert from an existing user id to a UUID.
         anne = getUtility(IUserManager).make_user('anne@example.com')
-        uuid = subscriber_validator('3.0')(str(anne.user_id.int))
+        uuid = subscriber_validator(API30)(str(anne.user_id.int))
         self.assertEqual(anne.user_id, uuid)
 
     def test_subscriber_validator_hex_uuid(self):
         # Convert from an existing user id to a UUID.
         anne = getUtility(IUserManager).make_user('anne@example.com')
-        uuid = subscriber_validator('3.1')(anne.user_id.hex)
+        uuid = subscriber_validator(API31)(anne.user_id.hex)
         self.assertEqual(anne.user_id, uuid)
 
     def test_subscriber_validator_no_int_uuid(self):
         # API 3.1 does not accept ints as subscriber id's.
         anne = getUtility(IUserManager).make_user('anne@example.com')
         self.assertRaises(ValueError,
-                          subscriber_validator('3.1'), str(anne.user_id.int))
+                          subscriber_validator(API31), str(anne.user_id.int))
 
     def test_subscriber_validator_bad_int_uuid(self):
         # In API 3.0, UUIDs are ints.
         self.assertRaises(ValueError,
-                          subscriber_validator('3.0'), 'not-a-thing')
+                          subscriber_validator(API30), 'not-a-thing')
 
     def test_subscriber_validator_bad_int_hex(self):
         # In API 3.1, UUIDs are hexes.
         self.assertRaises(ValueError,
-                          subscriber_validator('3.1'), 'not-a-thing')
+                          subscriber_validator(API31), 'not-a-thing')
 
     def test_subscriber_validator_email_address_API30(self):
-        self.assertEqual(subscriber_validator('3.0')('anne@example.com'),
+        self.assertEqual(subscriber_validator(API30)('anne@example.com'),
                          'anne@example.com')
 
     def test_subscriber_validator_email_address_API31(self):
-        self.assertEqual(subscriber_validator('3.1')('anne@example.com'),
+        self.assertEqual(subscriber_validator(API31)('anne@example.com'),
                          'anne@example.com')


=====================================
src/mailman/rest/users.py
=====================================
--- a/src/mailman/rest/users.py
+++ b/src/mailman/rest/users.py
@@ -35,12 +35,11 @@ from mailman.interfaces.usermanager import IUserManager
 from mailman.rest.addresses import UserAddresses
 from mailman.rest.helpers import (
     BadRequest, CollectionMixin, GetterSetter, NotFound, bad_request, child,
-    conflict, created, etag, forbidden, no_content, not_found, okay, path_to)
+    conflict, created, etag, forbidden, no_content, not_found, okay)
 from mailman.rest.preferences import Preferences
 from mailman.rest.validator import (
     PatchValidator, Validator, list_of_strings_validator)
 from passlib.utils import generate_password as generate
-from uuid import UUID
 from zope.component import getUtility
 
 
@@ -117,9 +116,9 @@ def create_user(arguments, request, response):
         password = generate(int(config.passwords.password_length))
     user.password = config.password_context.encrypt(password)
     user.is_server_owner = is_server_owner
-    api_version = request.context['api_version']
-    user_id = getattr(user.user_id, 'int' if api_version == '3.0' else 'hex')
-    location = path_to('users/{}'.format(user_id), api_version)
+    api = request.context['api']
+    user_id = api.from_uuid(user.user_id)
+    location = request.context.get('api').path_to('users/{}'.format(user_id))
     created(response, location)
     return user
 
@@ -128,21 +127,17 @@ def create_user(arguments, request, response):
 class _UserBase(CollectionMixin):
     """Shared base class for user representations."""
 
-    def _get_uuid(self, user):
-        return getattr(user.user_id,
-                       'int' if self.api_version == '3.0' else 'hex')
-
     def _resource_as_dict(self, user):
         """See `CollectionMixin`."""
         # The canonical URL for a user is their unique user id, although we
         # can always look up a user based on any registered and validated
         # email address associated with their account.  The user id is a UUID,
         # but we serialize its integer equivalent.
-        user_id = self._get_uuid(user)
+        user_id = self.api.from_uuid(user.user_id)
         resource = dict(
             created_on=user.created_on,
             is_server_owner=user.is_server_owner,
-            self_link=self.path_to('users/{}'.format(user_id)),
+            self_link=self.api.path_to('users/{}'.format(user_id)),
             user_id=user_id,
         )
         # Add the password attribute, only if the user has a password.  Same
@@ -182,9 +177,11 @@ class AllUsers(_UserBase):
 class AUser(_UserBase):
     """A user."""
 
-    def __init__(self, api_version, user_identifier):
+    def __init__(self, api, user_identifier):
         """Get a user by various type of identifiers.
 
+        :param api: The REST API object.
+        :type api: IAPI
         :param user_identifier: The identifier used to retrieve the user.  The
             identifier may either be an email address controlled by the user
             or the UUID of the user.  The type of identifier is auto-detected
@@ -193,7 +190,7 @@ class AUser(_UserBase):
             API 3.0 are integers, while in 3.1 are hex.
         :type user_identifier: string
         """
-        self.api_version = api_version
+        self.api = api
         user_manager = getUtility(IUserManager)
         if '@' in user_identifier:
             self._user = user_manager.get_user(user_identifier)
@@ -201,10 +198,7 @@ class AUser(_UserBase):
             # The identifier is the string representation of a UUID, either an
             # int in API 3.0 or a hex in API 3.1.
             try:
-                if api_version == '3.0':
-                    user_id = UUID(int=int(user_identifier))
-                else:
-                    user_id = UUID(hex=user_identifier)
+                user_id = api.to_uuid(user_identifier)
             except ValueError:
                 self._user = None
             else:
@@ -244,7 +238,7 @@ class AUser(_UserBase):
             return NotFound(), []
         child = Preferences(
             self._user.preferences,
-            'users/{}'.format(self._get_uuid(self._user)))
+            'users/{}'.format(self.api.from_uuid(self._user.user_id)))
         return child, []
 
     def on_patch(self, request, response):
@@ -319,14 +313,14 @@ class AddressUser(_UserBase):
         if self._user:
             conflict(response)
             return
-        api_version = request.context['api_version']
+        api = request.context['api']
         # When creating a linked user by POSTing, the user either must already
         # exist, or it can be automatically created, if the auto_create flag
         # is given and true (if missing, it defaults to true).  However, in
         # this case we do not accept 'email' as a POST field.
         fields = CREATION_FIELDS.copy()
         del fields['email']
-        fields['user_id'] = (int if api_version == '3.0' else str)
+        fields['user_id'] = api.to_uuid
         fields['auto_create'] = as_boolean
         fields['_optional'] = fields['_optional'] + (
             'user_id', 'auto_create', 'is_server_owner')
@@ -338,16 +332,11 @@ class AddressUser(_UserBase):
             return
         user_manager = getUtility(IUserManager)
         if 'user_id' in arguments:
-            raw_uid = arguments['user_id']
-            kws = {('int' if api_version == '3.0' else 'hex'): raw_uid}
-            try:
-                user_id = UUID(**kws)
-            except ValueError as error:
-                bad_request(response, str(error))
-                return
+            user_id = arguments['user_id']
             user = user_manager.get_user_by_id(user_id)
             if user is None:
-                not_found(response, b'No user with ID {}'.format(raw_uid))
+                bad_request(response, 'No user with ID {}'.format(
+                    self.api.from_uuid(user_id)))
                 return
             okay(response)
         else:
@@ -364,12 +353,12 @@ class AddressUser(_UserBase):
 
     def on_put(self, request, response):
         """Set or replace the addresses's user."""
-        api_version = request.context['api_version']
+        api = request.context['api']
         if self._user:
             self._user.unlink(self._address)
         # Process post data and check for an existing user.
         fields = CREATION_FIELDS.copy()
-        fields['user_id'] = (int if api_version == '3.0' else str)
+        fields['user_id'] = api.to_uuid
         fields['_optional'] = fields['_optional'] + (
             'user_id', 'email', 'is_server_owner')
         try:
@@ -380,16 +369,10 @@ class AddressUser(_UserBase):
             return
         user_manager = getUtility(IUserManager)
         if 'user_id' in arguments:
-            raw_uid = arguments['user_id']
-            kws = {('int' if api_version == '3.0' else 'hex'): raw_uid}
-            try:
-                user_id = UUID(**kws)
-            except ValueError as error:
-                bad_request(response, str(error))
-                return
+            user_id = arguments['user_id']
             user = user_manager.get_user_by_id(user_id)
             if user is None:
-                not_found(response, b'No user with ID {}'.format(raw_uid))
+                not_found(response, b'No user with ID {}'.format(user_id))
                 return
             okay(response)
         else:


=====================================
src/mailman/rest/validator.py
=====================================
--- a/src/mailman/rest/validator.py
+++ b/src/mailman/rest/validator.py
@@ -31,7 +31,6 @@ from mailman.core.errors import (
     ReadOnlyPATCHRequestError, UnknownPATCHRequestError)
 from mailman.interfaces.address import IEmailValidator
 from mailman.interfaces.languages import ILanguageManager
-from uuid import UUID
 from zope.component import getUtility
 
 
@@ -55,18 +54,11 @@ class enum_validator:
             raise ValueError(exception.args[0])
 
 
-def subscriber_validator(api_version):
+def subscriber_validator(api):
     """Convert an email-or-(int|hex) to an email-or-UUID."""
     def _inner(subscriber):
-        # In API 3.0, the uuid is represented by an int, so if we can int
-        # convert the value, we know it's a UUID-as-int.  In API 3.1 though,
-        # uuids are represented by the hex version, which of course cannot
-        # include an @ sign.
         try:
-            if api_version == '3.0':
-                return UUID(int=int(subscriber))
-            else:
-                return UUID(hex=subscriber)
+            return api.to_uuid(subscriber)
         except ValueError:
             # It must be an email address.
             if getUtility(IEmailValidator).is_valid(subscriber):


=====================================
src/mailman/rest/wsgiapp.py
=====================================
--- a/src/mailman/rest/wsgiapp.py
+++ b/src/mailman/rest/wsgiapp.py
@@ -76,7 +76,7 @@ class AdminWebServiceWSGIRequestHandler(WSGIRequestHandler):
 
 
 class SetAPIVersion:
-    """Falcon middleware object that sets the api_version on resources."""
+    """Falcon middleware object that sets the API on resources."""
 
     def process_resource(self, request, response, resource):
         # Set this attribute on the resource right before it is dispatched
@@ -88,7 +88,7 @@ class SetAPIVersion:
         # resource path does not exist.  This middleware method will still get
         # called, but there's nothing to set the api_version on.
         if resource is not None:
-            resource.api_version = request.context.get('api_version')
+            resource.api = request.context.get('api')
 
 
 class RootedAPI(API):


=====================================
tox.ini
=====================================
--- a/tox.ini
+++ b/tox.ini
@@ -30,9 +30,26 @@ commands =
     python -m coverage report -m {[coverage]rc}
 #sitepackages = True
 usedevelop = True
-whitelist_externals = python-coverage
 deps = coverage
 setenv =
     COVERAGE_PROCESS_START={[coverage]rcfile}
     COVERAGE_OPTIONS="-p"
     COVERAGE_FILE={toxinidir}/.coverage
+
+[testenv:diffcov]
+basepython = python3.5
+commands =
+    python -m coverage run {[coverage]rc} -m nose2 -v
+    python -m coverage combine {[coverage]rc}
+    python -m coverage xml {[coverage]rc}
+    diff-cover coverage.xml --html-report diffcov.html
+    diff-cover coverage.xml
+#sitepackages = True
+usedevelop = True
+deps =
+    coverage
+    diff_cover
+setenv =
+    COVERAGE_PROCESS_START={[coverage]rcfile}
+    COVERAGE_OPTIONS="-p"
+    COVERAGE_FILE={toxinidir}/.coverage



View it on GitLab: https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d



[Attachment #5 (text/html)]

<html lang='en'>
<head>
<meta content='text/html; charset=utf-8' http-equiv='Content-Type'>
<title>
GitLab
</title>
</meta>
</head>
<style>
  img {
    max-width: 100%;
    height: auto;
  }
  p.details {
    font-style:italic;
    color:#777
  }
  .footer p {
    font-size:small;
    color:#777
  }
  pre.commit-message {
    white-space: pre-wrap;
  }
  .file-stats a {
    text-decoration: none;
  }
  .file-stats .new-file {
    color: #090;
  }
  .file-stats .deleted-file {
    color: #B00;
  }
</style>
<body>
<div class='content'>
<h3>
Barry Warsaw pushed to branch master
at <a href="https://gitlab.com/mailman/mailman">mailman / Mailman</a>
</h3>
<h4>
Commits:
</h4>
<ul>
<li>
<strong><a href="https://gitlab.com/mailman/mailman/commit/d75a7ebb46279f341b498bf517d07e9ae4c27f0a">d75a7ebb</a></strong>
 <div>
<span>by Barry Warsaw</span>
<i>at 2016-01-13T00:17:49-05:00</i>
</div>
<pre class='commit-message'>Refactor API contexts.

Rather than sprinkle API version string tests all over the place, create
an IAPI interface which encapsulates the differences between API 3.0 and
3.1, and arrange for this to be used to convert to and from UUIDs.</pre>
</li>
<li>
<strong><a href="https://gitlab.com/mailman/mailman/commit/98c074f19492d81ebf5b5c3f4d4f2210aa56230d">98c074f1</a></strong>
 <div>
<span>by Barry Warsaw</span>
<i>at 2016-01-13T11:16:38-05:00</i>
</div>
<pre class='commit-message'>Refactor API differences into a separate class.

We now have an IAPI interface which defines methods to convert to/from
UUIDs to their REST representations, and to calculate the API-homed full
URL path to a resource.  Add implementations API30 and API31 to handle
the two different implementations so far.  This also simplifies the
various path_to() calls.

Also: Add support for diff_cover to tox.ini to check that all
differences against the master branch have full test coverage.</pre>
</li>
</ul>
<h4>21 changed files:</h4>
<ul>
<li class='file-stats'>
<a href='#diff-0'>
.gitignore
</a>
</li>
<li class='file-stats'>
<a href='#diff-1'>
coverage.ini
</a>
</li>
<li class='file-stats'>
<a href='#diff-2'>
src/mailman/commands/cli_info.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-3'>
<span class='new-file'>
&#43;
src/mailman/core/api.py
</span>
</a>
</li>
<li class='file-stats'>
<a href='#diff-4'>
<span class='new-file'>
&#43;
src/mailman/interfaces/api.py
</span>
</a>
</li>
<li class='file-stats'>
<a href='#diff-5'>
src/mailman/rest/addresses.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-6'>
src/mailman/rest/docs/helpers.rst
</a>
</li>
<li class='file-stats'>
<a href='#diff-7'>
src/mailman/rest/domains.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-8'>
src/mailman/rest/helpers.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-9'>
src/mailman/rest/lists.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-10'>
src/mailman/rest/members.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-11'>
src/mailman/rest/post_moderation.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-12'>
src/mailman/rest/preferences.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-13'>
src/mailman/rest/queues.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-14'>
src/mailman/rest/root.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-15'>
src/mailman/rest/tests/test_addresses.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-16'>
src/mailman/rest/tests/test_validator.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-17'>
src/mailman/rest/users.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-18'>
src/mailman/rest/validator.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-19'>
src/mailman/rest/wsgiapp.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-20'>
tox.ini
</a>
</li>
</ul>
<h4>Changes:</h4>
<li id='diff-0'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-0'>
 <strong>
.gitignore
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/.gitignore </span><span style="color: #000000;background-color: \
#ddffdd">+++ b/.gitignore </span><span style="color: #aaaaaa">@@ -4,3 +4,5 @@ var
</span> htmlcov
 __pycache__
 .tox
<span style="color: #000000;background-color: #ddffdd">+coverage.xml
+diffcov.html
</span></code></pre>

<br>
</li>
<li id='diff-1'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-1'>
 <strong>
coverage.ini
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/coverage.ini </span><span style="color: #000000;background-color: \
#ddffdd">+++ b/coverage.ini </span><span style="color: #aaaaaa">@@ -5,6 +5,7 @@ omit \
= </span>      setup*
      */showme.py
     .tox/coverage/lib/python3.5/site-packages/*
<span style="color: #000000;background-color: #ddffdd">+    \
.tox/diffcov/lib/python3.5/site-packages/* </span>     */test_*.py
 
 [report]
</code></pre>

<br>
</li>
<li id='diff-2'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-2'>
 <strong>
src/mailman/commands/cli_info.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/commands/cli_info.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/commands/cli_info.py \
</span><span style="color: #aaaaaa">@@ -26,9 +26,9 @@ import sys </span> 
 from lazr.config import as_boolean
 from mailman.config import config
<span style="color: #000000;background-color: #ddffdd">+from mailman.core.api import \
API30, API31 </span> from mailman.core.i18n import _
 from mailman.interfaces.command import ICLISubCommand
<span style="color: #000000;background-color: #ffdddd">-from mailman.rest.helpers \
import path_to </span> from mailman.version import MAILMAN_VERSION_FULL
 from zope.interface import implementer
 
<span style="color: #aaaaaa">@@ -68,9 +68,8 @@ class Info:
</span>         print('devmode:',
               'ENABLED' if as_boolean(config.devmode.enabled) else 'DISABLED',
               file=output)
<span style="color: #000000;background-color: #ffdddd">-        print('REST root \
                url:',
-              path_to('/', config.webservice.api_version),
-              file=output)
</span><span style="color: #000000;background-color: #ddffdd">+        api = (API30 \
if config.webservice.api_version == '3.0' else API31) +        print('REST root \
url:', api.path_to('/'), file=output) </span>         print('REST credentials: \
{0}:{1}'.format(  config.webservice.admin_user, config.webservice.admin_pass),
             file=output)
</code></pre>

<br>
</li>
<li id='diff-3'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-3'>
 <strong>
src/mailman/core/api.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- /dev/null </span><span style="color: #000000;background-color: \
#ddffdd">+++ b/src/mailman/core/api.py </span><span style="color: #aaaaaa">@@ -0,0 \
+1,82 @@ </span><span style="color: #000000;background-color: #ddffdd">+# Copyright \
(C) 2016 by the Free Software Foundation, Inc. +#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+"""REST web service API contexts."""
+
+__all__ = [
+    'API30',
+    'API31',
+    ]
+
+
+from lazr.config import as_boolean
+from mailman.config import config
+from mailman.interfaces.api import IAPI
+from uuid import UUID
+from zope.interface import implementer
+
+
+@implementer(IAPI)
+class API30:
+    version = '3.0'
+
+    @classmethod
+    def path_to(cls, resource):
+        """See `IAPI`."""
+        return '{}://{}:{}/{}/{}'.format(
+            ('https' if as_boolean(config.webservice.use_https) else 'http'),
+            config.webservice.hostname,
+            config.webservice.port,
+            cls.version,
+            (resource[1:] if resource.startswith('/') else resource),
+            )
+
+    @staticmethod
+    def from_uuid(uuid):
+        """See `IAPI`."""
+        return uuid.int
+
+    @staticmethod
+    def to_uuid(uuid_repr):
+        """See `IAPI`."""
+        return UUID(int=int(uuid_repr))
+
+
+@implementer(IAPI)
+class API31:
+    version = '3.1'
+
+    @classmethod
+    def path_to(cls, resource):
+        """See `IAPI`."""
+        return '{}://{}:{}/{}/{}'.format(
+            ('https' if as_boolean(config.webservice.use_https) else 'http'),
+            config.webservice.hostname,
+            config.webservice.port,
+            cls.version,
+            (resource[1:] if resource.startswith('/') else resource),
+            )
+
+    @staticmethod
+    def from_uuid(uuid):
+        """See `IAPI`."""
+        return uuid.hex
+
+    @staticmethod
+    def to_uuid(uuid_repr):
+        """See `IAPI`."""
+        return UUID(hex=uuid_repr)
</span></code></pre>

<br>
</li>
<li id='diff-4'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-4'>
 <strong>
src/mailman/interfaces/api.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- /dev/null </span><span style="color: #000000;background-color: \
#ddffdd">+++ b/src/mailman/interfaces/api.py </span><span style="color: #aaaaaa">@@ \
-0,0 +1,65 @@ </span><span style="color: #000000;background-color: #ddffdd">+# \
Copyright (C) 2016 by the Free Software Foundation, Inc. +#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+"""REST web service API context."""
+
+__all__ = [
+    'IAPI',
+    ]
+
+
+from zope.interface import Attribute, Interface
+
+
+class IAPI(Interface):
+    """The REST web service context."""
+
+    version = Attribute("""The REST API version.""")
+
+    def path_to(resource):
+        """Return the full REST URL to the given resource.
+
+        :param resource: Resource path string without the leading scheme,
+            host, port, or API version information.
+        :type resource: str
+        :return: Full URL path to the resource, with the scheme, host, port
+            and API version prepended.
+        :rtype: str
+        """
+
+    def from_uuid(uuid):
+        """Return the string representation of a UUID.
+
+        :param uuid: The UUID to convert.
+        :type uuid: UUID
+        :return: The string representation of the UUID, as appropriate for the
+            API version.  In 3.0 this is the representation of an integer,
+            while in 3.1 it is the hex representation.
+        :rtype: str
+        """
+
+    def to_uuid(uuid):
+        """Return the UUID from the string representation.
+
+        :param uuid: The string representation of the UUID.
+        :type uuid: str
+        :return: The UUID converted from the string representation, as
+            appropriate for the API version.  In 3.0, uuid is interpreted as
+            the integer representation of a UUID, while in 3.1 it is the hex
+            representation of the UUID.
+        :rtype: UUID
+        """
</span></code></pre>

<br>
</li>
<li id='diff-5'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-5'>
 <strong>
src/mailman/rest/addresses.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/addresses.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/addresses.py </span><span \
style="color: #aaaaaa">@@ -51,7 +51,7 @@ class _AddressBase(CollectionMixin): </span> \
email=address.email,  original_email=address.original_email,
             registered_on=address.registered_on,
<span style="color: #000000;background-color: #ffdddd">-            \
self_link=self.path_to('addresses/{}'.format(address.email)), </span><span \
style="color: #000000;background-color: #ddffdd">+            \
self_link=self.api.path_to('addresses/{}'.format(address.email)), </span>             \
                )
         # Add optional attributes.  These can be None or the empty string.
         if address.display_name:
<span style="color: #aaaaaa">@@ -59,9 +59,8 @@ class _AddressBase(CollectionMixin):
</span>         if address.verified_on:
             representation['verified_on'] = address.verified_on
         if address.user:
<span style="color: #000000;background-color: #ffdddd">-            uid = \
                getattr(address.user.user_id,
-                          'int' if self.api_version == '3.0' else 'hex')
-            representation['user'] = self.path_to('users/{}'.format(uid))
</span><span style="color: #000000;background-color: #ddffdd">+            uid = \
self.api.from_uuid(address.user.user_id) +            representation['user'] = \
self.api.path_to('users/{}'.format(uid)) </span>         return representation
 
     def _get_collection(self, request):
<span style="color: #aaaaaa">@@ -214,14 +213,15 @@ class UserAddresses(_AddressBase):
</span>             address = user_manager.get_address(validator(request)['email'])
             if address.user is None:
                 address.user = self._user
<span style="color: #000000;background-color: #ffdddd">-                location = \
self.path_to('addresses/{}'.format(address.email)) </span><span style="color: \
#000000;background-color: #ddffdd">+                location = self.api.path_to( +    \
'addresses/{}'.format(address.email)) </span>                 created(response, \
location)  else:
                 bad_request(response, 'Address belongs to other user.')
         else:
             # Link the address to the current user and return it.
             address.user = self._user
<span style="color: #000000;background-color: #ffdddd">-            location = \
self.path_to('addresses/{}'.format(address.email)) </span><span style="color: \
#000000;background-color: #ddffdd">+            location = \
self.api.path_to('addresses/{}'.format(address.email)) </span>             \
created(response, location)  
 
</code></pre>

<br>
</li>
<li id='diff-6'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-6'>
 <strong>
src/mailman/rest/docs/helpers.rst
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/docs/helpers.rst </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/docs/helpers.rst \
</span><span style="color: #aaaaaa">@@ -5,34 +5,6 @@ REST API helpers </span> There \
are a number of helpers that make building out the REST API easier.  
 
<span style="color: #000000;background-color: #ffdddd">-Resource paths
-==============
-
-For example, most resources don't have to worry about where they are rooted.
-They only need to know where they are relative to the root URI, and this
-function can return them the full path to the resource.  We have to pass in
-the REST API version because there is no request in flight.
-
-    &gt;&gt;&gt; from mailman.rest.helpers import path_to
-    &gt;&gt;&gt; print(path_to('system', '3.0'))
-    http://localhost:9001/3.0/system
-
-Parameters like the ``scheme``, ``host``, and ``port`` can be set in the
-configuration file.
-::
-
-    &gt;&gt;&gt; config.push('helpers', """
-    ... [webservice]
-    ... hostname: geddy
-    ... port: 2112
-    ... use_https: yes
-    ... """)
-    &gt;&gt;&gt; cleanups.append((config.pop, 'helpers'))
-
-    &gt;&gt;&gt; print(path_to('system', '4.2'))
-    https://geddy:2112/4.2/system
-
-
</span> Etags
 =====
 
</code></pre>

<br>
</li>
<li id='diff-7'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-7'>
 <strong>
src/mailman/rest/domains.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/domains.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/domains.py </span><span \
style="color: #aaaaaa">@@ -44,7 +44,7 @@ class _DomainBase(CollectionMixin): </span>  \
base_url=domain.base_url,  description=domain.description,
             mail_host=domain.mail_host,
<span style="color: #000000;background-color: #ffdddd">-            \
self_link=self.path_to('domains/{}'.format(domain.mail_host)), </span><span \
style="color: #000000;background-color: #ddffdd">+            \
self_link=self.api.path_to('domains/{}'.format(domain.mail_host)), </span>            \
url_host=domain.url_host,  )
 
<span style="color: #aaaaaa">@@ -123,7 +123,7 @@ class AllDomains(_DomainBase):
</span>         except BadDomainSpecificationError as error:
             bad_request(response, str(error))
         else:
<span style="color: #000000;background-color: #ffdddd">-            location = \
self.path_to('domains/{}'.format(domain.mail_host)) </span><span style="color: \
#000000;background-color: #ddffdd">+            location = \
self.api.path_to('domains/{}'.format(domain.mail_host)) </span>             \
created(response, location)  
     def on_get(self, request, response):
</code></pre>

<br>
</li>
<li id='diff-8'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-8'>
 <strong>
src/mailman/rest/helpers.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/helpers.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/helpers.py </span><span \
style="color: #aaaaaa">@@ -32,7 +32,6 @@ __all__ = [ </span>     'no_content',
     'not_found',
     'okay',
<span style="color: #000000;background-color: #ffdddd">-    'path_to',
</span>     ]
 
 
<span style="color: #aaaaaa">@@ -48,27 +47,6 @@ from pprint import pformat
</span> 
 
 
<span style="color: #000000;background-color: #ffdddd">-def path_to(resource, \
                api_version):
-    """Return the url path to a resource.
-
-    :param resource: The canonical path to the resource, relative to the
-        system base URI.
-    :type resource: string
-    :param api_version: API version to report.
-    :type api_version: string
-    :return: The full path to the resource.
-    :rtype: bytes
-    """
-    return '{0}://{1}:{2}/{3}/{4}'.format(
-        ('https' if as_boolean(config.webservice.use_https) else 'http'),
-        config.webservice.hostname,
-        config.webservice.port,
-        api_version,
-        (resource[1:] if resource.startswith('/') else resource),
-        )
-
-
-
</span> class ExtendedEncoder(json.JSONEncoder):
     """An extended JSON encoder which knows about other data types."""
 
<span style="color: #aaaaaa">@@ -185,9 +163,6 @@ class CollectionMixin:
</span>             result['entries'] = entries
         return result
 
<span style="color: #000000;background-color: #ffdddd">-    def path_to(self, \
                resource):
-        return path_to(resource, self.api_version)
-
</span> 
 
 class GetterSetter:
</code></pre>

<br>
</li>
<li id='diff-9'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-9'>
 <strong>
src/mailman/rest/lists.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/lists.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/lists.py </span><span \
style="color: #aaaaaa">@@ -110,7 +110,7 @@ class _ListBase(CollectionMixin): </span>  \
mail_host=mlist.mail_host,  member_count=mlist.members.member_count,
             volume=mlist.volume,
<span style="color: #000000;background-color: #ffdddd">-            \
self_link=self.path_to('lists/{}'.format(mlist.list_id)), </span><span style="color: \
#000000;background-color: #ddffdd">+            \
self_link=self.api.path_to('lists/{}'.format(mlist.list_id)), </span>             )
 
     def _get_collection(self, request):
<span style="color: #aaaaaa">@@ -155,7 +155,7 @@ class AList(_ListBase):
</span>             email, self._mlist.list_id, role)
         if member is None:
             return NotFound(), []
<span style="color: #000000;background-color: #ffdddd">-        return \
AMember(request.context['api_version'], member.member_id) </span><span style="color: \
#000000;background-color: #ddffdd">+        return AMember(request.context['api'], \
member.member_id) </span> 
     @child(roster_matcher)
     def roster(self, request, segments, role):
<span style="color: #aaaaaa">@@ -216,7 +216,7 @@ class AllLists(_ListBase):
</span>             reason = 'Domain does not exist: {}'.format(error.domain)
             bad_request(response, reason.encode('utf-8'))
         else:
<span style="color: #000000;background-color: #ffdddd">-            location = \
self.path_to('lists/{0}'.format(mlist.list_id)) </span><span style="color: \
#000000;background-color: #ddffdd">+            location = \
self.api.path_to('lists/{0}'.format(mlist.list_id)) </span>             \
created(response, location)  
     def on_get(self, request, response):
</code></pre>

<br>
</li>
<li id='diff-10'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-10'>
 <strong>
src/mailman/rest/members.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/members.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/members.py </span><span \
style="color: #aaaaaa">@@ -27,7 +27,7 @@ __all__ = [ </span> 
 from mailman.app.membership import add_member, delete_member
 from mailman.interfaces.action import Action
<span style="color: #000000;background-color: #ffdddd">-from \
mailman.interfaces.address import IAddress, InvalidEmailAddressError </span><span \
style="color: #000000;background-color: #ddffdd">+from mailman.interfaces.address \
import IAddress </span> from mailman.interfaces.listmanager import IListManager
 from mailman.interfaces.member import (
     AlreadySubscribedError, DeliveryMode, MemberRole, MembershipError,
<span style="color: #aaaaaa">@@ -43,7 +43,6 @@ from mailman.rest.helpers import (
</span> from mailman.rest.preferences import Preferences, ReadOnlyPreferences
 from mailman.rest.validator import (
     Validator, enum_validator, subscriber_validator)
<span style="color: #000000;background-color: #ffdddd">-from operator import \
attrgetter </span> from uuid import UUID
 from zope.component import getUtility
 
<span style="color: #aaaaaa">@@ -52,10 +51,6 @@ from zope.component import getUtility
</span> class _MemberBase(CollectionMixin):
     """Shared base class for member representations."""
 
<span style="color: #000000;background-color: #ffdddd">-    def _get_uuid(self, \
                member):
-        return getattr(member.member_id,
-                       'int' if self.api_version == '3.0' else 'hex')
-
</span>     def _resource_as_dict(self, member):
         """See `CollectionMixin`."""
         enum, dot, role = str(member.role).partition('.')
<span style="color: #aaaaaa">@@ -66,23 +61,23 @@ class _MemberBase(CollectionMixin):
</span>         # member_id are UUIDs.  In API 3.0 we use the integer equivalent of
         # the UID in the URL, but in API 3.1 we use the hex equivalent.  See
         # issue #121 for details.
<span style="color: #000000;background-color: #ffdddd">-        member_id = \
self._get_uuid(member) </span><span style="color: #000000;background-color: \
#ddffdd">+        member_id = self.api.from_uuid(member.member_id) </span>         \
response = dict( <span style="color: #000000;background-color: #ffdddd">-            \
address=self.path_to('addresses/{}'.format(member.address.email)), </span><span \
style="color: #000000;background-color: #ddffdd">+            \
address=self.api.path_to( +                \
'addresses/{}'.format(member.address.email)), </span>             \
delivery_mode=member.delivery_mode,  email=member.address.email,
             list_id=member.list_id,
             member_id=member_id,
             moderation_action=member.moderation_action,
             role=role,
<span style="color: #000000;background-color: #ffdddd">-            \
self_link=self.path_to('members/{}'.format(member_id)), </span><span style="color: \
#000000;background-color: #ddffdd">+            \
self_link=self.api.path_to('members/{}'.format(member_id)), </span>             )
         # Add the user link if there is one.
         user = member.user
         if user is not None:
<span style="color: #000000;background-color: #ffdddd">-            user_id = \
                getattr(user.user_id,
-                              'int' if self.api_version == '3.0' else 'hex')
-            response['user'] = self.path_to('users/{}'.format(user_id))
</span><span style="color: #000000;background-color: #ddffdd">+            user_id = \
self.api.from_uuid(user.user_id) +            response['user'] = \
self.api.path_to('users/{}'.format(user_id)) </span>         return response
 
     def _get_collection(self, request):
<span style="color: #aaaaaa">@@ -112,16 +107,13 @@ class \
MemberCollection(_MemberBase): </span> class AMember(_MemberBase):
     """A member."""
 
<span style="color: #000000;background-color: #ffdddd">-    def __init__(self, \
api_version, member_id_string): </span><span style="color: #000000;background-color: \
#ddffdd">+    def __init__(self, api, member_id_string): </span>         # The \
                member_id_string is the string representation of the member's
         # UUID.  In API 3.0, the argument is the string representation of the
         # int representation of the UUID.  In API 3.1 it's the hex.
<span style="color: #000000;background-color: #ffdddd">-        self.api_version = \
api_version </span><span style="color: #000000;background-color: #ddffdd">+        \
self.api = api </span>         try:
<span style="color: #000000;background-color: #ffdddd">-            if api_version == \
                '3.0':
-                member_id = UUID(int=int(member_id_string))
-            else:
-                member_id = UUID(hex=member_id_string)
</span><span style="color: #000000;background-color: #ddffdd">+            member_id \
= api.to_uuid(member_id_string) </span>         except ValueError:
             # The string argument could not be converted to a UUID.
             self._member = None
<span style="color: #aaaaaa">@@ -143,7 +135,7 @@ class AMember(_MemberBase):
</span>             return NotFound(), []
         if self._member is None:
             return NotFound(), []
<span style="color: #000000;background-color: #ffdddd">-        member_id = \
self._get_uuid(self._member) </span><span style="color: #000000;background-color: \
#ddffdd">+        member_id = self.api.from_uuid(self._member.member_id) </span>      \
child = Preferences(  self._member.preferences, 'members/{}'.format(member_id))
         return child, []
<span style="color: #aaaaaa">@@ -157,7 +149,8 @@ class AMember(_MemberBase):
</span>             return NotFound(), []
         child = ReadOnlyPreferences(
             self._member,
<span style="color: #000000;background-color: #ffdddd">-            \
'members/{}/all'.format(self._get_uuid(self._member))) </span><span style="color: \
#000000;background-color: #ddffdd">+            'members/{}/all'.format( +            \
self.api.from_uuid(self._member.member_id))) </span>         return child, []
 
     def on_delete(self, request, response):
<span style="color: #aaaaaa">@@ -220,7 +213,7 @@ class AllMembers(_MemberBase):
</span>         try:
             validator = Validator(
                 list_id=str,
<span style="color: #000000;background-color: #ffdddd">-                \
subscriber=subscriber_validator(self.api_version), </span><span style="color: \
#000000;background-color: #ddffdd">+                \
subscriber=subscriber_validator(self.api), </span>                 display_name=str,
                 delivery_mode=enum_validator(DeliveryMode),
                 role=enum_validator(MemberRole),
<span style="color: #aaaaaa">@@ -292,8 +285,8 @@ class AllMembers(_MemberBase):
</span>                 # and return the location to the new member.  Member ids are
                 # UUIDs and need to be converted to URLs because JSON doesn't
                 # directly support UUIDs.
<span style="color: #000000;background-color: #ffdddd">-                member_id = \
                self._get_uuid(member)
-                location = self.path_to('members/{}'.format(member_id))
</span><span style="color: #000000;background-color: #ddffdd">+                \
member_id = self.api.from_uuid(member.member_id) +                location = \
self.api.path_to('members/{}'.format(member_id)) </span>                 \
created(response, location)  return
             # The member could not be directly subscribed because there are
<span style="color: #aaaaaa">@@ -339,8 +332,8 @@ class AllMembers(_MemberBase):
</span>         # and return the location to the new member.  Member ids are
         # UUIDs and need to be converted to URLs because JSON doesn't
         # directly support UUIDs.
<span style="color: #000000;background-color: #ffdddd">-        member_id = \
                self._get_uuid(member)
-        location = self.path_to('members/{}'.format(member_id))
</span><span style="color: #000000;background-color: #ddffdd">+        member_id = \
self.api.from_uuid(member.member_id) +        location = \
self.api.path_to('members/{}'.format(member_id)) </span>         created(response, \
location)  
     def on_get(self, request, response):
<span style="color: #aaaaaa">@@ -353,10 +346,10 @@ class AllMembers(_MemberBase):
</span> class _FoundMembers(MemberCollection):
     """The found members collection."""
 
<span style="color: #000000;background-color: #ffdddd">-    def __init__(self, \
members, api_version): </span><span style="color: #000000;background-color: \
#ddffdd">+    def __init__(self, members, api): </span>         super().__init__()
         self._members = members
<span style="color: #000000;background-color: #ffdddd">-        self.api_version = \
api_version </span><span style="color: #000000;background-color: #ddffdd">+        \
self.api = api </span> 
     def _get_collection(self, request):
         """See `CollectionMixin`."""
<span style="color: #aaaaaa">@@ -380,5 +373,5 @@ class FindMembers(_MemberBase):
</span>             bad_request(response, str(error))
         else:
             members = service.find_members(**data)
<span style="color: #000000;background-color: #ffdddd">-            resource = \
_FoundMembers(members, self.api_version) </span><span style="color: \
#000000;background-color: #ddffdd">+            resource = _FoundMembers(members, \
self.api) </span>             okay(response, \
etag(resource._make_collection(request))) </code></pre>

<br>
</li>
<li id='diff-11'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-11'>
 <strong>
src/mailman/rest/post_moderation.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/post_moderation.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/post_moderation.py \
</span><span style="color: #aaaaaa">@@ -28,8 +28,7 @@ from mailman.interfaces.action \
import Action </span> from mailman.interfaces.messages import IMessageStore
 from mailman.interfaces.requests import IListRequests, RequestType
 from mailman.rest.helpers import (
<span style="color: #000000;background-color: #ffdddd">-    CollectionMixin, \
                bad_request, child, etag, no_content, not_found, okay,
-    path_to)
</span><span style="color: #000000;background-color: #ddffdd">+    CollectionMixin, \
bad_request, child, etag, no_content, not_found, okay) </span> from \
mailman.rest.validator import Validator, enum_validator  from zope.component import \
getUtility  
<span style="color: #aaaaaa">@@ -62,9 +61,8 @@ class _ModerationBase:
</span>         # that's fine too.
         resource.pop('id', None)
         # Add a self_link.
<span style="color: #000000;background-color: #ffdddd">-        resource['self_link'] \
                = path_to(
-            'lists/{}/held/{}'.format(self._mlist.list_id, request_id),
-            self.api_version)
</span><span style="color: #000000;background-color: #ddffdd">+        \
resource['self_link'] = self.api.path_to( +            \
'lists/{}/held/{}'.format(self._mlist.list_id, request_id)) </span>         return \
resource  
 
</code></pre>

<br>
</li>
<li id='diff-12'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-12'>
 <strong>
src/mailman/rest/preferences.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/preferences.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/preferences.py </span><span \
style="color: #aaaaaa">@@ -26,7 +26,7 @@ __all__ = [ </span> from lazr.config import \
as_boolean  from mailman.interfaces.member import DeliveryMode, DeliveryStatus
 from mailman.rest.helpers import (
<span style="color: #000000;background-color: #ffdddd">-    GetterSetter, \
bad_request, etag, no_content, not_found, okay, path_to) </span><span style="color: \
#000000;background-color: #ddffdd">+    GetterSetter, bad_request, etag, no_content, \
not_found, okay) </span> from mailman.rest.validator import (
     Validator, enum_validator, language_validator)
 
<span style="color: #aaaaaa">@@ -64,9 +64,8 @@ class ReadOnlyPreferences:
</span>         if preferred_language is not None:
             resource['preferred_language'] = preferred_language.code
         # Add the self link.
<span style="color: #000000;background-color: #ffdddd">-        resource['self_link'] \
                = path_to(
-            '{}/preferences'.format(self._base_url),
-            self.api_version)
</span><span style="color: #000000;background-color: #ddffdd">+        \
resource['self_link'] = self.api.path_to( +            \
'{}/preferences'.format(self._base_url)) </span>         okay(response, \
etag(resource))  
 
</code></pre>

<br>
</li>
<li id='diff-13'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-13'>
 <strong>
src/mailman/rest/queues.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/queues.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/queues.py </span><span \
style="color: #aaaaaa">@@ -46,7 +46,7 @@ class _QueuesBase(CollectionMixin): </span>  \
directory=switchboard.queue_directory,  count=len(files),
             files=files,
<span style="color: #000000;background-color: #ffdddd">-            \
self_link=self.path_to('queues/{}'.format(name)), </span><span style="color: \
#000000;background-color: #ddffdd">+            \
self_link=self.api.path_to('queues/{}'.format(name)), </span>             )
 
     def _get_collection(self, request):
<span style="color: #aaaaaa">@@ -89,7 +89,7 @@ class AQueue(_QueuesBase):
</span>             bad_request(response, str(error))
             return
         else:
<span style="color: #000000;background-color: #ffdddd">-            location = \
self.path_to( </span><span style="color: #000000;background-color: #ddffdd">+         \
location = self.api.path_to( </span>                 \
'queues/{}/{}'.format(self._name, filebase))  created(response, location)
 
<span style="color: #aaaaaa">@@ -122,5 +122,5 @@ class AllQueues(_QueuesBase):
</span>     def on_get(self, request, response):
         """&lt;api&gt;/queues"""
         resource = self._make_collection(request)
<span style="color: #000000;background-color: #ffdddd">-        resource['self_link'] \
= self.path_to('queues') </span><span style="color: #000000;background-color: \
#ddffdd">+        resource['self_link'] = self.api.path_to('queues') </span>         \
okay(response, etag(resource)) </code></pre>

<br>
</li>
<li id='diff-14'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-14'>
 <strong>
src/mailman/rest/root.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/root.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/root.py </span><span \
style="color: #aaaaaa">@@ -26,6 +26,7 @@ import falcon </span> 
 from base64 import b64decode
 from mailman.config import config
<span style="color: #000000;background-color: #ddffdd">+from mailman.core.api import \
API30, API31 </span> from mailman.core.constants import system_preferences
 from mailman.core.system import system
 from mailman.interfaces.listmanager import IListManager
<span style="color: #aaaaaa">@@ -33,7 +34,7 @@ from mailman.model.uid import UID
</span> from mailman.rest.addresses import AllAddresses, AnAddress
 from mailman.rest.domains import ADomain, AllDomains
 from mailman.rest.helpers import (
<span style="color: #000000;background-color: #ffdddd">-    BadRequest, NotFound, \
child, etag, no_content, not_found, okay, path_to) </span><span style="color: \
#000000;background-color: #ddffdd">+    BadRequest, NotFound, child, etag, \
no_content, not_found, okay) </span> from mailman.rest.lists import AList, AllLists, \
Styles  from mailman.rest.members import AMember, AllMembers, FindMembers
 from mailman.rest.preferences import ReadOnlyPreferences
<span style="color: #aaaaaa">@@ -59,7 +60,7 @@ class Root:
</span>     @child('3.0')
     def api_version_30(self, request, segments):
         # API version 3.0 was introduced in Mailman 3.0.
<span style="color: #000000;background-color: #ffdddd">-        \
request.context['api_version'] = '3.0' </span><span style="color: \
#000000;background-color: #ddffdd">+        request.context['api'] = API30 </span>    \
return self._check_authorization(request, segments)  
     @child('3.1')
<span style="color: #aaaaaa">@@ -68,7 +69,7 @@ class Root:
</span>         # incompatible difference is that uuids are represented as hex \
                strings
         # instead of 128 bit integers.  The latter is not compatible with all
         # versions of JavaScript.
<span style="color: #000000;background-color: #ffdddd">-        \
request.context['api_version'] = '3.1' </span><span style="color: \
#000000;background-color: #ddffdd">+        request.context['api'] = API31 </span>    \
return self._check_authorization(request, segments)  
     def _check_authorization(self, request, segments):
<span style="color: #aaaaaa">@@ -102,8 +103,8 @@ class Versions:
</span>         resource = dict(
             mailman_version=system.mailman_version,
             python_version=system.python_version,
<span style="color: #000000;background-color: #ffdddd">-            \
                api_version=self.api_version,
-            self_link=path_to('system/versions', self.api_version),
</span><span style="color: #000000;background-color: #ddffdd">+            \
api_version=self.api.version, +            \
self_link=self.api.path_to('system/versions'), </span>             )
         okay(response, etag(resource))
 
<span style="color: #aaaaaa">@@ -179,12 +180,12 @@ class TopLevel:
</span>         """
         if len(segments) == 0:
             resource = AllAddresses()
<span style="color: #000000;background-color: #ffdddd">-            \
resource.api_version = request.context['api_version'] </span><span style="color: \
#000000;background-color: #ddffdd">+            resource.api = request.context['api'] \
</span>             return resource  else:
             email = segments.pop(0)
             resource = AnAddress(email)
<span style="color: #000000;background-color: #ffdddd">-            \
resource.api_version = request.context['api_version'] </span><span style="color: \
#000000;background-color: #ddffdd">+            resource.api = request.context['api'] \
</span>             return resource, segments  
     @child()
<span style="color: #aaaaaa">@@ -218,32 +219,32 @@ class TopLevel:
</span>     @child()
     def members(self, request, segments):
         """/&lt;api&gt;/members"""
<span style="color: #000000;background-color: #ffdddd">-        api_version = \
request.context['api_version'] </span><span style="color: #000000;background-color: \
#ddffdd">+        api = request.context['api'] </span>         if len(segments) == 0:
             resource = AllMembers()
<span style="color: #000000;background-color: #ffdddd">-            \
resource.api_version = api_version </span><span style="color: \
#000000;background-color: #ddffdd">+            resource.api = api </span>            \
                return resource
         # Either the next segment is the string "find" or a member id.  They
         # cannot collide.
         segment = segments.pop(0)
         if segment == 'find':
             resource = FindMembers()
<span style="color: #000000;background-color: #ffdddd">-            \
resource.api_version = api_version </span><span style="color: \
#000000;background-color: #ddffdd">+            resource.api = api </span>         \
else: <span style="color: #000000;background-color: #ffdddd">-            resource = \
AMember(api_version, segment) </span><span style="color: #000000;background-color: \
#ddffdd">+            resource = AMember(api, segment) </span>         return \
resource, segments  
     @child()
     def users(self, request, segments):
         """/&lt;api&gt;/users"""
<span style="color: #000000;background-color: #ffdddd">-        api_version = \
request.context['api_version'] </span><span style="color: #000000;background-color: \
#ddffdd">+        api = request.context['api'] </span>         if len(segments) == 0:
             resource = AllUsers()
<span style="color: #000000;background-color: #ffdddd">-            \
resource.api_version = api_version </span><span style="color: \
#000000;background-color: #ddffdd">+            resource.api = api </span>            \
return resource  else:
             user_id = segments.pop(0)
<span style="color: #000000;background-color: #ffdddd">-            return \
AUser(api_version, user_id), segments </span><span style="color: \
#000000;background-color: #ddffdd">+            return AUser(api, user_id), segments \
</span>   @child()
     def owners(self, request, segments):
<span style="color: #aaaaaa">@@ -252,7 +253,7 @@ class TopLevel:
</span>             return BadRequest(), []
         else:
             resource = ServerOwners()
<span style="color: #000000;background-color: #ffdddd">-            \
resource.api_version = request.context['api_version'] </span><span style="color: \
#000000;background-color: #ddffdd">+            resource.api = request.context['api'] \
</span>             return resource, segments  
     @child()
</code></pre>

<br>
</li>
<li id='diff-15'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-15'>
 <strong>
src/mailman/rest/tests/test_addresses.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/tests/test_addresses.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/tests/test_addresses.py \
</span><span style="color: #aaaaaa">@@ -369,6 +369,18 @@ class \
TestAddresses(unittest.TestCase): </span>                 })
         self.assertEqual(cm.exception.code, 403)
 
<span style="color: #000000;background-color: #ddffdd">+    def \
test_user_subresource_post_no_such_user(self): +        # Try to link an address to a \
nonexistent user. +        with transaction():
+            getUtility(IUserManager).create_address('anne@example.com')
+        with self.assertRaises(HTTPError) as cm:
+            call_api(
+                'http://localhost:9001/3.0/addresses/anne@example.com/user', {
+                    'user_id': 2,
+                    })
+        self.assertEqual(cm.exception.code, 400)
+        self.assertEqual(cm.exception.reason, b'No user with ID 2')
+
</span>     def test_user_subresource_unlink(self):
         # By DELETEing the usr subresource, you can unlink a user from an
         # address.
<span style="color: #aaaaaa">@@ -542,7 +554,7 @@ class \
TestAPI31Addresses(unittest.TestCase): </span>                     })
         self.assertEqual(cm.exception.code, 400)
         self.assertEqual(cm.exception.reason,
<span style="color: #000000;background-color: #ffdddd">-                         \
b'badly formed hexadecimal UUID string') </span><span style="color: \
#000000;background-color: #ddffdd">+                         b'Cannot convert \
parameters: user_id') </span> 
     def test_user_subresource_put(self):
         # By PUTing to the 'user' resource, you can change the user that an
<span style="color: #aaaaaa">@@ -577,4 +589,4 @@ class \
TestAPI31Addresses(unittest.TestCase): </span>                     }, method='PUT')
         self.assertEqual(cm.exception.code, 400)
         self.assertEqual(cm.exception.reason,
<span style="color: #000000;background-color: #ffdddd">-                         \
b'badly formed hexadecimal UUID string') </span><span style="color: \
#000000;background-color: #ddffdd">+                         b'Cannot convert \
parameters: user_id') </span></code></pre>

<br>
</li>
<li id='diff-16'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-16'>
 <strong>
src/mailman/rest/tests/test_validator.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/tests/test_validator.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/tests/test_validator.py \
</span><span style="color: #aaaaaa">@@ -25,6 +25,7 @@ __all__ = [ </span> import \
unittest  
 from mailman.interfaces.usermanager import IUserManager
<span style="color: #000000;background-color: #ddffdd">+from mailman.core.api import \
API30, API31 </span> from mailman.rest.validator import (
     list_of_strings_validator, subscriber_validator)
 from mailman.testing.layers import RESTLayer
<span style="color: #aaaaaa">@@ -53,35 +54,35 @@ class \
TestValidators(unittest.TestCase): </span>     def \
test_subscriber_validator_int_uuid(self):  # Convert from an existing user id to a \
UUID.  anne = getUtility(IUserManager).make_user('anne@example.com')
<span style="color: #000000;background-color: #ffdddd">-        uuid = \
subscriber_validator('3.0')(str(anne.user_id.int)) </span><span style="color: \
#000000;background-color: #ddffdd">+        uuid = \
subscriber_validator(API30)(str(anne.user_id.int)) </span>         \
self.assertEqual(anne.user_id, uuid)  
     def test_subscriber_validator_hex_uuid(self):
         # Convert from an existing user id to a UUID.
         anne = getUtility(IUserManager).make_user('anne@example.com')
<span style="color: #000000;background-color: #ffdddd">-        uuid = \
subscriber_validator('3.1')(anne.user_id.hex) </span><span style="color: \
#000000;background-color: #ddffdd">+        uuid = \
subscriber_validator(API31)(anne.user_id.hex) </span>         \
self.assertEqual(anne.user_id, uuid)  
     def test_subscriber_validator_no_int_uuid(self):
         # API 3.1 does not accept ints as subscriber id's.
         anne = getUtility(IUserManager).make_user('anne@example.com')
         self.assertRaises(ValueError,
<span style="color: #000000;background-color: #ffdddd">-                          \
subscriber_validator('3.1'), str(anne.user_id.int)) </span><span style="color: \
#000000;background-color: #ddffdd">+                          \
subscriber_validator(API31), str(anne.user_id.int)) </span> 
     def test_subscriber_validator_bad_int_uuid(self):
         # In API 3.0, UUIDs are ints.
         self.assertRaises(ValueError,
<span style="color: #000000;background-color: #ffdddd">-                          \
subscriber_validator('3.0'), 'not-a-thing') </span><span style="color: \
#000000;background-color: #ddffdd">+                          \
subscriber_validator(API30), 'not-a-thing') </span> 
     def test_subscriber_validator_bad_int_hex(self):
         # In API 3.1, UUIDs are hexes.
         self.assertRaises(ValueError,
<span style="color: #000000;background-color: #ffdddd">-                          \
subscriber_validator('3.1'), 'not-a-thing') </span><span style="color: \
#000000;background-color: #ddffdd">+                          \
subscriber_validator(API31), 'not-a-thing') </span> 
     def test_subscriber_validator_email_address_API30(self):
<span style="color: #000000;background-color: #ffdddd">-        \
self.assertEqual(subscriber_validator('3.0')('anne@example.com'), </span><span \
style="color: #000000;background-color: #ddffdd">+        \
self.assertEqual(subscriber_validator(API30)('anne@example.com'), </span>             \
'anne@example.com')  
     def test_subscriber_validator_email_address_API31(self):
<span style="color: #000000;background-color: #ffdddd">-        \
self.assertEqual(subscriber_validator('3.1')('anne@example.com'), </span><span \
style="color: #000000;background-color: #ddffdd">+        \
self.assertEqual(subscriber_validator(API31)('anne@example.com'), </span>             \
'anne@example.com') </code></pre>

<br>
</li>
<li id='diff-17'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-17'>
 <strong>
src/mailman/rest/users.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/users.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/users.py </span><span \
style="color: #aaaaaa">@@ -35,12 +35,11 @@ from mailman.interfaces.usermanager import \
IUserManager </span> from mailman.rest.addresses import UserAddresses
 from mailman.rest.helpers import (
     BadRequest, CollectionMixin, GetterSetter, NotFound, bad_request, child,
<span style="color: #000000;background-color: #ffdddd">-    conflict, created, etag, \
forbidden, no_content, not_found, okay, path_to) </span><span style="color: \
#000000;background-color: #ddffdd">+    conflict, created, etag, forbidden, \
no_content, not_found, okay) </span> from mailman.rest.preferences import Preferences
 from mailman.rest.validator import (
     PatchValidator, Validator, list_of_strings_validator)
 from passlib.utils import generate_password as generate
<span style="color: #000000;background-color: #ffdddd">-from uuid import UUID
</span> from zope.component import getUtility
 
 
<span style="color: #aaaaaa">@@ -117,9 +116,9 @@ def create_user(arguments, request, \
response): </span>         password = generate(int(config.passwords.password_length))
     user.password = config.password_context.encrypt(password)
     user.is_server_owner = is_server_owner
<span style="color: #000000;background-color: #ffdddd">-    api_version = \
                request.context['api_version']
-    user_id = getattr(user.user_id, 'int' if api_version == '3.0' else 'hex')
-    location = path_to('users/{}'.format(user_id), api_version)
</span><span style="color: #000000;background-color: #ddffdd">+    api = \
request.context['api'] +    user_id = api.from_uuid(user.user_id)
+    location = request.context.get('api').path_to('users/{}'.format(user_id))
</span>     created(response, location)
     return user
 
<span style="color: #aaaaaa">@@ -128,21 +127,17 @@ def create_user(arguments, \
request, response): </span> class _UserBase(CollectionMixin):
     """Shared base class for user representations."""
 
<span style="color: #000000;background-color: #ffdddd">-    def _get_uuid(self, \
                user):
-        return getattr(user.user_id,
-                       'int' if self.api_version == '3.0' else 'hex')
-
</span>     def _resource_as_dict(self, user):
         """See `CollectionMixin`."""
         # The canonical URL for a user is their unique user id, although we
         # can always look up a user based on any registered and validated
         # email address associated with their account.  The user id is a UUID,
         # but we serialize its integer equivalent.
<span style="color: #000000;background-color: #ffdddd">-        user_id = \
self._get_uuid(user) </span><span style="color: #000000;background-color: #ddffdd">+  \
user_id = self.api.from_uuid(user.user_id) </span>         resource = dict(
             created_on=user.created_on,
             is_server_owner=user.is_server_owner,
<span style="color: #000000;background-color: #ffdddd">-            \
self_link=self.path_to('users/{}'.format(user_id)), </span><span style="color: \
#000000;background-color: #ddffdd">+            \
self_link=self.api.path_to('users/{}'.format(user_id)), </span>             \
user_id=user_id,  )
         # Add the password attribute, only if the user has a password.  Same
<span style="color: #aaaaaa">@@ -182,9 +177,11 @@ class AllUsers(_UserBase):
</span> class AUser(_UserBase):
     """A user."""
 
<span style="color: #000000;background-color: #ffdddd">-    def __init__(self, \
api_version, user_identifier): </span><span style="color: #000000;background-color: \
#ddffdd">+    def __init__(self, api, user_identifier): </span>         """Get a user \
by various type of identifiers.  
<span style="color: #000000;background-color: #ddffdd">+        :param api: The REST \
API object. +        :type api: IAPI
</span>         :param user_identifier: The identifier used to retrieve the user.  \
                The
             identifier may either be an email address controlled by the user
             or the UUID of the user.  The type of identifier is auto-detected
<span style="color: #aaaaaa">@@ -193,7 +190,7 @@ class AUser(_UserBase):
</span>             API 3.0 are integers, while in 3.1 are hex.
         :type user_identifier: string
         """
<span style="color: #000000;background-color: #ffdddd">-        self.api_version = \
api_version </span><span style="color: #000000;background-color: #ddffdd">+        \
self.api = api </span>         user_manager = getUtility(IUserManager)
         if '@' in user_identifier:
             self._user = user_manager.get_user(user_identifier)
<span style="color: #aaaaaa">@@ -201,10 +198,7 @@ class AUser(_UserBase):
</span>             # The identifier is the string representation of a UUID, either \
an  # int in API 3.0 or a hex in API 3.1.
             try:
<span style="color: #000000;background-color: #ffdddd">-                if \
                api_version == '3.0':
-                    user_id = UUID(int=int(user_identifier))
-                else:
-                    user_id = UUID(hex=user_identifier)
</span><span style="color: #000000;background-color: #ddffdd">+                \
user_id = api.to_uuid(user_identifier) </span>             except ValueError:
                 self._user = None
             else:
<span style="color: #aaaaaa">@@ -244,7 +238,7 @@ class AUser(_UserBase):
</span>             return NotFound(), []
         child = Preferences(
             self._user.preferences,
<span style="color: #000000;background-color: #ffdddd">-            \
'users/{}'.format(self._get_uuid(self._user))) </span><span style="color: \
#000000;background-color: #ddffdd">+            \
'users/{}'.format(self.api.from_uuid(self._user.user_id))) </span>         return \
child, []  
     def on_patch(self, request, response):
<span style="color: #aaaaaa">@@ -319,14 +313,14 @@ class AddressUser(_UserBase):
</span>         if self._user:
             conflict(response)
             return
<span style="color: #000000;background-color: #ffdddd">-        api_version = \
request.context['api_version'] </span><span style="color: #000000;background-color: \
#ddffdd">+        api = request.context['api'] </span>         # When creating a \
                linked user by POSTing, the user either must already
         # exist, or it can be automatically created, if the auto_create flag
         # is given and true (if missing, it defaults to true).  However, in
         # this case we do not accept 'email' as a POST field.
         fields = CREATION_FIELDS.copy()
         del fields['email']
<span style="color: #000000;background-color: #ffdddd">-        fields['user_id'] = \
(int if api_version == '3.0' else str) </span><span style="color: \
#000000;background-color: #ddffdd">+        fields['user_id'] = api.to_uuid </span>   \
fields['auto_create'] = as_boolean  fields['_optional'] = fields['_optional'] + (
             'user_id', 'auto_create', 'is_server_owner')
<span style="color: #aaaaaa">@@ -338,16 +332,11 @@ class AddressUser(_UserBase):
</span>             return
         user_manager = getUtility(IUserManager)
         if 'user_id' in arguments:
<span style="color: #000000;background-color: #ffdddd">-            raw_uid = \
                arguments['user_id']
-            kws = {('int' if api_version == '3.0' else 'hex'): raw_uid}
-            try:
-                user_id = UUID(**kws)
-            except ValueError as error:
-                bad_request(response, str(error))
-                return
</span><span style="color: #000000;background-color: #ddffdd">+            user_id = \
arguments['user_id'] </span>             user = user_manager.get_user_by_id(user_id)
             if user is None:
<span style="color: #000000;background-color: #ffdddd">-                \
not_found(response, b'No user with ID {}'.format(raw_uid)) </span><span style="color: \
#000000;background-color: #ddffdd">+                bad_request(response, 'No user \
with ID {}'.format( +                    self.api.from_uuid(user_id)))
</span>                 return
             okay(response)
         else:
<span style="color: #aaaaaa">@@ -364,12 +353,12 @@ class AddressUser(_UserBase):
</span> 
     def on_put(self, request, response):
         """Set or replace the addresses's user."""
<span style="color: #000000;background-color: #ffdddd">-        api_version = \
request.context['api_version'] </span><span style="color: #000000;background-color: \
#ddffdd">+        api = request.context['api'] </span>         if self._user:
             self._user.unlink(self._address)
         # Process post data and check for an existing user.
         fields = CREATION_FIELDS.copy()
<span style="color: #000000;background-color: #ffdddd">-        fields['user_id'] = \
(int if api_version == '3.0' else str) </span><span style="color: \
#000000;background-color: #ddffdd">+        fields['user_id'] = api.to_uuid </span>   \
fields['_optional'] = fields['_optional'] + (  'user_id', 'email', 'is_server_owner')
         try:
<span style="color: #aaaaaa">@@ -380,16 +369,10 @@ class AddressUser(_UserBase):
</span>             return
         user_manager = getUtility(IUserManager)
         if 'user_id' in arguments:
<span style="color: #000000;background-color: #ffdddd">-            raw_uid = \
                arguments['user_id']
-            kws = {('int' if api_version == '3.0' else 'hex'): raw_uid}
-            try:
-                user_id = UUID(**kws)
-            except ValueError as error:
-                bad_request(response, str(error))
-                return
</span><span style="color: #000000;background-color: #ddffdd">+            user_id = \
arguments['user_id'] </span>             user = user_manager.get_user_by_id(user_id)
             if user is None:
<span style="color: #000000;background-color: #ffdddd">-                \
not_found(response, b'No user with ID {}'.format(raw_uid)) </span><span style="color: \
#000000;background-color: #ddffdd">+                not_found(response, b'No user \
with ID {}'.format(user_id)) </span>                 return
             okay(response)
         else:
</code></pre>

<br>
</li>
<li id='diff-18'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-18'>
 <strong>
src/mailman/rest/validator.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/validator.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/validator.py </span><span \
style="color: #aaaaaa">@@ -31,7 +31,6 @@ from mailman.core.errors import ( </span>    \
ReadOnlyPATCHRequestError, UnknownPATCHRequestError)  from mailman.interfaces.address \
import IEmailValidator  from mailman.interfaces.languages import ILanguageManager
<span style="color: #000000;background-color: #ffdddd">-from uuid import UUID
</span> from zope.component import getUtility
 
 
<span style="color: #aaaaaa">@@ -55,18 +54,11 @@ class enum_validator:
</span>             raise ValueError(exception.args[0])
 
 
<span style="color: #000000;background-color: #ffdddd">-def \
subscriber_validator(api_version): </span><span style="color: \
#000000;background-color: #ddffdd">+def subscriber_validator(api): </span>     \
"""Convert an email-or-(int|hex) to an email-or-UUID."""  def _inner(subscriber):
<span style="color: #000000;background-color: #ffdddd">-        # In API 3.0, the \
                uuid is represented by an int, so if we can int
-        # convert the value, we know it's a UUID-as-int.  In API 3.1 though,
-        # uuids are represented by the hex version, which of course cannot
-        # include an @ sign.
</span>         try:
<span style="color: #000000;background-color: #ffdddd">-            if api_version == \
                '3.0':
-                return UUID(int=int(subscriber))
-            else:
-                return UUID(hex=subscriber)
</span><span style="color: #000000;background-color: #ddffdd">+            return \
api.to_uuid(subscriber) </span>         except ValueError:
             # It must be an email address.
             if getUtility(IEmailValidator).is_valid(subscriber):
</code></pre>

<br>
</li>
<li id='diff-19'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-19'>
 <strong>
src/mailman/rest/wsgiapp.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/wsgiapp.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/wsgiapp.py </span><span \
style="color: #aaaaaa">@@ -76,7 +76,7 @@ class \
AdminWebServiceWSGIRequestHandler(WSGIRequestHandler): </span> 
 
 class SetAPIVersion:
<span style="color: #000000;background-color: #ffdddd">-    """Falcon middleware \
object that sets the api_version on resources.""" </span><span style="color: \
#000000;background-color: #ddffdd">+    """Falcon middleware object that sets the API \
on resources.""" </span> 
     def process_resource(self, request, response, resource):
         # Set this attribute on the resource right before it is dispatched
<span style="color: #aaaaaa">@@ -88,7 +88,7 @@ class SetAPIVersion:
</span>         # resource path does not exist.  This middleware method will still \
get  # called, but there's nothing to set the api_version on.
         if resource is not None:
<span style="color: #000000;background-color: #ffdddd">-            \
resource.api_version = request.context.get('api_version') </span><span style="color: \
#000000;background-color: #ddffdd">+            resource.api = \
request.context.get('api') </span> 
 
 class RootedAPI(API):
</code></pre>

<br>
</li>
<li id='diff-20'>
<a href='https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d#diff-20'>
 <strong>
tox.ini
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/tox.ini </span><span style="color: #000000;background-color: \
#ddffdd">+++ b/tox.ini </span><span style="color: #aaaaaa">@@ -30,9 +30,26 @@ \
commands = </span>     python -m coverage report -m {[coverage]rc}
 #sitepackages = True
 usedevelop = True
<span style="color: #000000;background-color: #ffdddd">-whitelist_externals = \
python-coverage </span> deps = coverage
 setenv =
     COVERAGE_PROCESS_START={[coverage]rcfile}
     COVERAGE_OPTIONS="-p"
     COVERAGE_FILE={toxinidir}/.coverage
<span style="color: #000000;background-color: #ddffdd">+
+[testenv:diffcov]
+basepython = python3.5
+commands =
+    python -m coverage run {[coverage]rc} -m nose2 -v
+    python -m coverage combine {[coverage]rc}
+    python -m coverage xml {[coverage]rc}
+    diff-cover coverage.xml --html-report diffcov.html
+    diff-cover coverage.xml
+#sitepackages = True
+usedevelop = True
+deps =
+    coverage
+    diff_cover
+setenv =
+    COVERAGE_PROCESS_START={[coverage]rcfile}
+    COVERAGE_OPTIONS="-p"
+    COVERAGE_FILE={toxinidir}/.coverage
</span></code></pre>

<br>
</li>

</div>
<div class='footer' style='margin-top: 10px;'>
<p>
&mdash;
<br>
<a href="https://gitlab.com/mailman/mailman/compare/03bb57c8c2a47a08e19b20975622ebb2ef2b81c6...98c074f19492d81ebf5b5c3f4d4f2210aa56230d">View \
it on GitLab</a>. <br>
You're receiving this email because of your account on gitlab.com.
If you'd like to receive fewer emails, you can adjust your notification settings.

</p>
</div>
</body>
</html>



_______________________________________________
Mailman-checkins mailing list
Mailman-checkins@python.org
Unsubscribe: https://mail.python.org/mailman/options/mailman-checkins/mailman-cvs%40progressive-comp.com



[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic