From e73f735382371ac5c05a46f2a51fd10971270fe4 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 24 Apr 2015 13:49:09 -0400 Subject: - Fixed regression regarding the declarative ``__declare_first__`` and ``__declare_last__`` accessors where these would no longer be called on the superclass of the declarative base. fixes #3383 --- lib/sqlalchemy/ext/declarative/base.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py index 7d4020b24..d5fc76ed1 100644 --- a/lib/sqlalchemy/ext/declarative/base.py +++ b/lib/sqlalchemy/ext/declarative/base.py @@ -50,7 +50,7 @@ def _resolve_for_abstract(cls): return cls -def _get_immediate_cls_attr(cls, attrname): +def _get_immediate_cls_attr(cls, attrname, strict=False): """return an attribute of the class that is either present directly on the class, e.g. not on a superclass, or is from a superclass but this superclass is a mixin, that is, not a descendant of @@ -66,11 +66,12 @@ def _get_immediate_cls_attr(cls, attrname): for base in cls.__mro__: _is_declarative_inherits = hasattr(base, '_decl_class_registry') - if attrname in base.__dict__: - value = getattr(base, attrname) - if (base is cls or - (base in cls.__bases__ and not _is_declarative_inherits)): - return value + if attrname in base.__dict__ and ( + base is cls or + ((base in cls.__bases__ if strict else True) + and not _is_declarative_inherits) + ): + return getattr(base, attrname) else: return None @@ -92,7 +93,7 @@ class _MapperConfig(object): @classmethod def setup_mapping(cls, cls_, classname, dict_): defer_map = _get_immediate_cls_attr( - cls_, '_sa_decl_prepare_nocascade') or \ + cls_, '_sa_decl_prepare_nocascade', strict=True) or \ hasattr(cls_, '_sa_decl_prepare') if defer_map: @@ -158,7 +159,8 @@ class _MapperConfig(object): for base in cls.__mro__: class_mapped = base is not cls and \ _declared_mapping_info(base) is not None and \ - not _get_immediate_cls_attr(base, '_sa_decl_prepare_nocascade') + not _get_immediate_cls_attr( + base, '_sa_decl_prepare_nocascade', strict=True) if not class_mapped and base is not cls: self._produce_column_copies(base) @@ -412,7 +414,7 @@ class _MapperConfig(object): continue if _declared_mapping_info(c) is not None and \ not _get_immediate_cls_attr( - c, '_sa_decl_prepare_nocascade'): + c, '_sa_decl_prepare_nocascade', strict=True): self.inherits = c break else: -- cgit v1.2.1 From 01700759346c82d6a39ee6a6c70581e8417b9c45 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 24 Apr 2015 17:03:34 -0400 Subject: - add the "strict" version of this lookup for __abstract__ as well, fixes #3383 --- lib/sqlalchemy/ext/declarative/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py index d5fc76ed1..062936ea7 100644 --- a/lib/sqlalchemy/ext/declarative/base.py +++ b/lib/sqlalchemy/ext/declarative/base.py @@ -39,7 +39,7 @@ def _resolve_for_abstract(cls): if cls is object: return None - if _get_immediate_cls_attr(cls, '__abstract__'): + if _get_immediate_cls_attr(cls, '__abstract__', strict=True): for sup in cls.__bases__: sup = _resolve_for_abstract(sup) if sup is not None: @@ -82,7 +82,7 @@ def _as_declarative(cls, classname, dict_): from .api import declared_attr declarative_props = (declared_attr, util.classproperty) - if _get_immediate_cls_attr(cls, '__abstract__'): + if _get_immediate_cls_attr(cls, '__abstract__', strict=True): return _MapperConfig.setup_mapping(cls, classname, dict_) -- cgit v1.2.1 From 6c0f30db81d127920ca7a68d7a28b8ea086866b6 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 26 Apr 2015 18:22:41 -0400 Subject: - Fixed a regression regarding the :meth:`.MapperEvents.instrument_class` event where its invocation was moved to be after the class manager's instrumentation of the class, which is the opposite of what the documentation for the event explicitly states. The rationale for the switch was due to Declarative taking the step of setting up the full "instrumentation manager" for a class before it was mapped for the purpose of the new ``@declared_attr`` features described in :ref:`feature_3150`, but the change was also made against the classical use of :func:`.mapper` for consistency. However, SQLSoup relies upon the instrumentation event happening before any instrumentation under classical mapping. The behavior is reverted in the case of classical and declarative mapping, the latter implemented by using a simple memoization without using class manager. fixes #3388 --- lib/sqlalchemy/ext/declarative/api.py | 21 ++++++++------------- lib/sqlalchemy/ext/declarative/base.py | 10 +++++----- 2 files changed, 13 insertions(+), 18 deletions(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/declarative/api.py b/lib/sqlalchemy/ext/declarative/api.py index 713ea0aba..3d46bd4cb 100644 --- a/lib/sqlalchemy/ext/declarative/api.py +++ b/lib/sqlalchemy/ext/declarative/api.py @@ -163,21 +163,16 @@ class declared_attr(interfaces._MappedAttribute, property): self._cascading = cascading def __get__(desc, self, cls): - # use the ClassManager for memoization of values. This is better than - # adding yet another attribute onto the class, or using weakrefs - # here which are slow and take up memory. It also allows us to - # warn for non-mapped use of declared_attr. - - manager = attributes.manager_of_class(cls) - if manager is None: - util.warn( - "Unmanaged access of declarative attribute %s from " - "non-mapped class %s" % - (desc.fget.__name__, cls.__name__)) + reg = cls.__dict__.get('_sa_declared_attr_reg', None) + if reg is None: + manager = attributes.manager_of_class(cls) + if manager is None: + util.warn( + "Unmanaged access of declarative attribute %s from " + "non-mapped class %s" % + (desc.fget.__name__, cls.__name__)) return desc.fget(cls) - reg = manager.info.get('declared_attr_reg', None) - if reg is None: return desc.fget(cls) elif desc in reg: diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py index 062936ea7..57eb54f63 100644 --- a/lib/sqlalchemy/ext/declarative/base.py +++ b/lib/sqlalchemy/ext/declarative/base.py @@ -115,10 +115,10 @@ class _MapperConfig(object): self.column_copies = {} self._setup_declared_events() - # register up front, so that @declared_attr can memoize - # function evaluations in .info - manager = instrumentation.register_class(self.cls) - manager.info['declared_attr_reg'] = {} + # temporary registry. While early 1.0 versions + # set up the ClassManager here, by API contract + # we can't do that until there's a mapper. + self.cls._sa_declared_attr_reg = {} self._scan_attributes() @@ -529,7 +529,7 @@ class _MapperConfig(object): self.local_table, **self.mapper_args ) - del mp_.class_manager.info['declared_attr_reg'] + del self.cls._sa_declared_attr_reg return mp_ -- cgit v1.2.1 From 4f6e9ccae93b9c50298b041356953cb8a96b4895 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 28 Apr 2015 19:20:01 -0400 Subject: - Fixed bug in association proxy where an any()/has() on an relationship->scalar non-object attribute comparison would fail, e.g. ``filter(Parent.some_collection_to_attribute.any(Child.attr == 'foo'))`` fixes #3397 --- lib/sqlalchemy/ext/associationproxy.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py index a74141973..d837aab52 100644 --- a/lib/sqlalchemy/ext/associationproxy.py +++ b/lib/sqlalchemy/ext/associationproxy.py @@ -365,13 +365,17 @@ class AssociationProxy(interfaces.InspectionAttrInfo): operators of the underlying proxied attributes. """ - - if self._value_is_scalar: - value_expr = getattr( - self.target_class, self.value_attr).has(criterion, **kwargs) + if self._target_is_object: + if self._value_is_scalar: + value_expr = getattr( + self.target_class, self.value_attr).has( + criterion, **kwargs) + else: + value_expr = getattr( + self.target_class, self.value_attr).any( + criterion, **kwargs) else: - value_expr = getattr( - self.target_class, self.value_attr).any(criterion, **kwargs) + value_expr = criterion # check _value_is_scalar here, otherwise # we're scalar->scalar - call .any() so that -- cgit v1.2.1 From 93b5eae9843d423378f68be928a4f1e6fcacfb87 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 29 Apr 2015 00:05:25 -0400 Subject: - add boldface for "viable" plus a note describing that this refers to the table having a primary key. fixes #3398 --- lib/sqlalchemy/ext/automap.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/automap.py b/lib/sqlalchemy/ext/automap.py index ca550ded6..448d8492e 100644 --- a/lib/sqlalchemy/ext/automap.py +++ b/lib/sqlalchemy/ext/automap.py @@ -67,7 +67,7 @@ asking it to reflect the schema and produce mappings:: Above, calling :meth:`.AutomapBase.prepare` while passing along the :paramref:`.AutomapBase.prepare.reflect` parameter indicates that the :meth:`.MetaData.reflect` method will be called on this declarative base -classes' :class:`.MetaData` collection; then, each viable +classes' :class:`.MetaData` collection; then, each **viable** :class:`.Table` within the :class:`.MetaData` will get a new mapped class generated automatically. The :class:`.ForeignKeyConstraint` objects which link the various tables together will be used to produce new, bidirectional @@ -76,6 +76,12 @@ follow along a default naming scheme that we can customize. At this point, our basic mapping consisting of related ``User`` and ``Address`` classes is ready to use in the traditional way. +.. note:: By **viable**, we mean that for a table to be mapped, it must + specify a primary key. Additionally, if the table is detected as being + a pure association table between two other tables, it will not be directly + mapped and will instead be configured as a many-to-many table between + the mappings for the two referring tables. + Generating Mappings from an Existing MetaData ============================================= -- cgit v1.2.1 From 95949db715ff54be01bfd260a51903ede60597ae Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 1 May 2015 12:06:34 -0400 Subject: - Repair _reinstall_default_lookups to also flip the _extended flag off again so that test fixtures setup/teardown instrumentation as expected - clean up test_extendedattr.py and fix it to no longer leak itself outside by ensuring _reinstall_default_lookups is always called, part of #3408 - Fixed bug where when using extended attribute instrumentation system, the correct exception would not be raised when :func:`.class_mapper` were called with an invalid input that also happened to not be weak referencable, such as an integer. fixes #3408 --- lib/sqlalchemy/ext/instrumentation.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/instrumentation.py b/lib/sqlalchemy/ext/instrumentation.py index 024136661..30a0ab7d7 100644 --- a/lib/sqlalchemy/ext/instrumentation.py +++ b/lib/sqlalchemy/ext/instrumentation.py @@ -166,7 +166,13 @@ class ExtendedInstrumentationRegistry(InstrumentationFactory): def manager_of_class(self, cls): if cls is None: return None - return self._manager_finders.get(cls, _default_manager_getter)(cls) + try: + finder = self._manager_finders.get(cls, _default_manager_getter) + except TypeError: + # due to weakref lookup on invalid object + return None + else: + return finder(cls) def state_of(self, instance): if instance is None: @@ -392,6 +398,7 @@ def _reinstall_default_lookups(): manager_of_class=_default_manager_getter ) ) + _instrumentation_factory._extended = False def _install_lookups(lookups): -- cgit v1.2.1 From be81cb200d9efb45c3bf331315c54dff778b5de6 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 5 May 2015 16:55:09 -0400 Subject: - fix typo MANYTOONE -> MANYTOMANY, fixes #3415 --- lib/sqlalchemy/ext/automap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/automap.py b/lib/sqlalchemy/ext/automap.py index 448d8492e..119d10c42 100644 --- a/lib/sqlalchemy/ext/automap.py +++ b/lib/sqlalchemy/ext/automap.py @@ -631,7 +631,7 @@ def generate_relationship( :param base: the :class:`.AutomapBase` class doing the prepare. :param direction: indicate the "direction" of the relationship; this will - be one of :data:`.ONETOMANY`, :data:`.MANYTOONE`, :data:`.MANYTOONE`. + be one of :data:`.ONETOMANY`, :data:`.MANYTOONE`, :data:`.MANYTOMANY`. :param return_fn: the function that is used by default to create the relationship. This will be either :func:`.relationship` or -- cgit v1.2.1 From 525cc6fe0247a76201c173e535d8309333461afc Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 21 May 2015 14:21:01 -0400 Subject: - Fixed regression in the :mod:`sqlalchemy.ext.mutable` extension as a result of the bugfix for :ticket:`3167`, where attribute and validation events are no longer called within the flush process. The mutable extension was relying upon this behavior in the case where a column level Python-side default were responsible for generating the new value on INSERT or UPDATE, or when a value were fetched from the RETURNING clause for "eager defaults" mode. The new value would not be subject to any event when populated and the mutable extension could not establish proper coercion or history listening. A new event :meth:`.InstanceEvents.refresh_flush` is added which the mutable extension now makes use of for this use case. fixes #3427 - Added new event :meth:`.InstanceEvents.refresh_flush`, invoked when an INSERT or UPDATE level default value fetched via RETURNING or Python-side default is invoked within the flush process. This is to provide a hook that is no longer present as a result of :ticket:`3167`, where attribute and validation events are no longer called within the flush process. - Added a new semi-public method to :class:`.MutableBase` :meth:`.MutableBase._get_listen_keys`. Overriding this method is needed in the case where a :class:`.MutableBase` subclass needs events to propagate for attribute keys other than the key to which the mutable type is associated with, when intercepting the :meth:`.InstanceEvents.refresh` or :meth:`.InstanceEvents.refresh_flush` events. The current example of this is composites using :class:`.MutableComposite`. --- lib/sqlalchemy/ext/mutable.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py index 24fc37a42..501b18f39 100644 --- a/lib/sqlalchemy/ext/mutable.py +++ b/lib/sqlalchemy/ext/mutable.py @@ -402,6 +402,27 @@ class MutableBase(object): msg = "Attribute '%s' does not accept objects of type %s" raise ValueError(msg % (key, type(value))) + @classmethod + def _get_listen_keys(cls, attribute): + """Given a descriptor attribute, return a ``set()`` of the attribute + keys which indicate a change in the state of this attribute. + + This is normally just ``set([attribute.key])``, but can be overridden + to provide for additional keys. E.g. a :class:`.MutableComposite` + augments this set with the attribute keys associated with the columns + that comprise the composite value. + + This collection is consulted in the case of intercepting the + :meth:`.InstanceEvents.refresh` and + :meth:`.InstanceEvents.refresh_flush` events, which pass along a list + of attribute names that have been refreshed; the list is compared + against this set to determine if action needs to be taken. + + .. versionadded:: 1.0.5 + + """ + return set([attribute.key]) + @classmethod def _listen_on_attribute(cls, attribute, coerce, parent_cls): """Establish this type as a mutation listener for the given @@ -415,6 +436,8 @@ class MutableBase(object): # rely on "propagate" here parent_cls = attribute.class_ + listen_keys = cls._get_listen_keys(attribute) + def load(state, *args): """Listen for objects loaded or refreshed. @@ -429,6 +452,10 @@ class MutableBase(object): state.dict[key] = val val._parents[state.obj()] = key + def load_attrs(state, ctx, attrs): + if not attrs or listen_keys.intersection(attrs): + load(state) + def set(target, value, oldvalue, initiator): """Listen for set/replace events on the target data member. @@ -463,7 +490,9 @@ class MutableBase(object): event.listen(parent_cls, 'load', load, raw=True, propagate=True) - event.listen(parent_cls, 'refresh', load, + event.listen(parent_cls, 'refresh', load_attrs, + raw=True, propagate=True) + event.listen(parent_cls, 'refresh_flush', load_attrs, raw=True, propagate=True) event.listen(attribute, 'set', set, raw=True, retval=True, propagate=True) @@ -574,6 +603,10 @@ class MutableComposite(MutableBase): """ + @classmethod + def _get_listen_keys(cls, attribute): + return set([attribute.key]).union(attribute.property._attribute_keys) + def changed(self): """Subclasses should call this method whenever change events occur.""" -- cgit v1.2.1 From 36d2b40cf55bc0f4e850e9ec4e905f0051f573f2 Mon Sep 17 00:00:00 2001 From: INADA Naoki Date: Tue, 2 Jun 2015 22:05:18 +0900 Subject: baked: Support initial args for cache key When making baked query in classmethod of declarative base, cls should be added in cache key. @as_declarative class Base(object): @classmethod def baked_query(cls): return bakery(lambda: session.query(cls), (cls,)) --- lib/sqlalchemy/ext/baked.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py index 65d6a8603..96e1f1312 100644 --- a/lib/sqlalchemy/ext/baked.py +++ b/lib/sqlalchemy/ext/baked.py @@ -49,8 +49,8 @@ class BakedQuery(object): _bakery = util.LRUCache(size) - def call(initial_fn): - return cls(_bakery, initial_fn) + def call(initial_fn, args=()): + return cls(_bakery, initial_fn, args) return call -- cgit v1.2.1 From 47d29eae8fa14ab7b1aab63797860a475338f950 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 3 Jun 2015 10:08:33 -0400 Subject: - changelog for pr bitbucket:54 - alter the approach so that the initial callable is working just like add_criteria/with_criteria --- lib/sqlalchemy/ext/baked.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py index 96e1f1312..f01e0b348 100644 --- a/lib/sqlalchemy/ext/baked.py +++ b/lib/sqlalchemy/ext/baked.py @@ -34,11 +34,8 @@ class BakedQuery(object): __slots__ = 'steps', '_bakery', '_cache_key', '_spoiled' def __init__(self, bakery, initial_fn, args=()): - if args: - self._cache_key = tuple(args) - else: - self._cache_key = () - self._update_cache_key(initial_fn) + self._cache_key = () + self._update_cache_key(initial_fn, args) self.steps = [initial_fn] self._spoiled = False self._bakery = bakery @@ -49,7 +46,7 @@ class BakedQuery(object): _bakery = util.LRUCache(size) - def call(initial_fn, args=()): + def call(initial_fn, *args): return cls(_bakery, initial_fn, args) return call -- cgit v1.2.1 From 370b237ddb6af0ba62f0ce5b1bedfe7556c6ccf3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 18 Jun 2015 13:13:54 -0400 Subject: - automap is stable --- lib/sqlalchemy/ext/automap.py | 6 ------ 1 file changed, 6 deletions(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/automap.py b/lib/sqlalchemy/ext/automap.py index 119d10c42..1006e7326 100644 --- a/lib/sqlalchemy/ext/automap.py +++ b/lib/sqlalchemy/ext/automap.py @@ -11,12 +11,6 @@ schema, typically though not necessarily one which is reflected. .. versionadded:: 0.9.1 Added :mod:`sqlalchemy.ext.automap`. -.. note:: - - The :mod:`sqlalchemy.ext.automap` extension should be considered - **experimental** as of 0.9.1. Featureset and API stability is - not guaranteed at this time. - It is hoped that the :class:`.AutomapBase` system provides a quick and modernized solution to the problem that the very famous `SQLSoup `_ -- cgit v1.2.1 From ebe3e4569fc7e97c053c5bc8eb9a6288587d2b8b Mon Sep 17 00:00:00 2001 From: Benjamin Petersen Date: Mon, 22 Jun 2015 16:27:56 -0400 Subject: Missing comma in method args --- lib/sqlalchemy/ext/hybrid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py index f94c2079e..9c6178264 100644 --- a/lib/sqlalchemy/ext/hybrid.py +++ b/lib/sqlalchemy/ext/hybrid.py @@ -45,7 +45,7 @@ as the class itself:: return self.end - self.start @hybrid_method - def contains(self,point): + def contains(self, point): return (self.start <= point) & (point < self.end) @hybrid_method -- cgit v1.2.1 From 5717186122d5538e53205268846beb7143a3d4cc Mon Sep 17 00:00:00 2001 From: Yuri Baida Date: Fri, 26 Jun 2015 10:45:48 -0700 Subject: Fix code examples in automap's documentation Fix camelize_classname and pluralize_collection functions as they didn't work as expected. --- lib/sqlalchemy/ext/automap.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'lib/sqlalchemy/ext') diff --git a/lib/sqlalchemy/ext/automap.py b/lib/sqlalchemy/ext/automap.py index 1006e7326..330992e56 100644 --- a/lib/sqlalchemy/ext/automap.py +++ b/lib/sqlalchemy/ext/automap.py @@ -188,7 +188,7 @@ scheme for class names and a "pluralizer" for collection names using the "'words_and_underscores' -> 'WordsAndUnderscores'" return str(tablename[0].upper() + \\ - re.sub(r'_(\w)', lambda m: m.group(1).upper(), tablename[1:])) + re.sub(r'_([a-z])', lambda m: m.group(1).upper(), tablename[1:])) _pluralizer = inflect.engine() def pluralize_collection(base, local_cls, referred_cls, constraint): @@ -196,10 +196,9 @@ scheme for class names and a "pluralizer" for collection names using the "'SomeTerm' -> 'some_terms'" referred_name = referred_cls.__name__ - uncamelized = referred_name[0].lower() + \\ - re.sub(r'\W', - lambda m: "_%s" % m.group(0).lower(), - referred_name[1:]) + uncamelized = re.sub(r'[A-Z]', + lambda m: "_%s" % m.group(0).lower(), + referred_name)[1:] pluralized = _pluralizer.plural(uncamelized) return pluralized -- cgit v1.2.1