summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm/attributes.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2019-05-25 18:04:58 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2019-05-27 16:45:59 -0400
commite573752a986dec84216d948a1497b7d789d039ea (patch)
tree5c887df82321af54d1ccd445a16660fc0aa81707 /lib/sqlalchemy/orm/attributes.py
parent402da5f3f0bade44c2941ab8446f69cf33f5dd67 (diff)
downloadsqlalchemy-e573752a986dec84216d948a1497b7d789d039ea.tar.gz
Hold implicitly created collections in a pending area
Accessing a collection-oriented attribute on a newly created object no longer mutates ``__dict__``, but still returns an empty collection as has always been the case. This allows collection-oriented attributes to work consistently in comparison to scalar attributes which return ``None``, but also don't mutate ``__dict__``. In order to accommodate for the collection being mutated, the same empty collection is returned each time once initially created, and when it is mutated (e.g. an item appended, added, etc.) it is then moved into ``__dict__``. This removes the last of mutating side-effects on read-only attribute access within the ORM. Fixes: #4519 Change-Id: I06a058d24e6eb24b5c6b6092d3f8b31cf9c244ae
Diffstat (limited to 'lib/sqlalchemy/orm/attributes.py')
-rw-r--r--lib/sqlalchemy/orm/attributes.py27
1 files changed, 15 insertions, 12 deletions
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py
index 321ab7d6f..31c351bb0 100644
--- a/lib/sqlalchemy/orm/attributes.py
+++ b/lib/sqlalchemy/orm/attributes.py
@@ -653,8 +653,8 @@ class AttributeImpl(object):
"""
raise NotImplementedError()
- def initialize(self, state, dict_):
- """Initialize the given state's attribute with an empty value."""
+ def _default_value(self, state, dict_):
+ """Produce an empty value for an uninitialized scalar attribute."""
value = None
for fn in self.dispatch.init_scalar:
@@ -710,8 +710,7 @@ class AttributeImpl(object):
if not passive & INIT_OK:
return NO_VALUE
else:
- # Return a new, empty value
- return self.initialize(state, dict_)
+ return self._default_value(state, dict_)
def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
self.set(state, dict_, value, initiator, passive=passive)
@@ -1184,11 +1183,14 @@ class CollectionAttributeImpl(AttributeImpl):
# del is a no-op if collection not present.
del dict_[self.key]
- def initialize(self, state, dict_):
- """Initialize this attribute with an empty collection."""
+ def _default_value(self, state, dict_):
+ """Produce an empty collection for an un-initialized attribute"""
- _, user_data = self._initialize_collection(state)
- dict_[self.key] = user_data
+ if self.key in state._empty_collections:
+ return state._empty_collections[self.key]
+
+ adapter, user_data = self._initialize_collection(state)
+ adapter._set_empty(user_data)
return user_data
def _initialize_collection(self, state):
@@ -1287,7 +1289,7 @@ class CollectionAttributeImpl(AttributeImpl):
old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT)
if old is PASSIVE_NO_RESULT:
- old = self.initialize(state, dict_)
+ old = self._default_value(state, dict_)
elif old is orig_iterable:
# ignore re-assignment of the current collection, as happens
# implicitly with in-place operators (foo.collection |= other)
@@ -1699,7 +1701,6 @@ class History(History):
@classmethod
def from_collection(cls, attribute, state, current):
original = state.committed_state.get(attribute.key, _NO_HISTORY)
-
if current is NO_VALUE:
return cls((), (), ())
@@ -1892,8 +1893,10 @@ def init_state_collection(state, dict_, key):
"""Initialize a collection attribute and return the collection adapter."""
attr = state.manager[key].impl
- user_data = attr.initialize(state, dict_)
- return attr.get_collection(state, dict_, user_data)
+ user_data = attr._default_value(state, dict_)
+ adapter = attr.get_collection(state, dict_, user_data)
+ adapter._reset_empty()
+ return adapter
def set_committed_value(instance, key, value):