]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Intercept unresolveable comparator attrbute error for attr access
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 16 Jul 2019 17:02:16 +0000 (13:02 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 17 Jul 2019 14:42:05 +0000 (10:42 -0400)
Fixed bug where a synonym created against a mapped attribute that does not
exist yet, as is the case when it refers to backref before mappers are
configured, would raise recursion errors when trying to test for attributes
on it which ultimately don't exist (as occurs when the classes are run
through Sphinx autodoc), as the unconfigured state of the synonym would put
it into an attribute not found loop.

Fixes: #4767
Change-Id: I9aade8628349fbf538181a0049416cec0a17179c

doc/build/changelog/unreleased_13/4767.rst [new file with mode: 0644]
lib/sqlalchemy/orm/attributes.py
test/orm/test_mapper.py

diff --git a/doc/build/changelog/unreleased_13/4767.rst b/doc/build/changelog/unreleased_13/4767.rst
new file mode 100644 (file)
index 0000000..04608ad
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 4767
+
+    Fixed bug where a synonym created against a mapped attribute that does not
+    exist yet, as is the case when it refers to backref before mappers are
+    configured, would raise recursion errors when trying to test for attributes
+    on it which ultimately don't exist (as occurs when the classes are run
+    through Sphinx autodoc), as the unconfigured state of the synonym would put
+    it into an attribute not found loop.
+
index e277f6f5b82fe730a2e6f25c093262016835cb72..d47740e3dbac9a8e0b3585564543c2f292a2032d 100644 (file)
@@ -361,23 +361,34 @@ def create_proxied_attribute(descriptor):
         def __getattr__(self, attribute):
             """Delegate __getattr__ to the original descriptor and/or
             comparator."""
-
             try:
                 return getattr(descriptor, attribute)
             except AttributeError:
+                if attribute == "comparator":
+                    raise AttributeError("comparator")
                 try:
-                    return getattr(self.comparator, attribute)
+                    # comparator itself might be unreachable
+                    comparator = self.comparator
                 except AttributeError:
                     raise AttributeError(
-                        "Neither %r object nor %r object associated with %s "
-                        "has an attribute %r"
-                        % (
-                            type(descriptor).__name__,
-                            type(self.comparator).__name__,
-                            self,
-                            attribute,
-                        )
+                        "Neither %r object nor unconfigured comparator "
+                        "object associated with %s has an attribute %r"
+                        % (type(descriptor).__name__, self, attribute)
                     )
+                else:
+                    try:
+                        return getattr(comparator, attribute)
+                    except AttributeError:
+                        raise AttributeError(
+                            "Neither %r object nor %r object "
+                            "associated with %s has an attribute %r"
+                            % (
+                                type(descriptor).__name__,
+                                type(comparator).__name__,
+                                self,
+                                attribute,
+                            )
+                        )
 
     Proxy.__name__ = type(descriptor).__name__ + "Proxy"
 
index 15ab260fb84f92db93c8384cde37cfdde906a8ba..ceec344d9c29239451772583993ae28ca270fe27 100644 (file)
@@ -1535,6 +1535,32 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL):
         eq_(attributes.instance_state(u1).attrs.x.history, ([5], (), ()))
         eq_(attributes.instance_state(u1).attrs.y.history, ([5], (), ()))
 
+    def test_synonym_nonexistent_attr(self):
+        # test [ticket:4767].
+        # synonym points to non-existent attrbute that hasn't been mapped yet.
+        users = self.tables.users
+
+        class User(object):
+            def _x(self):
+                return self.id
+
+            x = property(_x)
+
+        m = mapper(
+            User,
+            users,
+            properties={"x": synonym("some_attr", descriptor=User.x)},
+        )
+
+        # object gracefully handles this condition
+        assert not hasattr(User.x, "__name__")
+        assert not hasattr(User.x, "comparator")
+
+        m.add_property("some_attr", column_property(users.c.name))
+
+        assert not hasattr(User.x, "__name__")
+        assert hasattr(User.x, "comparator")
+
     def test_synonym_of_non_property_raises(self):
         from sqlalchemy.ext.associationproxy import association_proxy