diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-04-23 18:59:17 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-04-23 18:59:17 -0400 |
| commit | 841ea194bd7cf239323ee21320210fd6dc5c551d (patch) | |
| tree | 0da6b6ae37d5def93b84e06afd451f50716c6caf /lib | |
| parent | 101da94e1282a410557784272bda58222ac048b4 (diff) | |
| download | sqlalchemy-841ea194bd7cf239323ee21320210fd6dc5c551d.tar.gz | |
- [removed] The legacy "mutable" system of the
ORM, including the MutableType class as well
as the mutable=True flag on PickleType
and postgresql.ARRAY has been removed.
In-place mutations are detected by the ORM
using the sqlalchemy.ext.mutable extension,
introduced in 0.7. The removal of MutableType
and associated constructs removes a great
deal of complexity from SQLAlchemy's internals.
The approach performed poorly as it would incur
a scan of the full contents of the Session
when in use. [ticket:2442]
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/sqlalchemy/dialects/postgresql/base.py | 37 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 59 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/identity.py | 15 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/instrumentation.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/state.py | 135 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/types.py | 183 |
7 files changed, 17 insertions, 428 deletions
diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index c31c23885..cc1aa7600 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -317,7 +317,7 @@ class UUID(sqltypes.TypeEngine): PGUuid = UUID -class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): +class ARRAY(sqltypes.Concatenable, sqltypes.TypeEngine): """Postgresql ARRAY type. Represents values as Python lists. @@ -329,7 +329,7 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): """ __visit_name__ = 'ARRAY' - def __init__(self, item_type, mutable=False, as_tuple=False): + def __init__(self, item_type, as_tuple=False): """Construct an ARRAY. E.g.:: @@ -344,25 +344,10 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): ``ARRAY(ARRAY(Integer))`` or such. The type mapping figures out on the fly - :param mutable=False: Specify whether lists passed to this - class should be considered mutable - this enables - "mutable types" mode in the ORM. Be sure to read the - notes for :class:`.MutableType` regarding ORM - performance implications (default changed from ``True`` in - 0.7.0). - - .. note:: - - This functionality is now superseded by the - ``sqlalchemy.ext.mutable`` extension described in - :ref:`mutable_toplevel`. - :param as_tuple=False: Specify whether return results should be converted to tuples from lists. DBAPIs such as psycopg2 return lists by default. When tuples are - returned, the results are hashable. This flag can only - be set to ``True`` when ``mutable`` is set to - ``False``. (new in 0.6.5) + returned, the results are hashable. """ if isinstance(item_type, ARRAY): @@ -371,27 +356,11 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): if isinstance(item_type, type): item_type = item_type() self.item_type = item_type - self.mutable = mutable - if mutable and as_tuple: - raise exc.ArgumentError( - "mutable must be set to False if as_tuple is True." - ) self.as_tuple = as_tuple - def copy_value(self, value): - if value is None: - return None - elif self.mutable: - return list(value) - else: - return value - def compare_values(self, x, y): return x == y - def is_mutable(self): - return self.mutable - def bind_processor(self, dialect): item_proc = self.item_type.dialect_impl(dialect).bind_processor(dialect) if item_proc: diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index fd51e200f..9933a2f4d 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -585,60 +585,6 @@ class ScalarAttributeImpl(AttributeImpl): self.property.columns[0].type -class MutableScalarAttributeImpl(ScalarAttributeImpl): - """represents a scalar value-holding InstrumentedAttribute, which can - detect changes within the value itself. - - """ - - uses_objects = False - supports_population = True - - def __init__(self, class_, key, callable_, dispatch, - class_manager, copy_function=None, - compare_function=None, **kwargs): - super(ScalarAttributeImpl, self).__init__( - class_, - key, - callable_, dispatch, - compare_function=compare_function, - **kwargs) - class_manager.mutable_attributes.add(key) - if copy_function is None: - raise sa_exc.ArgumentError( - "MutableScalarAttributeImpl requires a copy function") - self.copy = copy_function - - def get_history(self, state, dict_, passive=PASSIVE_OFF): - if not dict_: - v = state.committed_state.get(self.key, NO_VALUE) - else: - v = dict_.get(self.key, NO_VALUE) - - return History.from_scalar_attribute(self, state, v) - - def check_mutable_modified(self, state, dict_): - a, u, d = self.get_history(state, dict_) - return bool(a or d) - - def get(self, state, dict_, passive=PASSIVE_OFF): - if self.key not in state.mutable_dict: - ret = ScalarAttributeImpl.get(self, state, dict_, passive=passive) - if ret is not PASSIVE_NO_RESULT: - state.mutable_dict[self.key] = ret - return ret - else: - return state.mutable_dict[self.key] - - def delete(self, state, dict_): - ScalarAttributeImpl.delete(self, state, dict_) - state.mutable_dict.pop(self.key) - - def set(self, state, dict_, value, initiator, - passive=PASSIVE_OFF, check_old=None, pop=False): - ScalarAttributeImpl.set(self, state, dict_, value, - initiator, passive, check_old=check_old, pop=pop) - state.mutable_dict[self.key] = value class ScalarObjectAttributeImpl(ScalarAttributeImpl): @@ -1269,7 +1215,7 @@ def register_attribute(class_, key, **kw): def register_attribute_impl(class_, key, uselist=False, callable_=None, - useobject=False, mutable_scalars=False, + useobject=False, impl_class=None, backref=None, **kw): manager = manager_of_class(class_) @@ -1290,9 +1236,6 @@ def register_attribute_impl(class_, key, elif useobject: impl = ScalarObjectAttributeImpl(class_, key, callable_, dispatch,**kw) - elif mutable_scalars: - impl = MutableScalarAttributeImpl(class_, key, callable_, dispatch, - class_manager=manager, **kw) else: impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw) diff --git a/lib/sqlalchemy/orm/identity.py b/lib/sqlalchemy/orm/identity.py index 59d121de9..bb5fbb6e8 100644 --- a/lib/sqlalchemy/orm/identity.py +++ b/lib/sqlalchemy/orm/identity.py @@ -10,7 +10,6 @@ from sqlalchemy.orm import attributes class IdentityMap(dict): def __init__(self): - self._mutable_attrs = set() self._modified = set() self._wr = weakref.ref(self) @@ -31,28 +30,18 @@ class IdentityMap(dict): if state.modified: self._modified.add(state) - if state.manager.mutable_attributes: - self._mutable_attrs.add(state) def _manage_removed_state(self, state): del state._instance_dict - self._mutable_attrs.discard(state) self._modified.discard(state) def _dirty_states(self): - return self._modified.union(s for s in self._mutable_attrs.copy() - if s.modified) + return self._modified def check_modified(self): """return True if any InstanceStates present have been marked as 'modified'.""" - if self._modified: - return True - else: - for state in self._mutable_attrs.copy(): - if state.modified: - return True - return False + return bool(self._modified) def has_key(self, key): return key in self diff --git a/lib/sqlalchemy/orm/instrumentation.py b/lib/sqlalchemy/orm/instrumentation.py index 1012af67a..e9d1ca36a 100644 --- a/lib/sqlalchemy/orm/instrumentation.py +++ b/lib/sqlalchemy/orm/instrumentation.py @@ -86,7 +86,6 @@ class ClassManager(dict): self.factory = None # where we came from, for inheritance bookkeeping self.info = {} self.new_init = None - self.mutable_attributes = set() self.local_attrs = {} self.originals = {} @@ -155,10 +154,7 @@ class ClassManager(dict): @util.memoized_property def _state_constructor(self): self.dispatch.first_init(self, self.class_) - if self.mutable_attributes: - return state.MutableAttrInstanceState - else: - return state.InstanceState + return state.InstanceState def manage(self): """Mark this instance as the manager for its class.""" @@ -209,8 +205,6 @@ class ClassManager(dict): del self.local_attrs[key] self.uninstall_descriptor(key) del self[key] - if key in self.mutable_attributes: - self.mutable_attributes.remove(key) for cls in self.class_.__subclasses__(): manager = manager_of_class(cls) if manager: diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 64fca8715..bb6104762 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -33,7 +33,6 @@ class InstanceState(object): load_options = EMPTY_SET load_path = () insert_order = None - mutable_dict = None _strong_obj = None modified = False expired = False @@ -150,9 +149,6 @@ class InstanceState(object): manager.dispatch.init(self, args, kwargs) - #if manager.mutable_attributes: - # assert self.__class__ is MutableAttrInstanceState - try: return manager.original_init(*mixed[1:], **kwargs) except: @@ -175,7 +171,7 @@ class InstanceState(object): d.update( (k, self.__dict__[k]) for k in ( 'committed_state', '_pending_mutations', 'modified', 'expired', - 'callables', 'key', 'parents', 'load_options', 'mutable_dict', + 'callables', 'key', 'parents', 'load_options', 'class_', ) if k in self.__dict__ ) @@ -220,7 +216,7 @@ class InstanceState(object): self.__dict__.update([ (k, state[k]) for k in ( - 'key', 'load_options', 'mutable_dict' + 'key', 'load_options', ) if k in state ]) @@ -275,7 +271,6 @@ class InstanceState(object): self.committed_state.clear() self.__dict__.pop('_pending_mutations', None) - self.__dict__.pop('mutable_dict', None) # clear out 'parents' collection. not # entirely clear how we can best determine @@ -293,7 +288,6 @@ class InstanceState(object): def expire_attributes(self, dict_, attribute_names): pending = self.__dict__.get('_pending_mutations', None) - mutable_dict = self.mutable_dict for key in attribute_names: impl = self.manager[key].impl @@ -302,8 +296,6 @@ class InstanceState(object): dict_.pop(key, None) self.committed_state.pop(key, None) - if mutable_dict: - mutable_dict.pop(key, None) if pending: pending.pop(key, None) @@ -421,15 +413,8 @@ class InstanceState(object): """ class_manager = self.manager - if class_manager.mutable_attributes: - for key in keys: - if key in dict_ and key in class_manager.mutable_attributes: - self.committed_state[key] = self.manager[key].impl.copy(dict_[key]) - else: - self.committed_state.pop(key, None) - else: - for key in keys: - self.committed_state.pop(key, None) + for key in keys: + self.committed_state.pop(key, None) self.expired = False @@ -462,10 +447,6 @@ class InstanceState(object): if key in dict_ and callables[key] is self: del callables[key] - for key in self.manager.mutable_attributes: - if key in dict_: - self.committed_state[key] = self.manager[key].impl.copy(dict_[key]) - if instance_dict and self.modified: instance_dict._modified.discard(self) @@ -493,114 +474,6 @@ class InspectAttr(object): return self.state.get_history(self.key, PASSIVE_NO_INITIALIZE) -class MutableAttrInstanceState(InstanceState): - """InstanceState implementation for objects that reference 'mutable' - attributes. - - Has a more involved "cleanup" handler that checks mutable attributes - for changes upon dereference, resurrecting if needed. - - """ - - @util.memoized_property - def mutable_dict(self): - return {} - - def _get_modified(self, dict_=None): - if self.__dict__.get('modified', False): - return True - else: - if dict_ is None: - dict_ = self.dict - for key in self.manager.mutable_attributes: - if self.manager[key].impl.check_mutable_modified(self, dict_): - return True - else: - return False - - def _set_modified(self, value): - self.__dict__['modified'] = value - - modified = property(_get_modified, _set_modified) - - @property - def unmodified(self): - """a set of keys which have no uncommitted changes""" - - dict_ = self.dict - - return set([ - key for key in self.manager - if (key not in self.committed_state or - (key in self.manager.mutable_attributes and - not self.manager[key].impl.check_mutable_modified(self, dict_)))]) - - def unmodified_intersection(self, keys): - """Return self.unmodified.intersection(keys).""" - - dict_ = self.dict - - return set([ - key for key in keys - if (key not in self.committed_state or - (key in self.manager.mutable_attributes and - not self.manager[key].impl.check_mutable_modified(self, dict_)))]) - - - def _is_really_none(self): - """do a check modified/resurrect. - - This would be called in the extremely rare - race condition that the weakref returned None but - the cleanup handler had not yet established the - __resurrect callable as its replacement. - - """ - if self.modified: - self.obj = self.__resurrect - return self.obj() - else: - return None - - def reset(self, dict_, key): - self.mutable_dict.pop(key, None) - InstanceState.reset(self, dict_, key) - - def _cleanup(self, ref): - """weakref callback. - - This method may be called by an asynchronous - gc. - - If the state shows pending changes, the weakref - is replaced by the __resurrect callable which will - re-establish an object reference on next access, - else removes this InstanceState from the owning - identity map, if any. - - """ - if self._get_modified(self.mutable_dict): - self.obj = self.__resurrect - else: - instance_dict = self._instance_dict() - if instance_dict: - instance_dict.discard(self) - self._dispose() - - def __resurrect(self): - """A substitute for the obj() weakref function which resurrects.""" - - # store strong ref'ed version of the object; will revert - # to weakref when changes are persisted - obj = self.manager.new_instance(state=self) - self.obj = weakref.ref(obj, self._cleanup) - self._strong_obj = obj - obj.__dict__.update(self.mutable_dict) - - # re-establishes identity attributes from the key - self.manager.dispatch.resurrect(self) - - return obj class PendingCollection(object): """A writable placeholder for an unloaded collection. diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 06b630076..4cf32335f 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -25,8 +25,6 @@ import itertools def _register_attribute(strategy, mapper, useobject, compare_function=None, typecallable=None, - copy_function=None, - mutable_scalars=False, uselist=False, callable_=None, proxy_property=None, @@ -71,9 +69,7 @@ def _register_attribute(strategy, mapper, useobject, m.class_, prop.key, parent_token=prop, - mutable_scalars=mutable_scalars, uselist=uselist, - copy_function=copy_function, compare_function=compare_function, useobject=useobject, extension=attribute_ext, @@ -132,8 +128,6 @@ class ColumnLoader(LoaderStrategy): _register_attribute(self, mapper, useobject=False, compare_function=coltype.compare_values, - copy_function=coltype.copy_value, - mutable_scalars=self.columns[0].type.is_mutable(), active_history = active_history ) @@ -193,8 +187,6 @@ class DeferredColumnLoader(LoaderStrategy): _register_attribute(self, mapper, useobject=False, compare_function=self.columns[0].type.compare_values, - copy_function=self.columns[0].type.copy_value, - mutable_scalars=self.columns[0].type.is_mutable(), callable_=self._load_for_state, expire_missing=False ) diff --git a/lib/sqlalchemy/types.py b/lib/sqlalchemy/types.py index 512ac6268..c6e3ee3d6 100644 --- a/lib/sqlalchemy/types.py +++ b/lib/sqlalchemy/types.py @@ -17,7 +17,7 @@ __all__ = [ 'TypeEngine', 'TypeDecorator', 'AbstractType', 'UserDefinedType', 'CLOB', 'BLOB', 'BOOLEAN', 'SMALLINT', 'INTEGER', 'DATE', 'TIME', 'String', 'Integer', 'SmallInteger', 'BigInteger', 'Numeric', 'Float', 'DateTime', 'Date', 'Time', 'LargeBinary', 'Binary', - 'Boolean', 'Unicode', 'MutableType', 'Concatenable', + 'Boolean', 'Unicode', 'Concatenable', 'UnicodeText','PickleType', 'Interval', 'Enum' ] import inspect @@ -83,28 +83,6 @@ class TypeEngine(AbstractType): return x == y - def is_mutable(self): - """Return True if the target Python type is 'mutable'. - - This allows systems like the ORM to know if a column value can - be considered 'not changed' by comparing the identity of - objects alone. Values such as dicts, lists which - are serialized into strings are examples of "mutable" - column structures. - - .. note:: - - This functionality is now superseded by the - ``sqlalchemy.ext.mutable`` extension described in - :ref:`mutable_toplevel`. - - When this method is overridden, :meth:`copy_value` should - also be supplied. The :class:`.MutableType` mixin - is recommended as a helper. - - """ - return False - def get_dbapi_type(self, dbapi): """Return the corresponding type object from the underlying DB-API, if any. @@ -710,30 +688,6 @@ class TypeDecorator(TypeEngine): """ return self.impl.get_dbapi_type(dbapi) - def copy_value(self, value): - """Given a value, produce a copy of it. - - By default this calls upon :meth:`.TypeEngine.copy_value` - of the underlying "impl". - - :meth:`.copy_value` will return the object - itself, assuming "mutability" is not enabled. - Only the :class:`.MutableType` mixin provides a copy - function that actually produces a new object. - The copying function is used by the ORM when - "mutable" types are used, to memoize the original - version of an object as loaded from the database, - which is then compared to the possibly mutated - version to check for changes. - - Modern implementations should use the - ``sqlalchemy.ext.mutable`` extension described in - :ref:`mutable_toplevel` for intercepting in-place - changes to values. - - """ - return self.impl.copy_value(value) - def compare_values(self, x, y): """Given two values, compare them for equality. @@ -749,24 +703,6 @@ class TypeDecorator(TypeEngine): """ return self.impl.compare_values(x, y) - def is_mutable(self): - """Return True if the target Python type is 'mutable'. - - This allows systems like the ORM to know if a column value can - be considered 'not changed' by comparing the identity of - objects alone. Values such as dicts, lists which - are serialized into strings are examples of "mutable" - column structures. - - .. note:: - - This functionality is now superseded by the - ``sqlalchemy.ext.mutable`` extension described in - :ref:`mutable_toplevel`. - - """ - return self.impl.is_mutable() - def _adapt_expression(self, op, othertype): """ #todo @@ -828,82 +764,6 @@ class Variant(TypeDecorator): mapping[dialect_name] = type_ return Variant(self.impl, mapping) -class MutableType(object): - """A mixin that marks a :class:`.TypeEngine` as representing - a mutable Python object type. This functionality is used - only by the ORM. - - .. note:: - - :class:`.MutableType` is superseded as of SQLAlchemy 0.7 - by the ``sqlalchemy.ext.mutable`` extension described in - :ref:`mutable_toplevel`. This extension provides an event - driven approach to in-place mutation detection that does not - incur the severe performance penalty of the :class:`.MutableType` - approach. - - "mutable" means that changes can occur in place to a value - of this type. Examples includes Python lists, dictionaries, - and sets, as well as user-defined objects. The primary - need for identification of "mutable" types is by the ORM, - which applies special rules to such values in order to guarantee - that changes are detected. These rules may have a significant - performance impact, described below. - - A :class:`.MutableType` usually allows a flag called - ``mutable=False`` to enable/disable the "mutability" flag, - represented on this class by :meth:`is_mutable`. Examples - include :class:`.PickleType` and - :class:`~sqlalchemy.dialects.postgresql.base.ARRAY`. Setting - this flag to ``True`` enables mutability-specific behavior - by the ORM. - - The :meth:`copy_value` and :meth:`compare_values` functions - represent a copy and compare function for values of this - type - implementing subclasses should override these - appropriately. - - .. warning:: - - The usage of mutable types has significant performance - implications when using the ORM. In order to detect changes, the - ORM must create a copy of the value when it is first - accessed, so that changes to the current value can be compared - against the "clean" database-loaded value. Additionally, when the - ORM checks to see if any data requires flushing, it must scan - through all instances in the session which are known to have - "mutable" attributes and compare the current value of each - one to its "clean" - value. So for example, if the Session contains 6000 objects (a - fairly large amount) and autoflush is enabled, every individual - execution of :class:`.Query` will require a full scan of that subset of - the 6000 objects that have mutable attributes, possibly resulting - in tens of thousands of additional method calls for every query. - - As of SQLAlchemy 0.7, the ``sqlalchemy.ext.mutable`` is provided which - allows an event driven approach to in-place mutation detection. This - approach should now be favored over the usage of :class:`.MutableType` - with ``mutable=True``. ``sqlalchemy.ext.mutable`` is described in - :ref:`mutable_toplevel`. - - """ - - def is_mutable(self): - """Return True if the target Python type is 'mutable'. - - For :class:`.MutableType`, this method is set to - return ``True``. - - """ - return True - - def copy_value(self, value): - """Unimplemented.""" - raise NotImplementedError() - - def compare_values(self, x, y): - """Compare *x* == *y*.""" - return x == y def to_instance(typeobj, *arg, **kw): if typeobj is None: @@ -1961,7 +1821,7 @@ class Enum(String, SchemaType): else: return super(Enum, self).adapt(impltype, **kw) -class PickleType(MutableType, TypeDecorator): +class PickleType(TypeDecorator): """Holds Python objects, which are serialized using pickle. PickleType builds upon the Binary type to apply Python's @@ -1969,12 +1829,15 @@ class PickleType(MutableType, TypeDecorator): the way out, allowing any pickleable Python object to be stored as a serialized binary field. + To allow ORM change events to propagate for elements associated + with :class:`.PickleType`, see :ref:`mutable_toplevel`. + """ impl = LargeBinary def __init__(self, protocol=pickle.HIGHEST_PROTOCOL, - pickler=None, mutable=False, comparator=None): + pickler=None, comparator=None): """ Construct a PickleType. @@ -1984,21 +1847,6 @@ class PickleType(MutableType, TypeDecorator): cPickle is not available. May be any object with pickle-compatible ``dumps` and ``loads`` methods. - :param mutable: defaults to False; implements - :meth:`AbstractType.is_mutable`. When ``True``, incoming - objects will be compared against copies of themselves - using the Python "equals" operator, unless the - ``comparator`` argument is present. See - :class:`.MutableType` for details on "mutable" type - behavior. (default changed from ``True`` in - 0.7.0). - - .. note:: - - This functionality is now superseded by the - ``sqlalchemy.ext.mutable`` extension described in - :ref:`mutable_toplevel`. - :param comparator: a 2-arg callable predicate used to compare values of this type. If left as ``None``, the Python "equals" operator is used to compare values. @@ -2006,14 +1854,12 @@ class PickleType(MutableType, TypeDecorator): """ self.protocol = protocol self.pickler = pickler or pickle - self.mutable = mutable self.comparator = comparator super(PickleType, self).__init__() def __reduce__(self): return PickleType, (self.protocol, None, - self.mutable, self.comparator) def bind_processor(self, dialect): @@ -2048,29 +1894,12 @@ class PickleType(MutableType, TypeDecorator): return loads(value) return process - def copy_value(self, value): - if self.mutable: - return self.pickler.loads( - self.pickler.dumps(value, self.protocol)) - else: - return value - def compare_values(self, x, y): if self.comparator: return self.comparator(x, y) else: return x == y - def is_mutable(self): - """Return True if the target Python type is 'mutable'. - - When this method is overridden, :meth:`copy_value` should - also be supplied. The :class:`.MutableType` mixin - is recommended as a helper. - - """ - return self.mutable - class Boolean(TypeEngine, SchemaType): """A bool datatype. |
