summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/ext
diff options
context:
space:
mode:
authorjonathan vanasco <jonathan@2xlp.com>2015-07-10 18:52:46 -0400
committerjonathan vanasco <jonathan@2xlp.com>2015-07-10 18:52:46 -0400
commit0a5dcdc2c4112478d87e5cd68c187e302f586834 (patch)
treedfdb29e7668d4a007bd243805fd38cbccd971ad1 /lib/sqlalchemy/ext
parent6de3d490a2adb0fff43f98e15a53407b46668b61 (diff)
parentcadc2e0ba00feadf7e860598030bda0fb8bc691c (diff)
downloadsqlalchemy-0a5dcdc2c4112478d87e5cd68c187e302f586834.tar.gz
Merge branch 'master' of bitbucket.org:zzzeek/sqlalchemy
Diffstat (limited to 'lib/sqlalchemy/ext')
-rw-r--r--lib/sqlalchemy/ext/associationproxy.py16
-rw-r--r--lib/sqlalchemy/ext/automap.py25
-rw-r--r--lib/sqlalchemy/ext/baked.py11
-rw-r--r--lib/sqlalchemy/ext/declarative/api.py21
-rw-r--r--lib/sqlalchemy/ext/declarative/base.py34
-rw-r--r--lib/sqlalchemy/ext/hybrid.py2
-rw-r--r--lib/sqlalchemy/ext/instrumentation.py9
-rw-r--r--lib/sqlalchemy/ext/mutable.py35
8 files changed, 95 insertions, 58 deletions
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
diff --git a/lib/sqlalchemy/ext/automap.py b/lib/sqlalchemy/ext/automap.py
index ca550ded6..330992e56 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 <https://sqlsoup.readthedocs.org/en/latest/>`_
@@ -67,7 +61,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 +70,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
=============================================
@@ -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
@@ -625,7 +624,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
diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py
index 65d6a8603..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,8 +46,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
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 7d4020b24..57eb54f63 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:
@@ -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
@@ -81,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_)
@@ -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:
@@ -114,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()
@@ -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:
@@ -527,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_
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
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):
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
@@ -403,6 +403,27 @@ class MutableBase(object):
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
mapped descriptor.
@@ -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."""