Commits

Mike Bayer committed 88b9e45

- store only MultipleClassMarkers inside of ModuleMarker, then
store ModuleMarkers for multiple roots, one for each token in a
module path. this allows partial path resolution.
- docs to this effect

Comments (0)

Files changed (3)

lib/sqlalchemy/ext/declarative/__init__.py

                              primaryjoin="Address.user_id==User.id")
 
 For the case where more than one module contains a class of the same name,
-string class names can also be specified as fully module-qualified paths
+string class names can also be specified as module-qualified paths
 within any of these string expressions::
 
     class User(Base):
                              primaryjoin="myapp.model.address.Address.user_id=="
                                             "myapp.model.user.User.id")
 
+The qualified path can be any partial path that removes ambiguity between
+the names.  For example, to disambiguate between
+``myapp.model.address.Address`` and ``myapp.model.lookup.Address``,
+we can specify ``address.Address`` or ``lookup.Address``::
+
+    class User(Base):
+        # ....
+        addresses = relationship("address.Address",
+                             order_by="desc(address.Address.email)",
+                             primaryjoin="address.Address.user_id=="
+                                            "User.id")
+
 .. versionadded:: 0.8
-   Fully module-qualified paths can be used when specifying string arguments
-   with Declarative.
+   module-qualified paths can be used when specifying string arguments
+   with Declarative, in order to specify specific modules.
 
 Two alternatives also exist to using string-based attributes.  A lambda
 can also be used, which will be evaluated after all mappers have been

lib/sqlalchemy/ext/declarative/clsregistry.py

         cls._decl_class_registry[classname] = cls
 
     try:
-        module = cls._decl_class_registry['_sa_module_registry']
+        root_module = cls._decl_class_registry['_sa_module_registry']
     except KeyError:
         cls._decl_class_registry['_sa_module_registry'] = \
-            module = _ModuleMarker('_sa_module_registry', None)
-    for token in cls.__module__.split("."):
-        module = module.get_module(token)
+            root_module = _ModuleMarker('_sa_module_registry', None)
 
-    module.add_class(classname, cls)
+    tokens = cls.__module__.split(".")
+
+    # build up a tree like this:
+    # modulename:  myapp.snacks.nuts
+    #
+    # myapp->snack->nuts->(classes)
+    # snack->nuts->(classes)
+    # nuts->(classes)
+    #
+    # this allows partial token paths to be used.
+    while tokens:
+        token = tokens.pop(0)
+        module = root_module.get_module(token)
+        for token in tokens:
+            module = module.get_module(token)
+        module.add_class(classname, cls)
+
 
 class _MultipleClassMarker(object):
     """refers to multiple classes of the same name
 
     """
 
-    def __init__(self, classes):
+    def __init__(self, classes, on_remove=None):
+        self.on_remove = on_remove
         self.contents = set([
                 weakref.ref(item, self._remove_item) for item in classes])
         _registries.add(self)
     def __iter__(self):
         return (ref() for ref in self.contents)
 
-    def attempt_get(self, key):
+    def attempt_get(self, path, key):
         if len(self.contents) > 1:
             raise exc.InvalidRequestError(
-                "Multiple classes with the classname "
-                "%r are in the registry of this declarative "
-                "base. Please use a fully module-qualified path." % key)
+                "Multiple classes found for path \"%s\" "
+                "in the registry of this declarative "
+                "base. Please use a fully module-qualified path." %
+                (".".join(path + [key]))
+            )
         else:
             ref = list(self.contents)[0]
             cls = ref()
         self.contents.remove(ref)
         if not self.contents:
             _registries.discard(self)
+            if self.on_remove:
+                self.on_remove()
 
-    def add_item(self, item, base):
+    def add_item(self, item):
+        modules = set([cls().__module__ for cls in self.contents])
+        if item.__module__ in modules:
+            util.warn(
+                "This declarative base already contains a class with the "
+                "same class name and module name as %s.%s, and will "
+                "be replaced in the string-lookup table." % (
+                    item.__module__,
+                    item.__name__
+                )
+            )
         self.contents.add(weakref.ref(item, self._remove_item))
 
 class _ModuleMarker(object):
         self.name = name
         self.contents = {}
         self.mod_ns = _ModNS(self)
+        if self.parent:
+            self.path = self.parent.path + [self.name]
+        else:
+            self.path = []
         _registries.add(self)
 
     def __contains__(self, name):
         return name in self.contents
 
     def __getitem__(self, name):
-        return self.contents[name]()
+        return self.contents[name]
 
     def _remove_item(self, name):
         self.contents.pop(name, None)
     def get_module(self, name):
         if name not in self.contents:
             marker = _ModuleMarker(name, self)
