summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2019-07-16 13:02:16 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2019-07-17 10:42:05 -0400
commit234723fa296c9fa7ac63f1c4d877edc7ba120edd (patch)
tree103309417ca72507e5e951a81e7cc083a75395b6
parent1a9f5754e088ff521bf41bddc42b32ec989a8877 (diff)
downloadsqlalchemy-234723fa296c9fa7ac63f1c4d877edc7ba120edd.tar.gz
Intercept unresolveable comparator attrbute error for attr access
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
-rw-r--r--doc/build/changelog/unreleased_13/4767.rst11
-rw-r--r--lib/sqlalchemy/orm/attributes.py31
-rw-r--r--test/orm/test_mapper.py26
3 files changed, 58 insertions, 10 deletions
diff --git a/doc/build/changelog/unreleased_13/4767.rst b/doc/build/changelog/unreleased_13/4767.rst
new file mode 100644
index 000000000..04608ade9
--- /dev/null
+++ b/doc/build/changelog/unreleased_13/4767.rst
@@ -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.
+
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py
index e277f6f5b..d47740e3d 100644
--- a/lib/sqlalchemy/orm/attributes.py
+++ b/lib/sqlalchemy/orm/attributes.py
@@ -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"
diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py
index 15ab260fb..ceec344d9 100644
--- a/test/orm/test_mapper.py
+++ b/test/orm/test_mapper.py
@@ -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