Commits

Olemis Lang committed d23c56d

Trac #11588 : Patch for resource neighborhoods in Trac 1.1.x

Comments (0)

Files changed (2)

+t11588/t11588_r12675_resource_neighborhoods.diff
 # Placed by Bitbucket

t11588/t11588_r12675_resource_neighborhoods.diff

+# HG changeset patch
+# Parent df67d671a129206424eae17270d52ce0f618d222
+Trac #11588 : Resource neighborhoods
+
+diff --git a/trac/resource.py b/trac/resource.py
+--- a/trac/resource.py
++++ b/trac/resource.py
+@@ -82,6 +82,189 @@
+         """
+ 
+ 
++class IExternalResourceConnector(Interface):
++
++    def get_supported_neighborhoods():
++        """Return supported manager neighborhoods.
++
++        :rtype: `basestring` generator
++        """
++
++    def load_manager(neighborhood):
++        """Load the component manager identified by a given neighborhood.
++
++        :param neighborhood: manager identifier (i.e. `Neighborhood`)
++        :rtype: `trac.core.ComponentManager`
++        """
++
++    def manager_exists(neighborhood):
++        """Check whether the component manager identified by 
++        the given `neighborhood` exists physically.
++
++        :param neighborhood: manager identifier (i.e. `Neighborhood`)
++        :rtype: bool
++
++        Attempting to retrieve the manager object for a non-existing
++        neighborhood should raise a `ResourceNotFound` exception.
++        """
++
++
++class Neighborhood(object):
++    """Neighborhoods are the topmost level in the resources hierarchy. 
++    They represent resources managed by a component manager, thereby
++    identifying the later. As such, resource neighborhoods serve to
++    the purpose of specifying absolute references to resources hosted beyond
++    the boundaries of a given component manager. As a side effect they are
++    the key used to load component managers at run time.
++    """
++
++    __slots__ = ('_realm', '_id')
++
++    @property
++    def is_null(self):
++        return (self._realm, self._id) == (None, None)
++
++    def __repr__(self):
++        if self.is_null:
++            return '<Neighborhood (null)>'
++        else:
++            return '<Neighborhood %s:%s>' % (self._realm, self._id)
++
++    def __eq__(self, other):
++        return isinstance(other, Neighborhood) and \
++               self._realm == other._realm and \
++               self._id == other._id
++
++    def __hash__(self):
++        """Hash this resource descriptor, including its hierarchy."""
++        return hash((self._realm, self._id))
++
++    @property
++    def id(self):
++        return None
++
++    @id.setter
++    def id(self, value):
++        pass
++
++    realm = parent = neighborhood = version = id
++
++    # -- methods for creating other Resource identifiers
++
++    def __new__(cls, neighborhood_or_realm=None, id=False):
++        """Create a new Neighborhood object from a specification.
++
++        :param neighborhood_or_realm: this can be either:
++           - a `Neighborhood`, which is then used as a base for making a copy
++           - a `basestring`, used to specify a `realm`
++        :param id: the neighborhood identifier
++        :param version: the version or `None` for indicating the latest version
++
++        >>> main = Neighborhood('nbh', 'id')
++        >>> repr(main)
++        '<Neighborhood nbh:id>'
++
++        >>> Neighborhood(main) is main
++        True
++
++        >>> repr(Neighborhood(None))
++        '<Neighborhood (null)>'
++        """
++        realm = neighborhood_or_realm
++        if isinstance(neighborhood_or_realm, Neighborhood):
++            if id is False:
++                return neighborhood_or_realm
++            else: # copy and override
++                realm = neighborhood_or_realm._realm
++        elif id is False:
++            id = None
++        neighborhood = super(Neighborhood, cls).__new__(cls)
++        neighborhood._realm = realm
++        neighborhood._id = id
++        return neighborhood
++
++    def __call__(self, realm=False, id=False, version=False, parent=False):
++        """Create a new Resource using the current resource as a template.
++
++        Optional keyword arguments can be given to override `id` and
++        `version`.
++
++        >>> nbh = Neighborhood('nbh', 'id')
++        >>> repr(nbh)
++        '<Neighborhood nbh:id>'
++
++        >>> main = nbh('wiki', 'WikiStart')
++        >>> repr(main)
++        "<Resource u'wiki:WikiStart' in Neighborhood nbh:id>"
++
++        >>> Resource(main) is main
++        True
++
++        >>> main3 = Resource(main, version=3)
++        >>> repr(main3)
++        "<Resource u'wiki:WikiStart@3' in Neighborhood nbh:id>"
++
++        >>> main0 = main3(version=0)
++        >>> repr(main0)
++        "<Resource u'wiki:WikiStart@0' in Neighborhood nbh:id>"
++
++        In a copy, if `id` is overriden, then the original `version` value
++        will not be reused.
++
++        >>> repr(Resource(main3, id="WikiEnd"))
++        "<Resource u'wiki:WikiEnd' in Neighborhood nbh:id>"
++
++        >>> repr(nbh(None))
++        '<Neighborhood nbh:id>'
++
++        Null neighborhood will be used to put absolute resource
++        references ban into relative form (i.e. `resource.neiighborhood = None`)
++
++        >>> nullnbh = Neighborhood(None, None)
++        >>> repr(nullnbh)
++        '<Neighborhood (null)>'
++
++        >>> repr(nullnbh(main))
++        "<Resource u'wiki:WikiStart'>"
++        >>> repr(nullnbh(main3))
++        "<Resource u'wiki:WikiStart@3'>"
++        >>> repr(nullnbh(main0))
++        "<Resource u'wiki:WikiStart@0'>"
++        """
++        if (realm, id, version, parent) in ((False, False, False, False),
++                                            (None, False, False, False)):
++            return self
++        else:
++            resource = Resource(realm, id, version, parent)
++            if resource.neighborhood is not self:
++                resource = self._update_parents(resource)
++            return resource
++
++    def _update_parents(self, resource):
++        if self.is_null and resource.neighborhood is None:
++            return resource
++        newresource = Resource(resource.realm, resource.id, resource.version, self)
++        current = newresource
++        parent = resource.parent
++        while parent is not None:
++            current.parent = Resource(parent.realm, parent.id, parent.version, self)
++            current = current.parent
++            parent = parent.parent
++        return newresource
++
++    # -- methods for retrieving children Resource identifiers
++
++    def child(self, realm, id=False, version=False):
++        """Retrieve a child resource for a secondary `realm`.
++
++        Same as `__call__`, except that this one sets the parent to `self`.
++
++        >>> repr(Neighborhood('realm', 'id').child('attachment', 'file.txt'))
++        "<Resource u'attachment:file.txt' in Neighborhood realm:id>"
++        """
++        return self(realm, id, version)
++
++
+ class Resource(object):
+     """Resource identifier.
+ 
+@@ -103,7 +286,7 @@
+     the real work to the Resource's manager.
+     """
+ 
+-    __slots__ = ('realm', 'id', 'version', 'parent')
++    __slots__ = ('realm', 'id', 'version', 'parent', 'neighborhood')
+ 
+     def __repr__(self):
+         path = []
+@@ -116,13 +299,21 @@
+                 name += '@' + unicode(r.version)
+             path.append(name or '')
+             r = r.parent
+-        return '<Resource %r>' % (', '.join(reversed(path)))
++        path = reversed(path)
++        if self.neighborhood is None:
++            return '<Resource %r>' % (', '.join(path))
++        else:
++            return '<Resource %r in Neighborhood %s:%s>' % (', '.join(path), 
++                                                    self.neighborhood._realm,
++                                                    self.neighborhood._id)
+ 
+     def __eq__(self, other):
+-        return self.realm == other.realm and \
++        return isinstance(other, Resource) and \
++               self.realm == other.realm and \
+                self.id == other.id and \
+                self.version == other.version and \
+-               self.parent == other.parent
++               self.parent == other.parent and \
++               self.neighborhood == other.neighborhood
+ 
+     def __hash__(self):
+         """Hash this resource descriptor, including its hierarchy."""
+@@ -131,6 +322,10 @@
+         while current:
+             path += (self.realm, self.id, self.version)
+             current = current.parent
++        if self.neighborhood is not None:
++            path = (self.neighborhood._realm, self.neighborhood._id) + path
++        else:
++            path = (None, None) + path
+         return hash(path)
+ 
+     # -- methods for creating other Resource identifiers
+@@ -170,6 +365,11 @@
+         "<Resource ''>"
+         """
+         realm = resource_or_realm
++        if isinstance(parent, Neighborhood):
++            neighborhood = parent
++            parent = False
++        else:
++            neighborhood = None
+         if isinstance(resource_or_realm, Resource):
+             if id is False and version is False and parent is False:
+                 return resource_or_realm
+@@ -184,6 +384,7 @@
+                     version = None
+             if parent is False:
+                 parent = resource_or_realm.parent
++            neighborhood = neighborhood or resource_or_realm.neighborhood
+         else:
+             if id is False:
+                 id = None
+@@ -191,11 +392,15 @@
+                 version = None
+             if parent is False:
+                 parent = None
++            neighborhood = neighborhood or getattr(parent, 'neighborhood', None)
+         resource = super(Resource, cls).__new__(cls)
+         resource.realm = realm
+         resource.id = id
+         resource.version = version
+         resource.parent = parent
++        if neighborhood and neighborhood.is_null:
++            neighborhood = None
++        resource.neighborhood = neighborhood
+         return resource
+ 
+     def __call__(self, realm=False, id=False, version=False, parent=False):
+@@ -226,10 +431,12 @@
+     corresponding manager `Component`.
+     """
+ 
++    resource_connectors = ExtensionPoint(IExternalResourceConnector)
+     resource_managers = ExtensionPoint(IResourceManager)
+ 
+     def __init__(self):
+         self._resource_managers_map = None
++        self._resource_connector_map = None
+ 
+     # Public methods
+ 
+@@ -256,6 +463,66 @@
+                 realms.append(realm)
+         return realms
+ 
++    def get_resource_connector(self, realm):
++        """Return the component responsible for loading component managers
++         given the neighborhood `realm`
++
++        :param realm: the realm name
++        :return: a `ComponentManager` implementing `IExternalResourceConnector`
++                 or `None`
++        """
++        # build a dict of neighborhood realm keys to target implementations
++        if not self._resource_connector_map:
++            map = {}
++            for connector in self.resource_connectors:
++                for conn_realm in connector.get_supported_neighborhoods() or []:
++                    map[conn_realm] = connector
++            self._resource_connector_map = map
++        return self._resource_connector_map.get(realm)
++
++    def get_known_neighborhoods(self):
++        """Return a list of all the realm names of neighborhoods."""
++        realms = []
++        for connector in self.resource_connectors:
++            for realm in connector.get_supported_neighborhoods() or []:
++                realms.append(realm)
++        return realms
++
++    def load_component_manager(self, neighborhood, default=None):
++        """Load the component manager identified by a given instance of
++        `Neighborhood` class.
++
++        :throws ResourceNotFound: if there is no connector for neighborhood
++        """
++        if neighborhood is None or neighborhood._realm is None:
++            if default is not None:
++                return default
++            else:
++                raise ResourceNotFound('Unexpected neighborhood %s' % 
++                                       (neighborhood,))
++        c = self.get_resource_connector(neighborhood._realm)
++        if c is None:
++            raise ResourceNotFound('Missing connector for neighborhood %s' % 
++                                   (neighborhood,))
++        return c.load_manager(neighborhood)
++
++    def neighborhood_prefix(self, neighborhood):
++        return '' if neighborhood is None \
++                  else '[%s:%s] ' % (neighborhood._realm,
++                                     neighborhood._id or '') 
++
++
++def manager_for_neighborhood(compmgr, neighborhood):
++    """Instantiate a given component manager identified by
++    target neighborhood.
++    
++    :param compmgr: Source component manager.
++    :param neighborhood: Target neighborhood
++    :throws ResourceNotFound: if there is no connector for neighborhood
++    """
++    rsys = ResourceSystem(compmgr)
++    return rsys.load_component_manager(neighborhood, compmgr)
++
+ 
+ # -- Utilities for manipulating resources in a generic way
+ 
+@@ -293,9 +560,18 @@
+     '/trac.cgi/generic/Main?action=diff&version=5'
+ 
+     """
+-    manager = ResourceSystem(env).get_resource_manager(resource.realm)
+-    if manager and hasattr(manager, 'get_resource_url'):
+-        return manager.get_resource_url(resource, href, **kwargs)
++    try:
++        rsys = ResourceSystem(manager_for_neighborhood(env,
++                                                       resource.neighborhood))
++    except ResourceNotFound:
++        pass
++    else:
++        if rsys.env is not env:
++            # Use absolute href for external resources
++            href = rsys.env.abs_href
++        manager = rsys.get_resource_manager(resource.realm)
++        if manager and hasattr(manager, 'get_resource_url'):
++            return manager.get_resource_url(resource, href, **kwargs)
+     args = {'version': resource.version}
+     args.update(kwargs)
+     return href(resource.realm, resource.id, **args)
+@@ -329,10 +605,18 @@
+     u'generic:Main at version 3'
+ 
+     """
+-    manager = ResourceSystem(env).get_resource_manager(resource.realm)
+-    if manager and hasattr(manager, 'get_resource_description'):
+-        return manager.get_resource_description(resource, format, **kwargs)
+-    name = u'%s:%s' % (resource.realm, resource.id)
++    try:
++        rsys = ResourceSystem(manager_for_neighborhood(env,
++                                                       resource.neighborhood))
++    except ResourceNotFound:
++        rsys = ResourceSystem(env)
++    else:
++        manager = rsys.get_resource_manager(resource.realm)
++        if manager and hasattr(manager, 'get_resource_description'):
++            return manager.get_resource_description(resource, format, **kwargs)
++    nbhprefix = rsys.neighborhood_prefix(resource.neighborhood) 
++
++    name = u'%s%s:%s' % (nbhprefix, resource.realm, resource.id)
+     if format == 'summary':
+         name = _('%(name)s at version %(version)s',
+                  name=name, version=resource.version)
+@@ -454,7 +738,11 @@
+         >>> resource_exists(env, Resource('dummy-realm'))
+         False
+     """
+-    manager = ResourceSystem(env).get_resource_manager(resource.realm)
++    try:
++        compmgr = manager_for_neighborhood(env, resource.neighborhood)
++    except ResourceNotFound:
++        return False
++    manager = ResourceSystem(compmgr).get_resource_manager(resource.realm)
+     if manager and hasattr(manager, 'resource_exists'):
+         return manager.resource_exists(resource)
+     elif resource.id is None:
+diff --git a/trac/tests/resource.py b/trac/tests/resource.py
+--- a/trac/tests/resource.py
++++ b/trac/tests/resource.py
+@@ -112,11 +112,90 @@
+                      href='/trac.cgi/unmanaged/exists?version=1')
+         self.assertEqual(unicode(html), unicode(link))
+ 
++
++class NeighborhoodTestCase(unittest.TestCase):
++
++    def test_equals(self):
++        # Plain equalities
++        self.assertEqual(resource.Neighborhood(), resource.Neighborhood())
++        self.assertEqual(resource.Neighborhood(None), resource.Neighborhood())
++        self.assertEqual(resource.Neighborhood('realm'), 
++                         resource.Neighborhood('realm'))
++        self.assertEqual(resource.Neighborhood('realm', 'id'),
++                         resource.Neighborhood('realm', 'id'))
++        # Inequalities
++        self.assertNotEqual(resource.Neighborhood('realm', 'id'),
++                            resource.Neighborhood('realm', 'id1'))
++        self.assertNotEqual(resource.Neighborhood('realm1', 'id'),
++                            resource.Neighborhood('realm', 'id'))
++
++    def test_resources_equals(self):
++        nbh = resource.Neighborhood('realm', 'id')
++        nbh1 = resource.Neighborhood('realm', 'id1')
++        # Plain equalities
++        self.assertEqual(nbh(resource.Resource()), nbh(resource.Resource()))
++        self.assertEqual(nbh(resource.Resource(None)), nbh(resource.Resource()))
++        self.assertEqual(nbh(resource.Resource('wiki')), 
++                         nbh(resource.Resource('wiki')))
++        self.assertEqual(nbh(resource.Resource('wiki', 'WikiStart')),
++                         nbh(resource.Resource('wiki', 'WikiStart')))
++        self.assertEqual(nbh(resource.Resource('wiki', 'WikiStart', 42)),
++                         nbh(resource.Resource('wiki', 'WikiStart', 42)))
++        # Inequalities
++        self.assertNotEqual(nbh(resource.Resource('wiki', 'WikiStart', 42)),
++                            nbh(resource.Resource('wiki', 'WikiStart', 43)))
++        self.assertNotEqual(nbh(resource.Resource('wiki', 'WikiStart', 0)),
++                            nbh(resource.Resource('wiki', 'WikiStart', None)))
++        self.assertNotEqual(nbh1(resource.Resource()), 
++                            nbh(resource.Resource()))
++        self.assertNotEqual(nbh1(resource.Resource(None)), 
++                            nbh(resource.Resource()))
++        self.assertNotEqual(nbh1(resource.Resource('wiki')), 
++                            nbh(resource.Resource('wiki')))
++        self.assertNotEqual(nbh1(resource.Resource('wiki', 'WikiStart')),
++                            nbh(resource.Resource('wiki', 'WikiStart')))
++        self.assertNotEqual(nbh1(resource.Resource('wiki', 'WikiStart', 42)),
++                            nbh(resource.Resource('wiki', 'WikiStart', 42)))
++        # Resource hierarchy
++        r1 = nbh(resource.Resource('attachment', 'file.txt'))
++        r1.parent = nbh(resource.Resource('wiki', 'WikiStart'))
++        r2 = nbh(resource.Resource('attachment', 'file.txt'))
++        r2.parent = nbh(resource.Resource('wiki', 'WikiStart'))
++        self.assertEqual(r1, r2)
++        r2.parent = r2.parent(version=42)
++        self.assertNotEqual(r1, r2)
++
++    def test_hierarchy_clone(self):
++        def enum_parents(r):
++            while r is not None:
++                yield r
++                r = r.parent
++
++        nbh = resource.Neighborhood('realm', 'id')
++        nbh1 = resource.Neighborhood('realm', 'id1')
++
++        src = resource.Resource('attachment', 'file.txt')
++        src.parent = resource.Resource('wiki', 'WikiStart')
++        src.parent.parent = resource.Resource('x', 'y')
++
++        self.assertTrue(all(r.neighborhood is nbh 
++                            for r in enum_parents(nbh(src))))
++        self.assertTrue(all(r.neighborhood is None 
++                            for r in enum_parents(src)))
++
++        src = nbh1(src)
++        self.assertTrue(all(r.neighborhood is nbh 
++                            for r in enum_parents(nbh(src))))
++        self.assertTrue(all(r.neighborhood is nbh1 
++                            for r in enum_parents(src)))
++
++
+ def suite():
+     suite = unittest.TestSuite()
+     suite.addTest(doctest.DocTestSuite(resource))
+     suite.addTest(unittest.makeSuite(ResourceTestCase))
+     suite.addTest(unittest.makeSuite(RenderResourceLinkTestCase, 'test'))
++    suite.addTest(unittest.makeSuite(NeighborhoodTestCase, 'test'))
+     return suite
+ 
+ if __name__ == '__main__':