summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2015-01-05 19:02:08 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2015-01-05 19:02:08 -0500
commit1104dcaa67062f27bf7519c8589f550bd5d5b4af (patch)
tree702802bd3f0e5db235a109ba48707ea262f1782b
parent41ae0270d99793608ce563b84e7befb3aa39252e (diff)
downloadsqlalchemy-1104dcaa67062f27bf7519c8589f550bd5d5b4af.tar.gz
- add MemoizedSlots, a generalized solution to using __getattr__
for memoization on a class that uses slots. - apply many more __slots__. mem use for nova now at 46% savings
-rw-r--r--doc/build/changelog/migration_10.rst6
-rw-r--r--doc/build/core/metadata.rst1
-rw-r--r--doc/build/orm/internals.rst25
-rw-r--r--lib/sqlalchemy/event/attr.py25
-rw-r--r--lib/sqlalchemy/ext/associationproxy.py2
-rw-r--r--lib/sqlalchemy/ext/hybrid.py4
-rw-r--r--lib/sqlalchemy/orm/base.py7
-rw-r--r--lib/sqlalchemy/orm/descriptor_props.py4
-rw-r--r--lib/sqlalchemy/orm/interfaces.py51
-rw-r--r--lib/sqlalchemy/orm/mapper.py2
-rw-r--r--lib/sqlalchemy/orm/properties.py20
-rw-r--r--lib/sqlalchemy/orm/relationships.py1
-rw-r--r--lib/sqlalchemy/orm/util.py10
-rw-r--r--lib/sqlalchemy/sql/base.py21
-rw-r--r--lib/sqlalchemy/sql/elements.py18
-rw-r--r--lib/sqlalchemy/util/__init__.py2
-rw-r--r--lib/sqlalchemy/util/_collections.py17
-rw-r--r--lib/sqlalchemy/util/langhelpers.py43
18 files changed, 204 insertions, 55 deletions
diff --git a/doc/build/changelog/migration_10.rst b/doc/build/changelog/migration_10.rst
index e5382be54..8870cfd7e 100644
--- a/doc/build/changelog/migration_10.rst
+++ b/doc/build/changelog/migration_10.rst
@@ -300,7 +300,7 @@ internals, comparator objects and parts of the ORM attribute and
loader strategy system.
A bench that makes use of heapy measure the startup size of Nova
-illustrates a difference of about 2 megs of memory, a total of 27%
+illustrates a difference of about 2 megs of memory, a total of 46%
of memory taken up by SQLAlchemy's objects, associated dictionaries, as
well as weakrefs, within a basic import of "nova.db.sqlalchemy.models"::
@@ -308,13 +308,13 @@ well as weakrefs, within a basic import of "nova.db.sqlalchemy.models"::
# associated dicts + weakref-related objects with core of Nova imported:
Before: total count 26477 total bytes 7975712
- After: total count 21413 total bytes 5752976
+ After: total count 18181 total bytes 4236456
# reported for the Python module space overall with the
# core of Nova imported:
Before: Partition of a set of 355558 objects. Total size = 61661760 bytes.
- After: Partition of a set of 350281 objects. Total size = 59415104 bytes.
+ After: Partition of a set of 346034 objects. Total size = 57808016 bytes.
.. _feature_updatemany:
diff --git a/doc/build/core/metadata.rst b/doc/build/core/metadata.rst
index d6fc8c6af..e46217c17 100644
--- a/doc/build/core/metadata.rst
+++ b/doc/build/core/metadata.rst
@@ -316,6 +316,7 @@ Column, Table, MetaData API
.. autoclass:: SchemaItem
:members:
+ :undoc-members:
.. autoclass:: Table
:members:
diff --git a/doc/build/orm/internals.rst b/doc/build/orm/internals.rst
index bead784a3..debb1ab7e 100644
--- a/doc/build/orm/internals.rst
+++ b/doc/build/orm/internals.rst
@@ -38,6 +38,8 @@ sections, are listed here.
.. autoclass:: sqlalchemy.orm.base.InspectionAttr
:members:
+.. autoclass:: sqlalchemy.orm.base.InspectionAttrInfo
+ :members:
.. autoclass:: sqlalchemy.orm.state.InstanceState
:members:
@@ -54,6 +56,29 @@ sections, are listed here.
.. autoclass:: sqlalchemy.orm.interfaces.MapperProperty
:members:
+ .. py:attribute:: info
+
+ Info dictionary associated with the object, allowing user-defined
+ data to be associated with this :class:`.InspectionAttr`.
+
+ The dictionary is generated when first accessed. Alternatively,
+ it can be specified as a constructor argument to the
+ :func:`.column_property`, :func:`.relationship`, or :func:`.composite`
+ functions.
+
+ .. versionadded:: 0.8 Added support for .info to all
+ :class:`.MapperProperty` subclasses.
+
+ .. versionchanged:: 1.0.0 :attr:`.InspectionAttr.info` moved
+ from :class:`.MapperProperty` so that it can apply to a wider
+ variety of ORM and extension constructs.
+
+ .. seealso::
+
+ :attr:`.QueryableAttribute.info`
+
+ :attr:`.SchemaItem.info`
+
.. autodata:: sqlalchemy.orm.interfaces.NOT_EXTENSION
diff --git a/lib/sqlalchemy/event/attr.py b/lib/sqlalchemy/event/attr.py
index de5d34950..ed1dca644 100644
--- a/lib/sqlalchemy/event/attr.py
+++ b/lib/sqlalchemy/event/attr.py
@@ -40,15 +40,19 @@ import weakref
import collections
-class RefCollection(object):
- @util.memoized_property
- def ref(self):
+class RefCollection(util.MemoizedSlots):
+ __slots__ = 'ref',
+
+ def _memoized_attr_ref(self):
return weakref.ref(self, registry._collection_gced)
class _ClsLevelDispatch(RefCollection):
"""Class-level events on :class:`._Dispatch` classes."""
+ __slots__ = ('name', 'arg_names', 'has_kw',
+ 'legacy_signatures', '_clslevel')
+
def __init__(self, parent_dispatch_cls, fn):
self.name = fn.__name__
argspec = util.inspect_getargspec(fn)
@@ -60,8 +64,7 @@ class _ClsLevelDispatch(RefCollection):
key=lambda s: s[0]
)
))
- self.__doc__ = fn.__doc__ = legacy._augment_fn_docs(
- self, parent_dispatch_cls, fn)
+ fn.__doc__ = legacy._augment_fn_docs(self, parent_dispatch_cls, fn)
self._clslevel = weakref.WeakKeyDictionary()
@@ -158,7 +161,7 @@ class _ClsLevelDispatch(RefCollection):
return self
-class _InstanceLevelDispatch(object):
+class _InstanceLevelDispatch(RefCollection):
__slots__ = ()
def _adjust_fn_spec(self, fn, named):
@@ -229,10 +232,9 @@ class _EmptyListener(_InstanceLevelDispatch):
class _CompoundListener(_InstanceLevelDispatch):
_exec_once = False
- __slots__ = ()
+ __slots__ = '_exec_once_mutex',
- @util.memoized_property
- def _exec_once_mutex(self):
+ def _memoized_attr__exec_once_mutex(self):
return threading.Lock()
def exec_once(self, *args, **kw):
@@ -267,7 +269,7 @@ class _CompoundListener(_InstanceLevelDispatch):
__nonzero__ = __bool__
-class _ListenerCollection(RefCollection, _CompoundListener):
+class _ListenerCollection(_CompoundListener):
"""Instance-level attributes on instances of :class:`._Dispatch`.
Represents a collection of listeners.
@@ -277,8 +279,7 @@ class _ListenerCollection(RefCollection, _CompoundListener):
"""
- # RefCollection has a @memoized_property, so can't do
- # __slots__ here
+ __slots__ = 'parent_listeners', 'parent', 'name', 'listeners', 'propagate'
def __init__(self, parent, target_cls):
if target_cls not in parent._clslevel:
diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py
index 1aa68ac32..bb08ce9ba 100644
--- a/lib/sqlalchemy/ext/associationproxy.py
+++ b/lib/sqlalchemy/ext/associationproxy.py
@@ -86,7 +86,7 @@ ASSOCIATION_PROXY = util.symbol('ASSOCIATION_PROXY')
"""
-class AssociationProxy(interfaces.InspectionAttr):
+class AssociationProxy(interfaces.InspectionAttrInfo):
"""A descriptor that presents a read/write view of an object attribute."""
is_attribute = False
diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py
index d89a13fc9..f72de6099 100644
--- a/lib/sqlalchemy/ext/hybrid.py
+++ b/lib/sqlalchemy/ext/hybrid.py
@@ -660,7 +660,7 @@ HYBRID_PROPERTY = util.symbol('HYBRID_PROPERTY')
"""
-class hybrid_method(interfaces.InspectionAttr):
+class hybrid_method(interfaces.InspectionAttrInfo):
"""A decorator which allows definition of a Python object method with both
instance-level and class-level behavior.
@@ -703,7 +703,7 @@ class hybrid_method(interfaces.InspectionAttr):
return self
-class hybrid_property(interfaces.InspectionAttr):
+class hybrid_property(interfaces.InspectionAttrInfo):
"""A decorator which allows definition of a Python descriptor with both
instance-level and class-level behavior.
diff --git a/lib/sqlalchemy/orm/base.py b/lib/sqlalchemy/orm/base.py
index afeeba322..c5c8c5e2e 100644
--- a/lib/sqlalchemy/orm/base.py
+++ b/lib/sqlalchemy/orm/base.py
@@ -437,7 +437,6 @@ class InspectionAttr(object):
here intact for forwards-compatibility.
"""
-
__slots__ = ()
is_selectable = False
@@ -490,6 +489,12 @@ class InspectionAttr(object):
"""
+
+class InspectionAttrInfo(InspectionAttr):
+ """Adds the ``.info`` attribute to :class:`.Inspectionattr`.
+
+ """
+
@util.memoized_property
def info(self):
"""Info dictionary associated with the object, allowing user-defined
diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py
index 19ff71f73..e68ff1bea 100644
--- a/lib/sqlalchemy/orm/descriptor_props.py
+++ b/lib/sqlalchemy/orm/descriptor_props.py
@@ -143,6 +143,7 @@ class CompositeProperty(DescriptorProperty):
class. **Deprecated.** Please see :class:`.AttributeEvents`.
"""
+ super(CompositeProperty, self).__init__()
self.attrs = attrs
self.composite_class = class_
@@ -471,6 +472,7 @@ class ConcreteInheritedProperty(DescriptorProperty):
return comparator_callable
def __init__(self):
+ super(ConcreteInheritedProperty, self).__init__()
def warn():
raise AttributeError("Concrete %s does not implement "
"attribute %r at the instance level. Add "
@@ -555,6 +557,7 @@ class SynonymProperty(DescriptorProperty):
more complicated attribute-wrapping schemes than synonyms.
"""
+ super(SynonymProperty, self).__init__()
self.name = name
self.map_column = map_column
@@ -684,6 +687,7 @@ class ComparableProperty(DescriptorProperty):
.. versionadded:: 1.0.0
"""
+ super(ComparableProperty, self).__init__()
self.descriptor = descriptor
self.comparator_factory = comparator_factory
self.doc = doc or (descriptor and descriptor.__doc__) or None
diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py
index 68b86268c..346e2412e 100644
--- a/lib/sqlalchemy/orm/interfaces.py
+++ b/lib/sqlalchemy/orm/interfaces.py
@@ -24,7 +24,8 @@ from .. import util
from ..sql import operators
from .base import (ONETOMANY, MANYTOONE, MANYTOMANY,
EXT_CONTINUE, EXT_STOP, NOT_EXTENSION)
-from .base import InspectionAttr, _MappedAttribute
+from .base import (InspectionAttr, InspectionAttr,
+ InspectionAttrInfo, _MappedAttribute)
import collections
# imported later
@@ -48,7 +49,7 @@ __all__ = (
)
-class MapperProperty(_MappedAttribute, InspectionAttr):
+class MapperProperty(_MappedAttribute, InspectionAttr, util.MemoizedSlots):
"""Manage the relationship of a ``Mapper`` to a single class
attribute, as well as that attribute as it appears on individual
instances of the class, including attribute instrumentation,
@@ -63,6 +64,11 @@ class MapperProperty(_MappedAttribute, InspectionAttr):
"""
+ __slots__ = (
+ '_configure_started', '_configure_finished', 'parent', 'key',
+ 'info'
+ )
+
cascade = frozenset()
"""The set of 'cascade' attribute names.
@@ -78,6 +84,32 @@ class MapperProperty(_MappedAttribute, InspectionAttr):
"""
+ def _memoized_attr_info(self):
+ """Info dictionary associated with the object, allowing user-defined
+ data to be associated with this :class:`.InspectionAttr`.
+
+ The dictionary is generated when first accessed. Alternatively,
+ it can be specified as a constructor argument to the
+ :func:`.column_property`, :func:`.relationship`, or :func:`.composite`
+ functions.
+
+ .. versionadded:: 0.8 Added support for .info to all
+ :class:`.MapperProperty` subclasses.
+
+ .. versionchanged:: 1.0.0 :attr:`.InspectionAttr.info` moved
+ from :class:`.MapperProperty` so that it can apply to a wider
+ variety of ORM and extension constructs.
+
+ .. seealso::
+
+ :attr:`.QueryableAttribute.info`
+
+ :attr:`.SchemaItem.info`
+
+ """
+ return {}
+
+
def setup(self, context, entity, path, adapter, **kwargs):
"""Called by Query for the purposes of constructing a SQL statement.
@@ -139,8 +171,9 @@ class MapperProperty(_MappedAttribute, InspectionAttr):
"""
- _configure_started = False
- _configure_finished = False
+ def __init__(self):
+ self._configure_started = False
+ self._configure_finished = False
def init(self):
"""Called after all mappers are created to assemble
@@ -422,6 +455,8 @@ class StrategizedProperty(MapperProperty):
"""
+ __slots__ = '_strategies', 'strategy'
+
strategy_wildcard_key = None
def _get_context_loader(self, context, path):
@@ -485,14 +520,14 @@ class StrategizedProperty(MapperProperty):
not mapper.class_manager._attr_has_impl(self.key):
self.strategy.init_class_attribute(mapper)
- _strategies = collections.defaultdict(dict)
+ _all_strategies = collections.defaultdict(dict)
@classmethod
def strategy_for(cls, **kw):
def decorate(dec_cls):
dec_cls._strategy_keys = []
key = tuple(sorted(kw.items()))
- cls._strategies[cls][key] = dec_cls
+ cls._all_strategies[cls][key] = dec_cls
dec_cls._strategy_keys.append(key)
return dec_cls
return decorate
@@ -500,8 +535,8 @@ class StrategizedProperty(MapperProperty):
@classmethod
def _strategy_lookup(cls, *key):
for prop_cls in cls.__mro__:
- if prop_cls in cls._strategies:
- strategies = cls._strategies[prop_cls]
+ if prop_cls in cls._all_strategies:
+ strategies = cls._all_strategies[prop_cls]
try:
return strategies[key]
except KeyError:
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 9fe6b77f0..0469c2139 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -2787,6 +2787,8 @@ def _event_on_init(state, args, kwargs):
class _ColumnMapping(dict):
"""Error reporting helper for mapper._columntoproperty."""
+ __slots__ = 'mapper',
+
def __init__(self, mapper):
self.mapper = mapper
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index 291fabdd0..d51b6920d 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -34,6 +34,13 @@ class ColumnProperty(StrategizedProperty):
strategy_wildcard_key = 'column'
+ __slots__ = (
+ '_orig_columns', 'columns', 'group', 'deferred',
+ 'instrument', 'comparator_factory', 'descriptor', 'extension',
+ 'active_history', 'expire_on_flush', 'info', 'doc',
+ 'strategy_class', '_creation_order', '_is_polymorphic_discriminator',
+ '_mapped_by_synonym')
+
def __init__(self, *columns, **kwargs):
"""Provide a column-level property for use with a Mapper.
@@ -109,6 +116,7 @@ class ColumnProperty(StrategizedProperty):
**Deprecated.** Please see :class:`.AttributeEvents`.
"""
+ super(ColumnProperty, self).__init__()
self._orig_columns = [expression._labeled(c) for c in columns]
self.columns = [expression._labeled(_orm_full_deannotate(c))
for c in columns]
@@ -206,7 +214,7 @@ class ColumnProperty(StrategizedProperty):
elif dest_state.has_identity and self.key not in dest_dict:
dest_state._expire_attributes(dest_dict, [self.key])
- class Comparator(PropComparator):
+ class Comparator(util.MemoizedSlots, PropComparator):
"""Produce boolean, comparison, and other operators for
:class:`.ColumnProperty` attributes.
@@ -225,8 +233,9 @@ class ColumnProperty(StrategizedProperty):
"""
- @util.memoized_instancemethod
- def __clause_element__(self):
+ __slots__ = '__clause_element__', 'info'
+
+ def _memoized_method___clause_element__(self):
if self.adapter:
return self.adapter(self.prop.columns[0])
else:
@@ -234,15 +243,14 @@ class ColumnProperty(StrategizedProperty):
"parententity": self._parentmapper,
"parentmapper": self._parentmapper})
- @util.memoized_property
- def info(self):
+ def _memoized_attr_info(self):
ce = self.__clause_element__()
try:
return ce.info
except AttributeError:
return self.prop.info
- def __getattr__(self, key):
+ def _fallback_getattr(self, key):
"""proxy attribute access down to the mapped column.
this allows user-defined comparison methods to be accessed.
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index d3ae107b9..df2250a4c 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -775,6 +775,7 @@ class RelationshipProperty(StrategizedProperty):
"""
+ super(RelationshipProperty, self).__init__()
self.uselist = uselist
self.argument = argument
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 4be8d19ff..ee629b034 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -30,6 +30,10 @@ class CascadeOptions(frozenset):
'all', 'none', 'delete-orphan'])
_allowed_cascades = all_cascades
+ __slots__ = (
+ 'save_update', 'delete', 'refresh_expire', 'merge',
+ 'expunge', 'delete_orphan')
+
def __new__(cls, value_list):
if isinstance(value_list, str) or value_list is None:
return cls.from_string(value_list)
@@ -38,10 +42,7 @@ class CascadeOptions(frozenset):
raise sa_exc.ArgumentError(
"Invalid cascade option(s): %s" %
", ".join([repr(x) for x in
- sorted(
- values.difference(cls._allowed_cascades)
- )])
- )
+ sorted(values.difference(cls._allowed_cascades))]))
if "all" in values:
values.update(cls._add_w_all_cascades)
@@ -76,6 +77,7 @@ class CascadeOptions(frozenset):
]
return cls(values)
+
def _validator_events(
desc, key, validator, include_removes, include_backrefs):
"""Runs a validation method on an attribute value to be set or
diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py
index 2d06109b9..0f6405309 100644
--- a/lib/sqlalchemy/sql/base.py
+++ b/lib/sqlalchemy/sql/base.py
@@ -449,10 +449,12 @@ class ColumnCollection(util.OrderedProperties):
"""
+ __slots__ = '_all_col_set', '_all_columns'
+
def __init__(self, *columns):
super(ColumnCollection, self).__init__()
- self.__dict__['_all_col_set'] = util.column_set()
- self.__dict__['_all_columns'] = []
+ object.__setattr__(self, '_all_col_set', util.column_set())
+ object.__setattr__(self, '_all_columns', [])
for c in columns:
self.add(c)
@@ -576,13 +578,14 @@ class ColumnCollection(util.OrderedProperties):
return util.OrderedProperties.__contains__(self, other)
def __getstate__(self):
- return {'_data': self.__dict__['_data'],
- '_all_columns': self.__dict__['_all_columns']}
+ return {'_data': self._data,
+ '_all_columns': self._all_columns}
def __setstate__(self, state):
- self.__dict__['_data'] = state['_data']
- self.__dict__['_all_columns'] = state['_all_columns']
- self.__dict__['_all_col_set'] = util.column_set(state['_all_columns'])
+ object.__setattr__(self, '_data', state['_data'])
+ object.__setattr__(self, '_all_columns', state['_all_columns'])
+ object.__setattr__(
+ self, '_all_col_set', util.column_set(state['_all_columns']))
def contains_column(self, col):
# this has to be done via set() membership
@@ -596,8 +599,8 @@ class ColumnCollection(util.OrderedProperties):
class ImmutableColumnCollection(util.ImmutableProperties, ColumnCollection):
def __init__(self, data, colset, all_columns):
util.ImmutableProperties.__init__(self, data)
- self.__dict__['_all_col_set'] = colset
- self.__dict__['_all_columns'] = all_columns
+ object.__setattr__(self, '_all_col_set', colset)
+ object.__setattr__(self, '_all_columns', all_columns)
extend = remove = util.ImmutableProperties._immutable
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
index 445857b82..8df22b7a6 100644
--- a/lib/sqlalchemy/sql/elements.py
+++ b/lib/sqlalchemy/sql/elements.py
@@ -3387,7 +3387,7 @@ class ReleaseSavepointClause(_IdentifiedClause):
__visit_name__ = 'release_savepoint'
-class quoted_name(util.text_type):
+class quoted_name(util.MemoizedSlots, util.text_type):
"""Represent a SQL identifier combined with quoting preferences.
:class:`.quoted_name` is a Python unicode/str subclass which
@@ -3431,6 +3431,8 @@ class quoted_name(util.text_type):
"""
+ __slots__ = 'quote', 'lower', 'upper'
+
def __new__(cls, value, quote):
if value is None:
return None
@@ -3450,15 +3452,13 @@ class quoted_name(util.text_type):
def __reduce__(self):
return quoted_name, (util.text_type(self), self.quote)
- @util.memoized_instancemethod
- def lower(self):
+ def _memoized_method_lower(self):
if self.quote:
return self
else:
return util.text_type(self).lower()
- @util.memoized_instancemethod
- def upper(self):
+ def _memoized_method_upper(self):
if self.quote:
return self
else:
@@ -3475,6 +3475,8 @@ class _truncated_label(quoted_name):
"""A unicode subclass used to identify symbolic "
"names that may require truncation."""
+ __slots__ = ()
+
def __new__(cls, value, quote=None):
quote = getattr(value, "quote", quote)
# return super(_truncated_label, cls).__new__(cls, value, quote, True)
@@ -3531,6 +3533,7 @@ class conv(_truncated_label):
:ref:`constraint_naming_conventions`
"""
+ __slots__ = ()
class _defer_name(_truncated_label):
@@ -3538,6 +3541,8 @@ class _defer_name(_truncated_label):
generation.
"""
+ __slots__ = ()
+
def __new__(cls, value):
if value is None:
return _NONE_NAME
@@ -3552,6 +3557,7 @@ class _defer_name(_truncated_label):
class _defer_none_name(_defer_name):
"""indicate a 'deferred' name that was ultimately the value None."""
+ __slots__ = ()
_NONE_NAME = _defer_none_name("_unnamed_")
@@ -3566,6 +3572,8 @@ class _anonymous_label(_truncated_label):
"""A unicode subclass used to identify anonymously
generated names."""
+ __slots__ = ()
+
def __add__(self, other):
return _anonymous_label(
quoted_name(
diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py
index 7c85ef94b..c23b0196f 100644
--- a/lib/sqlalchemy/util/__init__.py
+++ b/lib/sqlalchemy/util/__init__.py
@@ -36,7 +36,7 @@ from .langhelpers import iterate_attributes, class_hierarchy, \
generic_repr, counter, PluginLoader, hybridproperty, hybridmethod, \
safe_reraise,\
get_callable_argspec, only_once, attrsetter, ellipses_string, \
- warn_limited, map_bits
+ warn_limited, map_bits, MemoizedSlots
from .deprecations import warn_deprecated, warn_pending_deprecation, \
deprecated, pending_deprecation, inject_docstring_text
diff --git a/lib/sqlalchemy/util/_collections.py b/lib/sqlalchemy/util/_collections.py
index d36852698..0f05e3427 100644
--- a/lib/sqlalchemy/util/_collections.py
+++ b/lib/sqlalchemy/util/_collections.py
@@ -179,8 +179,10 @@ class immutabledict(ImmutableContainer, dict):
class Properties(object):
"""Provide a __getattr__/__setattr__ interface over a dict."""
+ __slots__ = '_data',
+
def __init__(self, data):
- self.__dict__['_data'] = data
+ object.__setattr__(self, '_data', data)
def __len__(self):
return len(self._data)
@@ -200,8 +202,8 @@ class Properties(object):
def __delitem__(self, key):
del self._data[key]
- def __setattr__(self, key, object):
- self._data[key] = object
+ def __setattr__(self, key, obj):
+ self._data[key] = obj
def __getstate__(self):
return {'_data': self.__dict__['_data']}
@@ -252,6 +254,8 @@ class OrderedProperties(Properties):
"""Provide a __getattr__/__setattr__ interface with an OrderedDict
as backing store."""
+ __slots__ = ()
+
def __init__(self):
Properties.__init__(self, OrderedDict())
@@ -259,10 +263,17 @@ class OrderedProperties(Properties):
class ImmutableProperties(ImmutableContainer, Properties):
"""Provide immutable dict/object attribute to an underlying dictionary."""
+ __slots__ = ()
+
class OrderedDict(dict):
"""A dict that returns keys/values/items in the order they were added."""
+ __slots__ = '_list',
+
+ def __reduce__(self):
+ return OrderedDict, (self.items(),)
+
def __init__(self, ____sequence=None, **kwargs):
self._list = []
if ____sequence is None:
diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py
index b708665f9..22b6ad4ca 100644
--- a/lib/sqlalchemy/util/langhelpers.py
+++ b/lib/sqlalchemy/util/langhelpers.py
@@ -522,6 +522,15 @@ class portable_instancemethod(object):
"""
+ __slots__ = 'target', 'name', '__weakref__'
+
+ def __getstate__(self):
+ return {'target': self.target, 'name': self.name}
+
+ def __setstate__(self, state):
+ self.target = state['target']
+ self.name = state['name']
+
def __init__(self, meth):
self.target = meth.__self__
self.name = meth.__name__
@@ -800,6 +809,40 @@ class group_expirable_memoized_property(object):
return memoized_instancemethod(fn)
+class MemoizedSlots(object):
+ """Apply memoized items to an object using a __getattr__ scheme.
+
+ This allows the functionality of memoized_property and
+ memoized_instancemethod to be available to a class using __slots__.
+
+ """
+
+ def _fallback_getattr(self, key):
+ raise AttributeError(key)
+
+ def __getattr__(self, key):
+ if key.startswith('_memoized'):
+ raise AttributeError(key)
+ elif hasattr(self, '_memoized_attr_%s' % key):
+ value = getattr(self, '_memoized_attr_%s' % key)()
+ setattr(self, key, value)
+ return value
+ elif hasattr(self, '_memoized_method_%s' % key):
+ fn = getattr(self, '_memoized_method_%s' % key)
+
+ def oneshot(*args, **kw):
+ result = fn(*args, **kw)
+ memo = lambda *a, **kw: result
+ memo.__name__ = fn.__name__
+ memo.__doc__ = fn.__doc__
+ setattr(self, key, memo)
+ return result
+ oneshot.__doc__ = fn.__doc__
+ return oneshot
+ else:
+ return self._fallback_getattr(key)
+
+
def dependency_for(modulename):
def decorate(obj):
# TODO: would be nice to improve on this import silliness,