diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-09-24 20:53:29 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-09-24 20:53:29 -0400 |
commit | 3b79eafdbf3b8be84c6586fa699daceea7fa5fd7 (patch) | |
tree | 210b535d794ab1cf3bf1e483b4d32bfa466cfb97 | |
parent | 4cfed27b451188240e2fb3379aff5be5bd2e33d8 (diff) | |
download | sqlalchemy-3b79eafdbf3b8be84c6586fa699daceea7fa5fd7.tar.gz |
- simplify. make the base declared_attr memoized.
-rw-r--r-- | lib/sqlalchemy/ext/declarative/__init__.py | 8 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/declarative/api.py | 69 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/declarative/base.py | 22 | ||||
-rw-r--r-- | lib/sqlalchemy/util/__init__.py | 3 | ||||
-rw-r--r-- | lib/sqlalchemy/util/langhelpers.py | 13 | ||||
-rw-r--r-- | test/ext/declarative/test_inheritance.py | 2 | ||||
-rw-r--r-- | test/ext/declarative/test_mixin.py | 49 |
7 files changed, 68 insertions, 98 deletions
diff --git a/lib/sqlalchemy/ext/declarative/__init__.py b/lib/sqlalchemy/ext/declarative/__init__.py index dc9c55c09..9f3f36f0f 100644 --- a/lib/sqlalchemy/ext/declarative/__init__.py +++ b/lib/sqlalchemy/ext/declarative/__init__.py @@ -921,7 +921,7 @@ reference a common target class via many-to-one:: id = Column(Integer, primary_key=True) When using a mixin to specify a relationship or other mapper property, -the :meth:`.declared_attr.property` modifier is often helpful, as it +the :meth:`.declared_attr.after_mapping` modifier is often helpful, as it indicates that the callable should not be invoked at all until the target class is fully mapped:: @@ -930,7 +930,7 @@ target class is fully mapped:: def target_id(cls): return Column('target_id', ForeignKey('target.id')) - @declared_attr.property + @declared_attr.after_mapping def target(cls): return relationship("Target") @@ -1019,14 +1019,14 @@ requirement so that no reliance on copying is needed:: More advanced properties like that of a :func:`.column_property` which refers to other columns that are mapped to the class should make use of -:meth:`.declared_attr.property`, to ensure that the function is invoked +:meth:`.declared_attr.after_mapping`, to ensure that the function is invoked only after all columns are mapped on the target class:: class SomethingMixin(object): x = Column(Integer) y = Column(Integer) - @declared_attr.property + @declared_attr.after_mapping def x_plus_y(cls): return column_property(cls.x + cls.y) diff --git a/lib/sqlalchemy/ext/declarative/api.py b/lib/sqlalchemy/ext/declarative/api.py index 149cedbb6..99ec080f5 100644 --- a/lib/sqlalchemy/ext/declarative/api.py +++ b/lib/sqlalchemy/ext/declarative/api.py @@ -13,7 +13,7 @@ from ...orm import synonym as _orm_synonym, mapper,\ interfaces, properties from ...orm.util import polymorphic_union from ...orm.base import _mapper_or_none -from ...util import OrderedDict, classproperty +from ...util import OrderedDict, classproperty, hybridmethod, hybridproperty from ... import exc import weakref @@ -157,59 +157,48 @@ class declared_attr(interfaces._MappedAttribute, property): """ - def __init__(self, fget, *arg, **kw): - super(declared_attr, self).__init__(fget, *arg, **kw) + def __init__(self, fget, cascading=False, defer_until_mapping=False): + super(declared_attr, self).__init__(fget) self.__doc__ = fget.__doc__ - - def __get__(desc, self, cls): - return desc.fget(cls) - - @classproperty - def cascading(cls): - return _memoized_declared_attr.cascading - - @classproperty - def memoized(cls): - return _memoized_declared_attr - - @classproperty - def column(cls): - return _declared_column - - @classproperty - def property(cls): - return _declared_property - - defer_until_mapping = False - - -class _memoized_declared_attr(declared_attr): - def __init__(self, fget, cascading=False): - super(_memoized_declared_attr, self).__init__(fget) - self.reg = weakref.WeakKeyDictionary() + self._reg = weakref.WeakKeyDictionary() self._cascading = cascading + self._defer_until_mapping = defer_until_mapping def __get__(desc, self, cls): - if desc.defer_until_mapping: + if desc._defer_until_mapping: return desc - elif cls in desc.reg: - return desc.reg[cls] + elif cls in desc._reg: + return desc._reg[cls] else: - desc.reg[cls] = obj = desc.fget(cls) + desc._reg[cls] = obj = desc.fget(cls) return obj - @classproperty + @hybridmethod + def _stateful(cls, **kw): + return _stateful_declared_attr(**kw) + + @hybridproperty def cascading(cls): - return lambda decorated: cls(decorated, cascading=True) + return cls._stateful(cascading=True) + + @hybridproperty + def after_mapping(cls): + return cls._stateful(defer_until_mapping=True) + defer_until_mapping = False -class _declared_column(_memoized_declared_attr): - pass +class _stateful_declared_attr(declared_attr): + def __init__(self, **kw): + self.kw = kw -class _declared_property(_memoized_declared_attr): - defer_until_mapping = True + def _stateful(self, **kw): + new_kw = self.kw.copy() + new_kw.update(kw) + return _stateful_declared_attr(**new_kw) + def __call__(self, fn): + return declared_attr(fn, **self.kw) def declarative_base(bind=None, metadata=None, mapper=None, cls=object, diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py index 526af2823..4266da852 100644 --- a/lib/sqlalchemy/ext/declarative/base.py +++ b/lib/sqlalchemy/ext/declarative/base.py @@ -55,7 +55,7 @@ def _get_immediate_cls_attr(cls, attrname): def _as_declarative(cls, classname, dict_): - from .api import declared_attr, _memoized_declared_attr + from .api import declared_attr # dict_ will be a dictproxy, which we can't write to, and we need to! dict_ = dict(dict_) @@ -159,17 +159,17 @@ def _as_declarative(cls, classname, dict_): "column_property(), relationship(), etc.) must " "be declared as @declared_attr callables " "on declarative mixin classes.") - elif isinstance(obj, _memoized_declared_attr): + elif isinstance(obj, declarative_props): + if isinstance(obj, util.classproperty): + util.warn_deprecated( + "Use of sqlalchemy.util.classproperty on " + "declarative classes is deprecated.") if obj._cascading: - dict_[name] = ret = obj.__get__(obj, cls) + dict_[name] = column_copies[obj] = \ + ret = obj.__get__(obj, cls) else: - dict_[name] = ret = getattr(cls, name) - if isinstance(ret, (Column, MapperProperty)) and \ - ret.doc is None: - ret.doc = obj.__doc__ - elif isinstance(obj, declarative_props): - dict_[name] = ret = \ - column_copies[obj] = getattr(cls, name) + dict_[name] = column_copies[obj] = \ + ret = getattr(cls, name) if isinstance(ret, (Column, MapperProperty)) and \ ret.doc is None: ret.doc = obj.__doc__ @@ -193,7 +193,7 @@ def _as_declarative(cls, classname, dict_): value = dict_[k] if isinstance(value, declarative_props): - if value.defer_until_mapping: + if value._defer_until_mapping: add_later[k] = value else: value = getattr(cls, k) diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py index c963b18c3..dfed5b90a 100644 --- a/lib/sqlalchemy/util/__init__.py +++ b/lib/sqlalchemy/util/__init__.py @@ -33,7 +33,8 @@ from .langhelpers import iterate_attributes, class_hierarchy, \ duck_type_collection, assert_arg_type, symbol, dictlike_iteritems,\ classproperty, set_creation_order, warn_exception, warn, NoneType,\ constructor_copy, methods_equivalent, chop_traceback, asint,\ - generic_repr, counter, PluginLoader, hybridmethod, safe_reraise,\ + generic_repr, counter, PluginLoader, hybridproperty, hybridmethod, \ + safe_reraise,\ get_callable_argspec, only_once, attrsetter, ellipses_string, \ warn_limited diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 76f85f605..35d3b7174 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -1090,10 +1090,21 @@ class classproperty(property): return desc.fget(cls) +class hybridproperty(object): + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + if instance is None: + return self.func(owner) + else: + return self.func(instance) + + class hybridmethod(object): """Decorate a function as cls- or instance- level.""" - def __init__(self, func, expr=None): + def __init__(self, func): self.func = func def __get__(self, instance, owner): diff --git a/test/ext/declarative/test_inheritance.py b/test/ext/declarative/test_inheritance.py index f9e0a9833..d8ee0c19f 100644 --- a/test/ext/declarative/test_inheritance.py +++ b/test/ext/declarative/test_inheritance.py @@ -1327,7 +1327,7 @@ class ConcreteExtensionConfigTest( counter(cls, "something") return relationship("Something") - @declared_attr.property + @declared_attr.after_mapping def something_else(cls): counter(cls, "something_else") return relationship("Something") diff --git a/test/ext/declarative/test_mixin.py b/test/ext/declarative/test_mixin.py index da1328d69..00adca834 100644 --- a/test/ext/declarative/test_mixin.py +++ b/test/ext/declarative/test_mixin.py @@ -1305,7 +1305,7 @@ class DeclarativeMixinPropertyTest(DeclarativeTestBase): class DeclaredAttrTest(DeclarativeTestBase, testing.AssertsCompiledSQL): __dialect__ = 'default' - def test_plain_called_repeatedly(self): + def test_singleton_behavior(self): counter = mock.Mock() class Mixin(object): @@ -1321,37 +1321,6 @@ class DeclaredAttrTest(DeclarativeTestBase, testing.AssertsCompiledSQL): @declared_attr def my_other_prop(cls): return column_property(cls.my_prop + 5) - eq_(counter.mock_calls, [mock.call(A), mock.call(A)]) - - class B(Base, Mixin): - __tablename__ = 'b' - id = Column(Integer, primary_key=True) - - @declared_attr - def my_other_prop(cls): - return column_property(cls.my_prop + 5) - - eq_( - counter.mock_calls, - [mock.call(A), mock.call(A), mock.call(B), mock.call(B)] - ) - - def test_singleton_called_once(self): - counter = mock.Mock() - - class Mixin(object): - @declared_attr.memoized - def my_prop(cls): - counter(cls) - return Column('x', Integer) - - class A(Base, Mixin): - __tablename__ = 'a' - id = Column(Integer, primary_key=True) - - @declared_attr - def my_other_prop(cls): - return column_property(cls.my_prop + 5) eq_(counter.mock_calls, [mock.call(A)]) @@ -1369,7 +1338,7 @@ class DeclaredAttrTest(DeclarativeTestBase, testing.AssertsCompiledSQL): counter = mock.Mock() class Mixin(object): - @declared_attr.memoized + @declared_attr def my_prop(cls): counter(cls.__name__) return Column('x', Integer) @@ -1386,13 +1355,13 @@ class DeclaredAttrTest(DeclarativeTestBase, testing.AssertsCompiledSQL): del A gc_collect() assert "A" not in Base._decl_class_registry - assert not Mixin.__dict__['my_prop'].reg + assert not Mixin.__dict__['my_prop']._reg def test_property_noncascade(self): counter = mock.Mock() class Mixin(object): - @declared_attr.property + @declared_attr.after_mapping def my_prop(cls): counter(cls) return column_property(cls.x) @@ -1412,7 +1381,7 @@ class DeclaredAttrTest(DeclarativeTestBase, testing.AssertsCompiledSQL): counter = mock.Mock() class Mixin(object): - @declared_attr.property.cascading + @declared_attr.after_mapping.cascading def my_prop(cls): counter(cls) return column_property(cls.x) @@ -1432,7 +1401,7 @@ class DeclaredAttrTest(DeclarativeTestBase, testing.AssertsCompiledSQL): counter = mock.Mock() class Mixin(object): - @declared_attr.column + @declared_attr def my_col(cls): counter(cls) assert not orm_base._mapper_or_none(cls) @@ -1449,7 +1418,7 @@ class DeclaredAttrTest(DeclarativeTestBase, testing.AssertsCompiledSQL): counter = mock.Mock() class Mixin(object): - @declared_attr.property + @declared_attr.after_mapping def my_prop(cls): counter(cls) assert orm_base._mapper_or_none(cls) is cls.__mapper__ @@ -1464,7 +1433,7 @@ class DeclaredAttrTest(DeclarativeTestBase, testing.AssertsCompiledSQL): def test_column_prop(self): - # this is the use case for .property. + # this is the use case for .after_mapping # we don't want address_count() to run against # the "id" column until we know we will get the # one that's mapped. @@ -1472,7 +1441,7 @@ class DeclaredAttrTest(DeclarativeTestBase, testing.AssertsCompiledSQL): class HasAddressCount(object): id = Column(Integer, primary_key=True) - @declared_attr.property + @declared_attr.after_mapping def address_count(cls): return column_property( select([func.count(Address.id)]). |