-            self.contents[name] = lambda: marker
+            self.contents[name] = marker
         else:
-            marker = self.contents[name]()
+            marker = self.contents[name]
         return marker
 
     def add_class(self, name, cls):
         if name in self.contents:
-            util.warn(
-                "This declarative base already contains a class with the "
-                "same class name and module name as %r, and will be replaced "
-                "in the string-lookup table." % cls)
+            existing = self.contents[name]
+            existing.add_item(cls)
+        else:
+            existing = self.contents[name] = \
+                    _MultipleClassMarker([cls],
+                        on_remove=lambda: self._remove_item(name))
 
-        self.contents[name] = weakref.ref(cls,
-                            lambda ref: self._remove_item(name))
 
 
 class _ModNS(object):
         except KeyError:
             pass
         else:
-            value = value()
             if value is not None:
                 if isinstance(value, _ModuleMarker):
                     return value.mod_ns
                 else:
-                    return value
+                    assert isinstance(value, _MultipleClassMarker)
+                    return value.attempt_get(self.__parent.path, key)
         raise AttributeError("Module %r has no mapped classes "
                     "registered under the name %r" % (self.__parent.name, key))
 
 
 def _determine_container(key, value):
     if isinstance(value, _MultipleClassMarker):
-        value = value.attempt_get(key)
+        value = value.attempt_get([], key)
     return _GetColumns(value)
 
 def _resolver(cls, prop):

test/ext/declarative/test_clsregistry.py

         self._decl_class_registry = base
         tokens = name.split(".")
         self.__module__ = ".".join(tokens[0:-1])
-        self.name = tokens[-1]
+        self.name = self.__name__ = tokens[-1]
         self.metadata = MetaData()
 
 
 
         assert_raises_message(
             exc.SAWarning,
-            "This declarative base already contains a class ",
+            "This declarative base already contains a class with the "
+            "same class name and module name as foo.bar.Foo, and "
+            "will be replaced in the string-lookup table.",
             clsregistry.add_class, "Foo", f2
         )
 
         is_(resolver("foo.bar.Foo")(), f1)
         is_(resolver("foo.alt.Foo")(), f2)
 
+    def test_fragment_resolve(self):
+        base = weakref.WeakValueDictionary()
+        f1 = MockClass(base, "foo.bar.Foo")
+        f2 = MockClass(base, "foo.alt.Foo")
+        f3 = MockClass(base, "bat.alt.Hoho")
+        clsregistry.add_class("Foo", f1)
+        clsregistry.add_class("Foo", f2)
+        clsregistry.add_class("HoHo", f3)
+        resolver = clsregistry._resolver(f1, MockProp())
+
+        gc_collect()
+
+        is_(resolver("bar.Foo")(), f1)
+        is_(resolver("alt.Foo")(), f2)
+
+    def test_fragment_ambiguous(self):
+        base = weakref.WeakValueDictionary()
+        f1 = MockClass(base, "foo.bar.Foo")
+        f2 = MockClass(base, "foo.alt.Foo")
+        f3 = MockClass(base, "bat.alt.Foo")
+        clsregistry.add_class("Foo", f1)
+        clsregistry.add_class("Foo", f2)
+        clsregistry.add_class("Foo", f3)
+        resolver = clsregistry._resolver(f1, MockProp())
+
+        gc_collect()
+
+        assert_raises_message(
+            exc.InvalidRequestError,
+            'Multiple classes found for path "alt.Foo" in the registry '
+            'of this declarative base. Please use a fully '
+            'module-qualified path.',
+            resolver("alt.Foo")
+        )
+
     def test_resolve_dupe_by_name(self):
         base = weakref.WeakValueDictionary()
         f1 = MockClass(base, "foo.bar.Foo")
         resolver = resolver("Foo")
         assert_raises_message(
             exc.InvalidRequestError,
-            "Multiple classes with the classname 'Foo' "
-            "are in the registry of this declarative "
-            "base. Please use a fully module-qualified path.",
+            'Multiple classes found for path "Foo" in the '
+            'registry of this declarative base. Please use a '
+            'fully module-qualified path.',
             resolver
         )
 
             clsregistry.add_class("Foo", f1)
             clsregistry.add_class("Foo", f2)
 
-            eq_(len(clsregistry._registries), 5)
+            eq_(len(clsregistry._registries), 11)
 
             del f1
             del f2
         mod_entry = reg['foo']['bar']
         resolver = clsregistry._resolver(f1, MockProp())
         resolver = resolver("foo")
-        mod_entry.contents.update({"Foo": lambda: None})
+        del mod_entry.contents["Foo"]
         assert_raises_message(
             AttributeError,
             "Module 'bar' has no mapped classes registered "
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.