Remove Elixir library
Update SQLAlchemy
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
# ext/__init__.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ext/associationproxy.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -15,11 +15,9 @@ See the example ``examples/association/proxied_association.py``.
|
||||
import itertools
|
||||
import operator
|
||||
import weakref
|
||||
from sqlalchemy import exceptions
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.orm import collections, ColumnProperty
|
||||
from sqlalchemy.sql import not_
|
||||
from .. import exc, orm, util
|
||||
from ..orm import collections, interfaces
|
||||
from ..sql import not_, or_
|
||||
|
||||
|
||||
def association_proxy(target_collection, attr, **kw):
|
||||
@@ -29,24 +27,25 @@ def association_proxy(target_collection, attr, **kw):
|
||||
|
||||
The returned value is an instance of :class:`.AssociationProxy`.
|
||||
|
||||
Implements a Python property representing a relationship as a collection of
|
||||
simpler values, or a scalar value. The proxied property will mimic the collection type of
|
||||
the target (list, dict or set), or, in the case of a one to one relationship,
|
||||
a simple scalar value.
|
||||
Implements a Python property representing a relationship as a collection
|
||||
of simpler values, or a scalar value. The proxied property will mimic
|
||||
the collection type of the target (list, dict or set), or, in the case of
|
||||
a one to one relationship, a simple scalar value.
|
||||
|
||||
:param target_collection: Name of the attribute we'll proxy to.
|
||||
This attribute is typically mapped by
|
||||
:func:`~sqlalchemy.orm.relationship` to link to a target collection, but
|
||||
can also be a many-to-one or non-scalar relationship.
|
||||
|
||||
:param attr: Attribute on the associated instance or instances we'll proxy for.
|
||||
:param attr: Attribute on the associated instance or instances we'll
|
||||
proxy for.
|
||||
|
||||
For example, given a target collection of [obj1, obj2], a list created
|
||||
by this proxy property would look like [getattr(obj1, *attr*),
|
||||
getattr(obj2, *attr*)]
|
||||
|
||||
If the relationship is one-to-one or otherwise uselist=False, then simply:
|
||||
getattr(obj, *attr*)
|
||||
If the relationship is one-to-one or otherwise uselist=False, then
|
||||
simply: getattr(obj, *attr*)
|
||||
|
||||
:param creator: optional.
|
||||
|
||||
@@ -76,9 +75,22 @@ def association_proxy(target_collection, attr, **kw):
|
||||
return AssociationProxy(target_collection, attr, **kw)
|
||||
|
||||
|
||||
class AssociationProxy(object):
|
||||
ASSOCIATION_PROXY = util.symbol('ASSOCIATION_PROXY')
|
||||
"""Symbol indicating an :class:`_InspectionAttr` that's
|
||||
of type :class:`.AssociationProxy`.
|
||||
|
||||
Is assigned to the :attr:`._InspectionAttr.extension_type`
|
||||
attibute.
|
||||
|
||||
"""
|
||||
|
||||
class AssociationProxy(interfaces._InspectionAttr):
|
||||
"""A descriptor that presents a read/write view of an object attribute."""
|
||||
|
||||
is_attribute = False
|
||||
extension_type = ASSOCIATION_PROXY
|
||||
|
||||
|
||||
def __init__(self, target_collection, attr, creator=None,
|
||||
getset_factory=None, proxy_factory=None,
|
||||
proxy_bulk_set=None):
|
||||
@@ -91,34 +103,36 @@ class AssociationProxy(object):
|
||||
:param target_collection: Name of the collection we'll proxy to,
|
||||
usually created with :func:`.relationship`.
|
||||
|
||||
:param attr: Attribute on the collected instances we'll proxy for. For example,
|
||||
given a target collection of [obj1, obj2], a list created by this
|
||||
proxy property would look like [getattr(obj1, attr), getattr(obj2,
|
||||
attr)]
|
||||
:param attr: Attribute on the collected instances we'll proxy
|
||||
for. For example, given a target collection of [obj1, obj2], a
|
||||
list created by this proxy property would look like
|
||||
[getattr(obj1, attr), getattr(obj2, attr)]
|
||||
|
||||
:param creator: Optional. When new items are added to this proxied collection, new
|
||||
instances of the class collected by the target collection will be
|
||||
created. For list and set collections, the target class constructor
|
||||
will be called with the 'value' for the new instance. For dict
|
||||
types, two arguments are passed: key and value.
|
||||
:param creator: Optional. When new items are added to this proxied
|
||||
collection, new instances of the class collected by the target
|
||||
collection will be created. For list and set collections, the
|
||||
target class constructor will be called with the 'value' for the
|
||||
new instance. For dict types, two arguments are passed:
|
||||
key and value.
|
||||
|
||||
If you want to construct instances differently, supply a 'creator'
|
||||
function that takes arguments as above and returns instances.
|
||||
|
||||
:param getset_factory: Optional. Proxied attribute access is automatically handled by
|
||||
routines that get and set values based on the `attr` argument for
|
||||
this proxy.
|
||||
:param getset_factory: Optional. Proxied attribute access is
|
||||
automatically handled by routines that get and set values based on
|
||||
the `attr` argument for this proxy.
|
||||
|
||||
If you would like to customize this behavior, you may supply a
|
||||
`getset_factory` callable that produces a tuple of `getter` and
|
||||
`setter` functions. The factory is called with two arguments, the
|
||||
abstract type of the underlying collection and this proxy instance.
|
||||
|
||||
:param proxy_factory: Optional. The type of collection to emulate is determined by
|
||||
sniffing the target collection. If your collection type can't be
|
||||
determined by duck typing or you'd like to use a different
|
||||
collection implementation, you may supply a factory function to
|
||||
produce those collections. Only applicable to non-scalar relationships.
|
||||
:param proxy_factory: Optional. The type of collection to emulate is
|
||||
determined by sniffing the target collection. If your collection
|
||||
type can't be determined by duck typing or you'd like to use a
|
||||
different collection implementation, you may supply a factory
|
||||
function to produce those collections. Only applicable to
|
||||
non-scalar relationships.
|
||||
|
||||
:param proxy_bulk_set: Optional, use with proxy_factory. See
|
||||
the _set() method for details.
|
||||
@@ -217,6 +231,10 @@ class AssociationProxy(object):
|
||||
return not self._get_property().\
|
||||
mapper.get_property(self.value_attr).uselist
|
||||
|
||||
@util.memoized_property
|
||||
def _target_is_object(self):
|
||||
return getattr(self.target_class, self.value_attr).impl.uses_objects
|
||||
|
||||
def __get__(self, obj, class_):
|
||||
if self.owning_class is None:
|
||||
self.owning_class = class_ and class_ or type(obj)
|
||||
@@ -224,7 +242,11 @@ class AssociationProxy(object):
|
||||
return self
|
||||
|
||||
if self.scalar:
|
||||
return self._scalar_get(getattr(obj, self.target_collection))
|
||||
target = getattr(obj, self.target_collection)
|
||||
if target is not None:
|
||||
return self._scalar_get(target)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
# If the owning instance is reborn (orm session resurrect,
|
||||
@@ -281,7 +303,8 @@ class AssociationProxy(object):
|
||||
self.collection_class = util.duck_type_collection(lazy_collection())
|
||||
|
||||
if self.proxy_factory:
|
||||
return self.proxy_factory(lazy_collection, creator, self.value_attr, self)
|
||||
return self.proxy_factory(
|
||||
lazy_collection, creator, self.value_attr, self)
|
||||
|
||||
if self.getset_factory:
|
||||
getter, setter = self.getset_factory(self.collection_class, self)
|
||||
@@ -289,13 +312,16 @@ class AssociationProxy(object):
|
||||
getter, setter = self._default_getset(self.collection_class)
|
||||
|
||||
if self.collection_class is list:
|
||||
return _AssociationList(lazy_collection, creator, getter, setter, self)
|
||||
return _AssociationList(
|
||||
lazy_collection, creator, getter, setter, self)
|
||||
elif self.collection_class is dict:
|
||||
return _AssociationDict(lazy_collection, creator, getter, setter, self)
|
||||
return _AssociationDict(
|
||||
lazy_collection, creator, getter, setter, self)
|
||||
elif self.collection_class is set:
|
||||
return _AssociationSet(lazy_collection, creator, getter, setter, self)
|
||||
return _AssociationSet(
|
||||
lazy_collection, creator, getter, setter, self)
|
||||
else:
|
||||
raise exceptions.ArgumentError(
|
||||
raise exc.ArgumentError(
|
||||
'could not guess which interface to use for '
|
||||
'collection_class "%s" backing "%s"; specify a '
|
||||
'proxy_factory and proxy_bulk_set manually' %
|
||||
@@ -323,7 +349,7 @@ class AssociationProxy(object):
|
||||
elif self.collection_class is set:
|
||||
proxy.update(values)
|
||||
else:
|
||||
raise exceptions.ArgumentError(
|
||||
raise exc.ArgumentError(
|
||||
'no proxy_bulk_set supplied for custom '
|
||||
'collection_class implementation')
|
||||
|
||||
@@ -342,9 +368,11 @@ class AssociationProxy(object):
|
||||
"""
|
||||
|
||||
if self._value_is_scalar:
|
||||
value_expr = getattr(self.target_class, self.value_attr).has(criterion, **kwargs)
|
||||
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)
|
||||
value_expr = getattr(
|
||||
self.target_class, self.value_attr).any(criterion, **kwargs)
|
||||
|
||||
# check _value_is_scalar here, otherwise
|
||||
# we're scalar->scalar - call .any() so that
|
||||
@@ -368,10 +396,17 @@ class AssociationProxy(object):
|
||||
|
||||
"""
|
||||
|
||||
return self._comparator.has(
|
||||
if self._target_is_object:
|
||||
return self._comparator.has(
|
||||
getattr(self.target_class, self.value_attr).\
|
||||
has(criterion, **kwargs)
|
||||
)
|
||||
else:
|
||||
if criterion is not None or kwargs:
|
||||
raise exc.ArgumentError(
|
||||
"Non-empty has() not allowed for "
|
||||
"column-targeted association proxy; use ==")
|
||||
return self._comparator.has()
|
||||
|
||||
def contains(self, obj):
|
||||
"""Produce a proxied 'contains' expression using EXISTS.
|
||||
@@ -391,10 +426,21 @@ class AssociationProxy(object):
|
||||
return self._comparator.any(**{self.value_attr: obj})
|
||||
|
||||
def __eq__(self, obj):
|
||||
return self._comparator.has(**{self.value_attr: obj})
|
||||
# note the has() here will fail for collections; eq_()
|
||||
# is only allowed with a scalar.
|
||||
if obj is None:
|
||||
return or_(
|
||||
self._comparator.has(**{self.value_attr: obj}),
|
||||
self._comparator == None
|
||||
)
|
||||
else:
|
||||
return self._comparator.has(**{self.value_attr: obj})
|
||||
|
||||
def __ne__(self, obj):
|
||||
return not_(self.__eq__(obj))
|
||||
# note the has() here will fail for collections; eq_()
|
||||
# is only allowed with a scalar.
|
||||
return self._comparator.has(
|
||||
getattr(self.target_class, self.value_attr) != obj)
|
||||
|
||||
|
||||
class _lazy_collection(object):
|
||||
@@ -405,18 +451,19 @@ class _lazy_collection(object):
|
||||
def __call__(self):
|
||||
obj = self.ref()
|
||||
if obj is None:
|
||||
raise exceptions.InvalidRequestError(
|
||||
raise exc.InvalidRequestError(
|
||||
"stale association proxy, parent object has gone out of "
|
||||
"scope")
|
||||
return getattr(obj, self.target)
|
||||
|
||||
def __getstate__(self):
|
||||
return {'obj':self.ref(), 'target':self.target}
|
||||
return {'obj': self.ref(), 'target': self.target}
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.ref = weakref.ref(state['obj'])
|
||||
self.target = state['target']
|
||||
|
||||
|
||||
class _AssociationCollection(object):
|
||||
def __init__(self, lazy_collection, creator, getter, setter, parent):
|
||||
"""Constructs an _AssociationCollection.
|
||||
@@ -454,17 +501,20 @@ class _AssociationCollection(object):
|
||||
def __len__(self):
|
||||
return len(self.col)
|
||||
|
||||
def __nonzero__(self):
|
||||
def __bool__(self):
|
||||
return bool(self.col)
|
||||
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __getstate__(self):
|
||||
return {'parent':self.parent, 'lazy_collection':self.lazy_collection}
|
||||
return {'parent': self.parent, 'lazy_collection': self.lazy_collection}
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.parent = state['parent']
|
||||
self.lazy_collection = state['lazy_collection']
|
||||
self.parent._inflate(self)
|
||||
|
||||
|
||||
class _AssociationList(_AssociationCollection):
|
||||
"""Generic, converting, list-to-list proxy."""
|
||||
|
||||
@@ -492,7 +542,7 @@ class _AssociationList(_AssociationCollection):
|
||||
stop = index.stop
|
||||
step = index.step or 1
|
||||
|
||||
rng = range(index.start or 0, stop, step)
|
||||
rng = list(range(index.start or 0, stop, step))
|
||||
if step == 1:
|
||||
for i in rng:
|
||||
del self[index.start]
|
||||
@@ -547,7 +597,7 @@ class _AssociationList(_AssociationCollection):
|
||||
|
||||
def count(self, value):
|
||||
return sum([1 for _ in
|
||||
itertools.ifilter(lambda v: v == value, iter(self))])
|
||||
util.itertools_filter(lambda v: v == value, iter(self))])
|
||||
|
||||
def extend(self, values):
|
||||
for v in values:
|
||||
@@ -646,14 +696,16 @@ class _AssociationList(_AssociationCollection):
|
||||
def __hash__(self):
|
||||
raise TypeError("%s objects are unhashable" % type(self).__name__)
|
||||
|
||||
for func_name, func in locals().items():
|
||||
if (util.callable(func) and func.func_name == func_name and
|
||||
for func_name, func in list(locals().items()):
|
||||
if (util.callable(func) and func.__name__ == func_name and
|
||||
not func.__doc__ and hasattr(list, func_name)):
|
||||
func.__doc__ = getattr(list, func_name).__doc__
|
||||
del func_name, func
|
||||
|
||||
|
||||
_NotProvided = util.symbol('_NotProvided')
|
||||
|
||||
|
||||
class _AssociationDict(_AssociationCollection):
|
||||
"""Generic, converting, dict-to-dict proxy."""
|
||||
|
||||
@@ -687,7 +739,7 @@ class _AssociationDict(_AssociationCollection):
|
||||
return key in self.col
|
||||
|
||||
def __iter__(self):
|
||||
return self.col.iterkeys()
|
||||
return iter(self.col.keys())
|
||||
|
||||
def clear(self):
|
||||
self.col.clear()
|
||||
@@ -732,24 +784,27 @@ class _AssociationDict(_AssociationCollection):
|
||||
def keys(self):
|
||||
return self.col.keys()
|
||||
|
||||
def iterkeys(self):
|
||||
return self.col.iterkeys()
|
||||
if util.py2k:
|
||||
def iteritems(self):
|
||||
return ((key, self._get(self.col[key])) for key in self.col)
|
||||
|
||||
def values(self):
|
||||
return [ self._get(member) for member in self.col.values() ]
|
||||
def itervalues(self):
|
||||
return (self._get(self.col[key]) for key in self.col)
|
||||
|
||||
def itervalues(self):
|
||||
for key in self.col:
|
||||
yield self._get(self.col[key])
|
||||
raise StopIteration
|
||||
def iterkeys(self):
|
||||
return self.col.iterkeys()
|
||||
|
||||
def items(self):
|
||||
return [(k, self._get(self.col[k])) for k in self]
|
||||
def values(self):
|
||||
return [self._get(member) for member in self.col.values()]
|
||||
|
||||
def iteritems(self):
|
||||
for key in self.col:
|
||||
yield (key, self._get(self.col[key]))
|
||||
raise StopIteration
|
||||
def items(self):
|
||||
return [(k, self._get(self.col[k])) for k in self]
|
||||
else:
|
||||
def items(self):
|
||||
return ((key, self._get(self.col[key])) for key in self.col)
|
||||
|
||||
def values(self):
|
||||
return (self._get(self.col[key]) for key in self.col)
|
||||
|
||||
def pop(self, key, default=_NotProvided):
|
||||
if default is _NotProvided:
|
||||
@@ -768,8 +823,8 @@ class _AssociationDict(_AssociationCollection):
|
||||
len(a))
|
||||
elif len(a) == 1:
|
||||
seq_or_map = a[0]
|
||||
# discern dict from sequence - took the advice
|
||||
# from http://www.voidspace.org.uk/python/articles/duck_typing.shtml
|
||||
# discern dict from sequence - took the advice from
|
||||
# http://www.voidspace.org.uk/python/articles/duck_typing.shtml
|
||||
# still not perfect :(
|
||||
if hasattr(seq_or_map, 'keys'):
|
||||
for item in seq_or_map:
|
||||
@@ -792,8 +847,8 @@ class _AssociationDict(_AssociationCollection):
|
||||
def __hash__(self):
|
||||
raise TypeError("%s objects are unhashable" % type(self).__name__)
|
||||
|
||||
for func_name, func in locals().items():
|
||||
if (util.callable(func) and func.func_name == func_name and
|
||||
for func_name, func in list(locals().items()):
|
||||
if (util.callable(func) and func.__name__ == func_name and
|
||||
not func.__doc__ and hasattr(dict, func_name)):
|
||||
func.__doc__ = getattr(dict, func_name).__doc__
|
||||
del func_name, func
|
||||
@@ -814,12 +869,14 @@ class _AssociationSet(_AssociationCollection):
|
||||
def __len__(self):
|
||||
return len(self.col)
|
||||
|
||||
def __nonzero__(self):
|
||||
def __bool__(self):
|
||||
if self.col:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __contains__(self, value):
|
||||
for member in self.col:
|
||||
# testlib.pragma exempt:__eq__
|
||||
@@ -990,8 +1047,8 @@ class _AssociationSet(_AssociationCollection):
|
||||
def __hash__(self):
|
||||
raise TypeError("%s objects are unhashable" % type(self).__name__)
|
||||
|
||||
for func_name, func in locals().items():
|
||||
if (util.callable(func) and func.func_name == func_name and
|
||||
for func_name, func in list(locals().items()):
|
||||
if (util.callable(func) and func.__name__ == func_name and
|
||||
not func.__doc__ and hasattr(set, func_name)):
|
||||
func.__doc__ = getattr(set, func_name).__doc__
|
||||
del func_name, func
|
||||
|
||||
@@ -0,0 +1,840 @@
|
||||
# ext/automap.py
|
||||
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Define an extension to the :mod:`sqlalchemy.ext.declarative` system
|
||||
which automatically generates mapped classes and relationships from a database
|
||||
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/>`_
|
||||
also tries to solve, that of generating a quick and rudimentary object
|
||||
model from an existing database on the fly. By addressing the issue strictly
|
||||
at the mapper configuration level, and integrating fully with existing
|
||||
Declarative class techniques, :class:`.AutomapBase` seeks to provide
|
||||
a well-integrated approach to the issue of expediently auto-generating ad-hoc
|
||||
mappings.
|
||||
|
||||
|
||||
Basic Use
|
||||
=========
|
||||
|
||||
The simplest usage is to reflect an existing database into a new model.
|
||||
We create a new :class:`.AutomapBase` class in a similar manner as to how
|
||||
we create a declarative base class, using :func:`.automap_base`.
|
||||
We then call :meth:`.AutomapBase.prepare` on the resulting base class,
|
||||
asking it to reflect the schema and produce mappings::
|
||||
|
||||
from sqlalchemy.ext.automap import automap_base
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
Base = automap_base()
|
||||
|
||||
# engine, suppose it has two tables 'user' and 'address' set up
|
||||
engine = create_engine("sqlite:///mydatabase.db")
|
||||
|
||||
# reflect the tables
|
||||
Base.prepare(engine, reflect=True)
|
||||
|
||||
# mapped classes are now created with names by default
|
||||
# matching that of the table name.
|
||||
User = Base.classes.user
|
||||
Address = Base.classes.address
|
||||
|
||||
session = Session(engine)
|
||||
|
||||
# rudimentary relationships are produced
|
||||
session.add(Address(email_address="foo@bar.com", user=User(name="foo")))
|
||||
session.commit()
|
||||
|
||||
# collection-based relationships are by default named "<classname>_collection"
|
||||
print (u1.address_collection)
|
||||
|
||||
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
|
||||
: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
|
||||
:func:`.relationship` objects between classes. The classes and relationships
|
||||
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.
|
||||
|
||||
Generating Mappings from an Existing MetaData
|
||||
=============================================
|
||||
|
||||
We can pass a pre-declared :class:`.MetaData` object to :func:`.automap_base`.
|
||||
This object can be constructed in any way, including programmatically, from
|
||||
a serialized file, or from itself being reflected using :meth:`.MetaData.reflect`.
|
||||
Below we illustrate a combination of reflection and explicit table declaration::
|
||||
|
||||
from sqlalchemy import create_engine, MetaData, Table, Column, ForeignKey
|
||||
engine = create_engine("sqlite:///mydatabase.db")
|
||||
|
||||
# produce our own MetaData object
|
||||
metadata = MetaData()
|
||||
|
||||
# we can reflect it ourselves from a database, using options
|
||||
# such as 'only' to limit what tables we look at...
|
||||
metadata.reflect(engine, only=['user', 'address'])
|
||||
|
||||
# ... or just define our own Table objects with it (or combine both)
|
||||
Table('user_order', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('user_id', ForeignKey('user.id'))
|
||||
)
|
||||
|
||||
# we can then produce a set of mappings from this MetaData.
|
||||
Base = automap_base(metadata=metadata)
|
||||
|
||||
# calling prepare() just sets up mapped classes and relationships.
|
||||
Base.prepare()
|
||||
|
||||
# mapped classes are ready
|
||||
User, Address, Order = Base.classes.user, Base.classes.address, Base.classes.user_order
|
||||
|
||||
Specifying Classes Explcitly
|
||||
============================
|
||||
|
||||
The :mod:`.sqlalchemy.ext.automap` extension allows classes to be defined
|
||||
explicitly, in a way similar to that of the :class:`.DeferredReflection` class.
|
||||
Classes that extend from :class:`.AutomapBase` act like regular declarative
|
||||
classes, but are not immediately mapped after their construction, and are instead
|
||||
mapped when we call :meth:`.AutomapBase.prepare`. The :meth:`.AutomapBase.prepare`
|
||||
method will make use of the classes we've established based on the table name
|
||||
we use. If our schema contains tables ``user`` and ``address``, we can define
|
||||
one or both of the classes to be used::
|
||||
|
||||
from sqlalchemy.ext.automap import automap_base
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
# automap base
|
||||
Base = automap_base()
|
||||
|
||||
# pre-declare User for the 'user' table
|
||||
class User(Base):
|
||||
__tablename__ = 'user'
|
||||
|
||||
# override schema elements like Columns
|
||||
user_name = Column('name', String)
|
||||
|
||||
# override relationships too, if desired.
|
||||
# we must use the same name that automap would use for the relationship,
|
||||
# and also must refer to the class name that automap will generate
|
||||
# for "address"
|
||||
address_collection = relationship("address", collection_class=set)
|
||||
|
||||
# reflect
|
||||
engine = create_engine("sqlite:///mydatabase.db")
|
||||
Base.prepare(engine, reflect=True)
|
||||
|
||||
# we still have Address generated from the tablename "address",
|
||||
# but User is the same as Base.classes.User now
|
||||
|
||||
Address = Base.classes.address
|
||||
|
||||
u1 = session.query(User).first()
|
||||
print (u1.address_collection)
|
||||
|
||||
# the backref is still there:
|
||||
a1 = session.query(Address).first()
|
||||
print (a1.user)
|
||||
|
||||
Above, one of the more intricate details is that we illustrated overriding
|
||||
one of the :func:`.relationship` objects that automap would have created.
|
||||
To do this, we needed to make sure the names match up with what automap
|
||||
would normally generate, in that the relationship name would be ``User.address_collection``
|
||||
and the name of the class referred to, from automap's perspective, is called
|
||||
``address``, even though we are referring to it as ``Address`` within our usage
|
||||
of this class.
|
||||
|
||||
Overriding Naming Schemes
|
||||
=========================
|
||||
|
||||
:mod:`.sqlalchemy.ext.automap` is tasked with producing mapped classes and
|
||||
relationship names based on a schema, which means it has decision points in how
|
||||
these names are determined. These three decision points are provided using
|
||||
functions which can be passed to the :meth:`.AutomapBase.prepare` method, and
|
||||
are known as :func:`.classname_for_table`,
|
||||
:func:`.name_for_scalar_relationship`,
|
||||
and :func:`.name_for_collection_relationship`. Any or all of these
|
||||
functions are provided as in the example below, where we use a "camel case"
|
||||
scheme for class names and a "pluralizer" for collection names using the
|
||||
`Inflect <https://pypi.python.org/pypi/inflect>`_ package::
|
||||
|
||||
import re
|
||||
import inflect
|
||||
|
||||
def camelize_classname(base, tablename, table):
|
||||
"Produce a 'camelized' class name, e.g. "
|
||||
"'words_and_underscores' -> 'WordsAndUnderscores'"
|
||||
|
||||
return str(tablename[0].upper() + \\
|
||||
re.sub(r'_(\w)', lambda m: m.group(1).upper(), tablename[1:]))
|
||||
|
||||
_pluralizer = inflect.engine()
|
||||
def pluralize_collection(base, local_cls, referred_cls, constraint):
|
||||
"Produce an 'uncamelized', 'pluralized' class name, e.g. "
|
||||
"'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:])
|
||||
pluralized = _pluralizer.plural(uncamelized)
|
||||
return pluralized
|
||||
|
||||
from sqlalchemy.ext.automap import automap_base
|
||||
|
||||
Base = automap_base()
|
||||
|
||||
engine = create_engine("sqlite:///mydatabase.db")
|
||||
|
||||
Base.prepare(engine, reflect=True,
|
||||
classname_for_table=camelize_classname,
|
||||
name_for_collection_relationship=pluralize_collection
|
||||
)
|
||||
|
||||
From the above mapping, we would now have classes ``User`` and ``Address``,
|
||||
where the collection from ``User`` to ``Address`` is called ``User.addresses``::
|
||||
|
||||
User, Address = Base.classes.User, Base.classes.Address
|
||||
|
||||
u1 = User(addresses=[Address(email="foo@bar.com")])
|
||||
|
||||
Relationship Detection
|
||||
======================
|
||||
|
||||
The vast majority of what automap accomplishes is the generation of
|
||||
:func:`.relationship` structures based on foreign keys. The mechanism
|
||||
by which this works for many-to-one and one-to-many relationships is as follows:
|
||||
|
||||
1. A given :class:`.Table`, known to be mapped to a particular class,
|
||||
is examined for :class:`.ForeignKeyConstraint` objects.
|
||||
|
||||
2. From each :class:`.ForeignKeyConstraint`, the remote :class:`.Table`
|
||||
object present is matched up to the class to which it is to be mapped,
|
||||
if any, else it is skipped.
|
||||
|
||||
3. As the :class:`.ForeignKeyConstraint` we are examining correponds to a reference
|
||||
from the immediate mapped class,
|
||||
the relationship will be set up as a many-to-one referring to the referred class;
|
||||
a corresponding one-to-many backref will be created on the referred class referring
|
||||
to this class.
|
||||
|
||||
4. The names of the relationships are determined using the
|
||||
:paramref:`.AutomapBase.prepare.name_for_scalar_relationship` and
|
||||
:paramref:`.AutomapBase.prepare.name_for_collection_relationship`
|
||||
callable functions. It is important to note that the default relationship
|
||||
naming derives the name from the **the actual class name**. If you've
|
||||
given a particular class an explicit name by declaring it, or specified an
|
||||
alternate class naming scheme, that's the name from which the relationship
|
||||
name will be derived.
|
||||
|
||||
5. The classes are inspected for an existing mapped property matching these
|
||||
names. If one is detected on one side, but none on the other side, :class:`.AutomapBase`
|
||||
attempts to create a relationship on the missing side, then uses the
|
||||
:paramref:`.relationship.back_populates` parameter in order to point
|
||||
the new relationship to the other side.
|
||||
|
||||
6. In the usual case where no relationship is on either side,
|
||||
:meth:`.AutomapBase.prepare` produces a :func:`.relationship` on the "many-to-one"
|
||||
side and matches it to the other using the :paramref:`.relationship.backref`
|
||||
parameter.
|
||||
|
||||
7. Production of the :func:`.relationship` and optionally the :func:`.backref`
|
||||
is handed off to the :paramref:`.AutomapBase.prepare.generate_relationship`
|
||||
function, which can be supplied by the end-user in order to augment
|
||||
the arguments passed to :func:`.relationship` or :func:`.backref` or to
|
||||
make use of custom implementations of these functions.
|
||||
|
||||
Custom Relationship Arguments
|
||||
-----------------------------
|
||||
|
||||
The :paramref:`.AutomapBase.prepare.generate_relationship` hook can be used
|
||||
to add parameters to relationships. For most cases, we can make use of the
|
||||
existing :func:`.automap.generate_relationship` function to return
|
||||
the object, after augmenting the given keyword dictionary with our own
|
||||
arguments.
|
||||
|
||||
Below is an illustration of how to send
|
||||
:paramref:`.relationship.cascade` and
|
||||
:paramref:`.relationship.passive_deletes`
|
||||
options along to all one-to-many relationships::
|
||||
|
||||
from sqlalchemy.ext.automap import generate_relationship
|
||||
|
||||
def _gen_relationship(base, direction, return_fn,
|
||||
attrname, local_cls, referred_cls, **kw):
|
||||
if direction is interfaces.ONETOMANY:
|
||||
kw['cascade'] = 'all, delete-orphan'
|
||||
kw['passive_deletes'] = True
|
||||
# make use of the built-in function to actually return
|
||||
# the result.
|
||||
return generate_relationship(base, direction, return_fn,
|
||||
attrname, local_cls, referred_cls, **kw)
|
||||
|
||||
from sqlalchemy.ext.automap import automap_base
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
# automap base
|
||||
Base = automap_base()
|
||||
|
||||
engine = create_engine("sqlite:///mydatabase.db")
|
||||
Base.prepare(engine, reflect=True,
|
||||
generate_relationship=_gen_relationship)
|
||||
|
||||
Many-to-Many relationships
|
||||
--------------------------
|
||||
|
||||
:mod:`.sqlalchemy.ext.automap` will generate many-to-many relationships, e.g.
|
||||
those which contain a ``secondary`` argument. The process for producing these
|
||||
is as follows:
|
||||
|
||||
1. A given :class:`.Table` is examined for :class:`.ForeignKeyConstraint` objects,
|
||||
before any mapped class has been assigned to it.
|
||||
|
||||
2. If the table contains two and exactly two :class:`.ForeignKeyConstraint`
|
||||
objects, and all columns within this table are members of these two
|
||||
:class:`.ForeignKeyConstraint` objects, the table is assumed to be a
|
||||
"secondary" table, and will **not be mapped directly**.
|
||||
|
||||
3. The two (or one, for self-referential) external tables to which the :class:`.Table`
|
||||
refers to are matched to the classes to which they will be mapped, if any.
|
||||
|
||||
4. If mapped classes for both sides are located, a many-to-many bi-directional
|
||||
:func:`.relationship` / :func:`.backref` pair is created between the two
|
||||
classes.
|
||||
|
||||
5. The override logic for many-to-many works the same as that of one-to-many/
|
||||
many-to-one; the :func:`.generate_relationship` function is called upon
|
||||
to generate the strucures and existing attributes will be maintained.
|
||||
|
||||
Using Automap with Explicit Declarations
|
||||
========================================
|
||||
|
||||
As noted previously, automap has no dependency on reflection, and can make
|
||||
use of any collection of :class:`.Table` objects within a :class:`.MetaData`
|
||||
collection. From this, it follows that automap can also be used
|
||||
generate missing relationships given an otherwise complete model that fully defines
|
||||
table metadata::
|
||||
|
||||
from sqlalchemy.ext.automap import automap_base
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
|
||||
Base = automap_base()
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'user'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
|
||||
class Address(Base):
|
||||
__tablename__ = 'address'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
email = Column(String)
|
||||
user_id = Column(ForeignKey('user.id'))
|
||||
|
||||
# produce relationships
|
||||
Base.prepare()
|
||||
|
||||
# mapping is complete, with "address_collection" and
|
||||
# "user" relationships
|
||||
a1 = Address(email='u1')
|
||||
a2 = Address(email='u2')
|
||||
u1 = User(address_collection=[a1, a2])
|
||||
assert a1.user is u1
|
||||
|
||||
Above, given mostly complete ``User`` and ``Address`` mappings, the
|
||||
:class:`.ForeignKey` which we defined on ``Address.user_id`` allowed a
|
||||
bidirectional relationship pair ``Address.user`` and ``User.address_collection``
|
||||
to be generated on the mapped classes.
|
||||
|
||||
Note that when subclassing :class:`.AutomapBase`, the :meth:`.AutomapBase.prepare`
|
||||
method is required; if not called, the classes we've declared are in an
|
||||
un-mapped state.
|
||||
|
||||
|
||||
"""
|
||||
from .declarative import declarative_base as _declarative_base
|
||||
from .declarative.base import _DeferredMapperConfig
|
||||
from ..sql import and_
|
||||
from ..schema import ForeignKeyConstraint
|
||||
from ..orm import relationship, backref, interfaces
|
||||
from .. import util
|
||||
|
||||
|
||||
def classname_for_table(base, tablename, table):
|
||||
"""Return the class name that should be used, given the name
|
||||
of a table.
|
||||
|
||||
The default implementation is::
|
||||
|
||||
return str(tablename)
|
||||
|
||||
Alternate implementations can be specified using the
|
||||
:paramref:`.AutomapBase.prepare.classname_for_table`
|
||||
parameter.
|
||||
|
||||
:param base: the :class:`.AutomapBase` class doing the prepare.
|
||||
|
||||
:param tablename: string name of the :class:`.Table`.
|
||||
|
||||
:param table: the :class:`.Table` object itself.
|
||||
|
||||
:return: a string class name.
|
||||
|
||||
.. note::
|
||||
|
||||
In Python 2, the string used for the class name **must** be a non-Unicode
|
||||
object, e.g. a ``str()`` object. The ``.name`` attribute of
|
||||
:class:`.Table` is typically a Python unicode subclass, so the ``str()``
|
||||
function should be applied to this name, after accounting for any non-ASCII
|
||||
characters.
|
||||
|
||||
"""
|
||||
return str(tablename)
|
||||
|
||||
def name_for_scalar_relationship(base, local_cls, referred_cls, constraint):
|
||||
"""Return the attribute name that should be used to refer from one
|
||||
class to another, for a scalar object reference.
|
||||
|
||||
The default implementation is::
|
||||
|
||||
return referred_cls.__name__.lower()
|
||||
|
||||
Alternate implementations can be specified using the
|
||||
:paramref:`.AutomapBase.prepare.name_for_scalar_relationship`
|
||||
parameter.
|
||||
|
||||
:param base: the :class:`.AutomapBase` class doing the prepare.
|
||||
|
||||
:param local_cls: the class to be mapped on the local side.
|
||||
|
||||
:param referred_cls: the class to be mapped on the referring side.
|
||||
|
||||
:param constraint: the :class:`.ForeignKeyConstraint` that is being
|
||||
inspected to produce this relationship.
|
||||
|
||||
"""
|
||||
return referred_cls.__name__.lower()
|
||||
|
||||
def name_for_collection_relationship(base, local_cls, referred_cls, constraint):
|
||||
"""Return the attribute name that should be used to refer from one
|
||||
class to another, for a collection reference.
|
||||
|
||||
The default implementation is::
|
||||
|
||||
return referred_cls.__name__.lower() + "_collection"
|
||||
|
||||
Alternate implementations
|
||||
can be specified using the :paramref:`.AutomapBase.prepare.name_for_collection_relationship`
|
||||
parameter.
|
||||
|
||||
:param base: the :class:`.AutomapBase` class doing the prepare.
|
||||
|
||||
:param local_cls: the class to be mapped on the local side.
|
||||
|
||||
:param referred_cls: the class to be mapped on the referring side.
|
||||
|
||||
:param constraint: the :class:`.ForeignKeyConstraint` that is being
|
||||
inspected to produce this relationship.
|
||||
|
||||
"""
|
||||
return referred_cls.__name__.lower() + "_collection"
|
||||
|
||||
def generate_relationship(base, direction, return_fn, attrname, local_cls, referred_cls, **kw):
|
||||
"""Generate a :func:`.relationship` or :func:`.backref` on behalf of two
|
||||
mapped classes.
|
||||
|
||||
An alternate implementation of this function can be specified using the
|
||||
:paramref:`.AutomapBase.prepare.generate_relationship` parameter.
|
||||
|
||||
The default implementation of this function is as follows::
|
||||
|
||||
if return_fn is backref:
|
||||
return return_fn(attrname, **kw)
|
||||
elif return_fn is relationship:
|
||||
return return_fn(referred_cls, **kw)
|
||||
else:
|
||||
raise TypeError("Unknown relationship function: %s" % return_fn)
|
||||
|
||||
: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`.
|
||||
|
||||
:param return_fn: the function that is used by default to create the
|
||||
relationship. This will be either :func:`.relationship` or :func:`.backref`.
|
||||
The :func:`.backref` function's result will be used to produce a new
|
||||
:func:`.relationship` in a second step, so it is critical that user-defined
|
||||
implementations correctly differentiate between the two functions, if
|
||||
a custom relationship function is being used.
|
||||
|
||||
:attrname: the attribute name to which this relationship is being assigned.
|
||||
If the value of :paramref:`.generate_relationship.return_fn` is the
|
||||
:func:`.backref` function, then this name is the name that is being
|
||||
assigned to the backref.
|
||||
|
||||
:param local_cls: the "local" class to which this relationship or backref
|
||||
will be locally present.
|
||||
|
||||
:param referred_cls: the "referred" class to which the relationship or backref
|
||||
refers to.
|
||||
|
||||
:param \**kw: all additional keyword arguments are passed along to the
|
||||
function.
|
||||
|
||||
:return: a :func:`.relationship` or :func:`.backref` construct, as dictated
|
||||
by the :paramref:`.generate_relationship.return_fn` parameter.
|
||||
|
||||
"""
|
||||
if return_fn is backref:
|
||||
return return_fn(attrname, **kw)
|
||||
elif return_fn is relationship:
|
||||
return return_fn(referred_cls, **kw)
|
||||
else:
|
||||
raise TypeError("Unknown relationship function: %s" % return_fn)
|
||||
|
||||
class AutomapBase(object):
|
||||
"""Base class for an "automap" schema.
|
||||
|
||||
The :class:`.AutomapBase` class can be compared to the "declarative base"
|
||||
class that is produced by the :func:`.declarative.declarative_base`
|
||||
function. In practice, the :class:`.AutomapBase` class is always used
|
||||
as a mixin along with an actual declarative base.
|
||||
|
||||
A new subclassable :class:`.AutomapBase` is typically instantated
|
||||
using the :func:`.automap_base` function.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`automap_toplevel`
|
||||
|
||||
"""
|
||||
__abstract__ = True
|
||||
|
||||
classes = None
|
||||
"""An instance of :class:`.util.Properties` containing classes.
|
||||
|
||||
This object behaves much like the ``.c`` collection on a table. Classes
|
||||
are present under the name they were given, e.g.::
|
||||
|
||||
Base = automap_base()
|
||||
Base.prepare(engine=some_engine, reflect=True)
|
||||
|
||||
User, Address = Base.classes.User, Base.classes.Address
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def prepare(cls,
|
||||
engine=None,
|
||||
reflect=False,
|
||||
classname_for_table=classname_for_table,
|
||||
collection_class=list,
|
||||
name_for_scalar_relationship=name_for_scalar_relationship,
|
||||
name_for_collection_relationship=name_for_collection_relationship,
|
||||
generate_relationship=generate_relationship):
|
||||
|
||||
"""Extract mapped classes and relationships from the :class:`.MetaData` and
|
||||
perform mappings.
|
||||
|
||||
:param engine: an :class:`.Engine` or :class:`.Connection` with which
|
||||
to perform schema reflection, if specified.
|
||||
If the :paramref:`.AutomapBase.prepare.reflect` argument is False, this
|
||||
object is not used.
|
||||
|
||||
:param reflect: if True, the :meth:`.MetaData.reflect` method is called
|
||||
on the :class:`.MetaData` associated with this :class:`.AutomapBase`.
|
||||
The :class:`.Engine` passed via :paramref:`.AutomapBase.prepare.engine` will
|
||||
be used to perform the reflection if present; else, the :class:`.MetaData`
|
||||
should already be bound to some engine else the operation will fail.
|
||||
|
||||
:param classname_for_table: callable function which will be used to
|
||||
produce new class names, given a table name. Defaults to
|
||||
:func:`.classname_for_table`.
|
||||
|
||||
:param name_for_scalar_relationship: callable function which will be used
|
||||
to produce relationship names for scalar relationships. Defaults to
|
||||
:func:`.name_for_scalar_relationship`.
|
||||
|
||||
:param name_for_collection_relationship: callable function which will be used
|
||||
to produce relationship names for collection-oriented relationships. Defaults to
|
||||
:func:`.name_for_collection_relationship`.
|
||||
|
||||
:param generate_relationship: callable function which will be used to
|
||||
actually generate :func:`.relationship` and :func:`.backref` constructs.
|
||||
Defaults to :func:`.generate_relationship`.
|
||||
|
||||
:param collection_class: the Python collection class that will be used
|
||||
when a new :func:`.relationship` object is created that represents a
|
||||
collection. Defaults to ``list``.
|
||||
|
||||
"""
|
||||
if reflect:
|
||||
cls.metadata.reflect(
|
||||
engine,
|
||||
extend_existing=True,
|
||||
autoload_replace=False
|
||||
)
|
||||
|
||||
table_to_map_config = dict(
|
||||
(m.local_table, m)
|
||||
for m in _DeferredMapperConfig.classes_for_base(cls)
|
||||
)
|
||||
|
||||
many_to_many = []
|
||||
|
||||
for table in cls.metadata.tables.values():
|
||||
lcl_m2m, rem_m2m, m2m_const = _is_many_to_many(cls, table)
|
||||
if lcl_m2m is not None:
|
||||
many_to_many.append((lcl_m2m, rem_m2m, m2m_const, table))
|
||||
elif not table.primary_key:
|
||||
continue
|
||||
elif table not in table_to_map_config:
|
||||
mapped_cls = type(
|
||||
classname_for_table(cls, table.name, table),
|
||||
(cls, ),
|
||||
{"__table__": table}
|
||||
)
|
||||
map_config = _DeferredMapperConfig.config_for_cls(mapped_cls)
|
||||
cls.classes[map_config.cls.__name__] = mapped_cls
|
||||
table_to_map_config[table] = map_config
|
||||
|
||||
for map_config in table_to_map_config.values():
|
||||
_relationships_for_fks(cls,
|
||||
map_config,
|
||||
table_to_map_config,
|
||||
collection_class,
|
||||
name_for_scalar_relationship,
|
||||
name_for_collection_relationship,
|
||||
generate_relationship)
|
||||
|
||||
for lcl_m2m, rem_m2m, m2m_const, table in many_to_many:
|
||||
_m2m_relationship(cls, lcl_m2m, rem_m2m, m2m_const, table,
|
||||
table_to_map_config,
|
||||
collection_class,
|
||||
name_for_scalar_relationship,
|
||||
name_for_collection_relationship,
|
||||
generate_relationship)
|
||||
for map_config in table_to_map_config.values():
|
||||
map_config.map()
|
||||
|
||||
|
||||
_sa_decl_prepare = True
|
||||
"""Indicate that the mapping of classes should be deferred.
|
||||
|
||||
The presence of this attribute name indicates to declarative
|
||||
that the call to mapper() should not occur immediately; instead,
|
||||
information about the table and attributes to be mapped are gathered
|
||||
into an internal structure called _DeferredMapperConfig. These
|
||||
objects can be collected later using classes_for_base(), additional
|
||||
mapping decisions can be made, and then the map() method will actually
|
||||
apply the mapping.
|
||||
|
||||
The only real reason this deferral of the whole
|
||||
thing is needed is to support primary key columns that aren't reflected
|
||||
yet when the class is declared; everything else can theoretically be
|
||||
added to the mapper later. However, the _DeferredMapperConfig is a
|
||||
nice interface in any case which exists at that not usually exposed point
|
||||
at which declarative has the class and the Table but hasn't called
|
||||
mapper() yet.
|
||||
|
||||
"""
|
||||
|
||||
def automap_base(declarative_base=None, **kw):
|
||||
"""Produce a declarative automap base.
|
||||
|
||||
This function produces a new base class that is a product of the
|
||||
:class:`.AutomapBase` class as well a declarative base produced by
|
||||
:func:`.declarative.declarative_base`.
|
||||
|
||||
All parameters other than ``declarative_base`` are keyword arguments
|
||||
that are passed directly to the :func:`.declarative.declarative_base`
|
||||
function.
|
||||
|
||||
:param declarative_base: an existing class produced by
|
||||
:func:`.declarative.declarative_base`. When this is passed, the function
|
||||
no longer invokes :func:`.declarative.declarative_base` itself, and all other
|
||||
keyword arguments are ignored.
|
||||
|
||||
:param \**kw: keyword arguments are passed along to
|
||||
:func:`.declarative.declarative_base`.
|
||||
|
||||
"""
|
||||
if declarative_base is None:
|
||||
Base = _declarative_base(**kw)
|
||||
else:
|
||||
Base = declarative_base
|
||||
|
||||
return type(
|
||||
Base.__name__,
|
||||
(AutomapBase, Base,),
|
||||
{"__abstract__": True, "classes": util.Properties({})}
|
||||
)
|
||||
|
||||
def _is_many_to_many(automap_base, table):
|
||||
fk_constraints = [const for const in table.constraints
|
||||
if isinstance(const, ForeignKeyConstraint)]
|
||||
if len(fk_constraints) != 2:
|
||||
return None, None, None
|
||||
|
||||
cols = sum(
|
||||
[[fk.parent for fk in fk_constraint.elements]
|
||||
for fk_constraint in fk_constraints], [])
|
||||
|
||||
if set(cols) != set(table.c):
|
||||
return None, None, None
|
||||
|
||||
return (
|
||||
fk_constraints[0].elements[0].column.table,
|
||||
fk_constraints[1].elements[0].column.table,
|
||||
fk_constraints
|
||||
)
|
||||
|
||||
def _relationships_for_fks(automap_base, map_config, table_to_map_config,
|
||||
collection_class,
|
||||
name_for_scalar_relationship,
|
||||
name_for_collection_relationship,
|
||||
generate_relationship):
|
||||
local_table = map_config.local_table
|
||||
local_cls = map_config.cls
|
||||
|
||||
for constraint in local_table.constraints:
|
||||
if isinstance(constraint, ForeignKeyConstraint):
|
||||
fks = constraint.elements
|
||||
referred_table = fks[0].column.table
|
||||
referred_cfg = table_to_map_config.get(referred_table, None)
|
||||
if referred_cfg is None:
|
||||
continue
|
||||
referred_cls = referred_cfg.cls
|
||||
|
||||
relationship_name = name_for_scalar_relationship(
|
||||
automap_base,
|
||||
local_cls,
|
||||
referred_cls, constraint)
|
||||
backref_name = name_for_collection_relationship(
|
||||
automap_base,
|
||||
referred_cls,
|
||||
local_cls,
|
||||
constraint
|
||||
)
|
||||
|
||||
create_backref = backref_name not in referred_cfg.properties
|
||||
|
||||
if relationship_name not in map_config.properties:
|
||||
if create_backref:
|
||||
backref_obj = generate_relationship(automap_base,
|
||||
interfaces.ONETOMANY, backref,
|
||||
backref_name, referred_cls, local_cls,
|
||||
collection_class=collection_class)
|
||||
else:
|
||||
backref_obj = None
|
||||
map_config.properties[relationship_name] = \
|
||||
generate_relationship(automap_base,
|
||||
interfaces.MANYTOONE,
|
||||
relationship,
|
||||
relationship_name,
|
||||
local_cls, referred_cls,
|
||||
foreign_keys=[fk.parent for fk in constraint.elements],
|
||||
backref=backref_obj,
|
||||
remote_side=[fk.column for fk in constraint.elements]
|
||||
)
|
||||
if not create_backref:
|
||||
referred_cfg.properties[backref_name].back_populates = relationship_name
|
||||
elif create_backref:
|
||||
referred_cfg.properties[backref_name] = \
|
||||
generate_relationship(automap_base,
|
||||
interfaces.ONETOMANY,
|
||||
relationship,
|
||||
backref_name,
|
||||
referred_cls, local_cls,
|
||||
foreign_keys=[fk.parent for fk in constraint.elements],
|
||||
back_populates=relationship_name,
|
||||
collection_class=collection_class)
|
||||
map_config.properties[relationship_name].back_populates = backref_name
|
||||
|
||||
def _m2m_relationship(automap_base, lcl_m2m, rem_m2m, m2m_const, table,
|
||||
table_to_map_config,
|
||||
collection_class,
|
||||
name_for_scalar_relationship,
|
||||
name_for_collection_relationship,
|
||||
generate_relationship):
|
||||
|
||||
map_config = table_to_map_config.get(lcl_m2m, None)
|
||||
referred_cfg = table_to_map_config.get(rem_m2m, None)
|
||||
if map_config is None or referred_cfg is None:
|
||||
return
|
||||
|
||||
local_cls = map_config.cls
|
||||
referred_cls = referred_cfg.cls
|
||||
|
||||
relationship_name = name_for_collection_relationship(
|
||||
automap_base,
|
||||
local_cls,
|
||||
referred_cls, m2m_const[0])
|
||||
backref_name = name_for_collection_relationship(
|
||||
automap_base,
|
||||
referred_cls,
|
||||
local_cls,
|
||||
m2m_const[1]
|
||||
)
|
||||
|
||||
create_backref = backref_name not in referred_cfg.properties
|
||||
|
||||
if relationship_name not in map_config.properties:
|
||||
if create_backref:
|
||||
backref_obj = generate_relationship(automap_base,
|
||||
interfaces.MANYTOMANY,
|
||||
backref,
|
||||
backref_name,
|
||||
referred_cls, local_cls,
|
||||
collection_class=collection_class
|
||||
)
|
||||
else:
|
||||
backref_obj = None
|
||||
map_config.properties[relationship_name] = \
|
||||
generate_relationship(automap_base,
|
||||
interfaces.MANYTOMANY,
|
||||
relationship,
|
||||
relationship_name,
|
||||
local_cls, referred_cls,
|
||||
secondary=table,
|
||||
primaryjoin=and_(fk.column == fk.parent for fk in m2m_const[0].elements),
|
||||
secondaryjoin=and_(fk.column == fk.parent for fk in m2m_const[1].elements),
|
||||
backref=backref_obj,
|
||||
collection_class=collection_class
|
||||
)
|
||||
if not create_backref:
|
||||
referred_cfg.properties[backref_name].back_populates = relationship_name
|
||||
elif create_backref:
|
||||
referred_cfg.properties[backref_name] = \
|
||||
generate_relationship(automap_base,
|
||||
interfaces.MANYTOMANY,
|
||||
relationship,
|
||||
backref_name,
|
||||
referred_cls, local_cls,
|
||||
secondary=table,
|
||||
primaryjoin=and_(fk.column == fk.parent for fk in m2m_const[1].elements),
|
||||
secondaryjoin=and_(fk.column == fk.parent for fk in m2m_const[0].elements),
|
||||
back_populates=relationship_name,
|
||||
collection_class=collection_class)
|
||||
map_config.properties[relationship_name].back_populates = backref_name
|
||||
@@ -1,5 +1,5 @@
|
||||
# ext/compiler.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -9,8 +9,9 @@
|
||||
Synopsis
|
||||
========
|
||||
|
||||
Usage involves the creation of one or more :class:`~sqlalchemy.sql.expression.ClauseElement`
|
||||
subclasses and one or more callables defining its compilation::
|
||||
Usage involves the creation of one or more
|
||||
:class:`~sqlalchemy.sql.expression.ClauseElement` subclasses and one or
|
||||
more callables defining its compilation::
|
||||
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.sql.expression import ColumnClause
|
||||
@@ -58,16 +59,18 @@ invoked for the dialect in use::
|
||||
def visit_alter_column(element, compiler, **kw):
|
||||
return "ALTER TABLE %s ALTER COLUMN %s ..." % (element.table.name, element.column.name)
|
||||
|
||||
The second ``visit_alter_table`` will be invoked when any ``postgresql`` dialect is used.
|
||||
The second ``visit_alter_table`` will be invoked when any ``postgresql``
|
||||
dialect is used.
|
||||
|
||||
Compiling sub-elements of a custom expression construct
|
||||
=======================================================
|
||||
|
||||
The ``compiler`` argument is the :class:`~sqlalchemy.engine.base.Compiled`
|
||||
object in use. This object can be inspected for any information about the
|
||||
in-progress compilation, including ``compiler.dialect``,
|
||||
``compiler.statement`` etc. The :class:`~sqlalchemy.sql.compiler.SQLCompiler`
|
||||
and :class:`~sqlalchemy.sql.compiler.DDLCompiler` both include a ``process()``
|
||||
The ``compiler`` argument is the
|
||||
:class:`~sqlalchemy.engine.interfaces.Compiled` object in use. This object
|
||||
can be inspected for any information about the in-progress compilation,
|
||||
including ``compiler.dialect``, ``compiler.statement`` etc. The
|
||||
:class:`~sqlalchemy.sql.compiler.SQLCompiler` and
|
||||
:class:`~sqlalchemy.sql.compiler.DDLCompiler` both include a ``process()``
|
||||
method which can be used for compilation of embedded attributes::
|
||||
|
||||
from sqlalchemy.sql.expression import Executable, ClauseElement
|
||||
@@ -91,6 +94,12 @@ Produces::
|
||||
|
||||
"INSERT INTO mytable (SELECT mytable.x, mytable.y, mytable.z FROM mytable WHERE mytable.x > :x_1)"
|
||||
|
||||
.. note::
|
||||
|
||||
The above ``InsertFromSelect`` construct is only an example, this actual
|
||||
functionality is already available using the
|
||||
:meth:`.Insert.from_select` method.
|
||||
|
||||
.. note::
|
||||
|
||||
The above ``InsertFromSelect`` construct probably wants to have "autocommit"
|
||||
@@ -99,10 +108,11 @@ Produces::
|
||||
Cross Compiling between SQL and DDL compilers
|
||||
---------------------------------------------
|
||||
|
||||
SQL and DDL constructs are each compiled using different base compilers - ``SQLCompiler``
|
||||
and ``DDLCompiler``. A common need is to access the compilation rules of SQL expressions
|
||||
from within a DDL expression. The ``DDLCompiler`` includes an accessor ``sql_compiler`` for this reason, such as below where we generate a CHECK
|
||||
constraint that embeds a SQL expression::
|
||||
SQL and DDL constructs are each compiled using different base compilers -
|
||||
``SQLCompiler`` and ``DDLCompiler``. A common need is to access the
|
||||
compilation rules of SQL expressions from within a DDL expression. The
|
||||
``DDLCompiler`` includes an accessor ``sql_compiler`` for this reason, such as
|
||||
below where we generate a CHECK constraint that embeds a SQL expression::
|
||||
|
||||
@compiles(MyConstraint)
|
||||
def compile_my_constraint(constraint, ddlcompiler, **kw):
|
||||
@@ -116,20 +126,22 @@ constraint that embeds a SQL expression::
|
||||
Enabling Autocommit on a Construct
|
||||
==================================
|
||||
|
||||
Recall from the section :ref:`autocommit` that the :class:`.Engine`, when asked to execute
|
||||
a construct in the absence of a user-defined transaction, detects if the given
|
||||
construct represents DML or DDL, that is, a data modification or data definition statement, which
|
||||
requires (or may require, in the case of DDL) that the transaction generated by the DBAPI be committed
|
||||
(recall that DBAPI always has a transaction going on regardless of what SQLAlchemy does). Checking
|
||||
for this is actually accomplished
|
||||
by checking for the "autocommit" execution option on the construct. When building a construct like
|
||||
an INSERT derivation, a new DDL type, or perhaps a stored procedure that alters data, the "autocommit"
|
||||
option needs to be set in order for the statement to function with "connectionless" execution
|
||||
Recall from the section :ref:`autocommit` that the :class:`.Engine`, when
|
||||
asked to execute a construct in the absence of a user-defined transaction,
|
||||
detects if the given construct represents DML or DDL, that is, a data
|
||||
modification or data definition statement, which requires (or may require,
|
||||
in the case of DDL) that the transaction generated by the DBAPI be committed
|
||||
(recall that DBAPI always has a transaction going on regardless of what
|
||||
SQLAlchemy does). Checking for this is actually accomplished by checking for
|
||||
the "autocommit" execution option on the construct. When building a
|
||||
construct like an INSERT derivation, a new DDL type, or perhaps a stored
|
||||
procedure that alters data, the "autocommit" option needs to be set in order
|
||||
for the statement to function with "connectionless" execution
|
||||
(as described in :ref:`dbengine_implicit`).
|
||||
|
||||
Currently a quick way to do this is to subclass :class:`.Executable`, then add the "autocommit" flag
|
||||
to the ``_execution_options`` dictionary (note this is a "frozen" dictionary which supplies a generative
|
||||
``union()`` method)::
|
||||
Currently a quick way to do this is to subclass :class:`.Executable`, then
|
||||
add the "autocommit" flag to the ``_execution_options`` dictionary (note this
|
||||
is a "frozen" dictionary which supplies a generative ``union()`` method)::
|
||||
|
||||
from sqlalchemy.sql.expression import Executable, ClauseElement
|
||||
|
||||
@@ -137,8 +149,9 @@ to the ``_execution_options`` dictionary (note this is a "frozen" dictionary whi
|
||||
_execution_options = \\
|
||||
Executable._execution_options.union({'autocommit': True})
|
||||
|
||||
More succinctly, if the construct is truly similar to an INSERT, UPDATE, or DELETE, :class:`.UpdateBase`
|
||||
can be used, which already is a subclass of :class:`.Executable`, :class:`.ClauseElement` and includes the
|
||||
More succinctly, if the construct is truly similar to an INSERT, UPDATE, or
|
||||
DELETE, :class:`.UpdateBase` can be used, which already is a subclass
|
||||
of :class:`.Executable`, :class:`.ClauseElement` and includes the
|
||||
``autocommit`` flag::
|
||||
|
||||
from sqlalchemy.sql.expression import UpdateBase
|
||||
@@ -150,7 +163,8 @@ can be used, which already is a subclass of :class:`.Executable`, :class:`.Claus
|
||||
|
||||
|
||||
|
||||
DDL elements that subclass :class:`.DDLElement` already have the "autocommit" flag turned on.
|
||||
DDL elements that subclass :class:`.DDLElement` already have the
|
||||
"autocommit" flag turned on.
|
||||
|
||||
|
||||
|
||||
@@ -158,13 +172,16 @@ DDL elements that subclass :class:`.DDLElement` already have the "autocommit" fl
|
||||
Changing the default compilation of existing constructs
|
||||
=======================================================
|
||||
|
||||
The compiler extension applies just as well to the existing constructs. When overriding
|
||||
the compilation of a built in SQL construct, the @compiles decorator is invoked upon
|
||||
the appropriate class (be sure to use the class, i.e. ``Insert`` or ``Select``, instead of the creation function such as ``insert()`` or ``select()``).
|
||||
The compiler extension applies just as well to the existing constructs. When
|
||||
overriding the compilation of a built in SQL construct, the @compiles
|
||||
decorator is invoked upon the appropriate class (be sure to use the class,
|
||||
i.e. ``Insert`` or ``Select``, instead of the creation function such
|
||||
as ``insert()`` or ``select()``).
|
||||
|
||||
Within the new compilation function, to get at the "original" compilation routine,
|
||||
use the appropriate visit_XXX method - this because compiler.process() will call upon the
|
||||
overriding routine and cause an endless loop. Such as, to add "prefix" to all insert statements::
|
||||
Within the new compilation function, to get at the "original" compilation
|
||||
routine, use the appropriate visit_XXX method - this
|
||||
because compiler.process() will call upon the overriding routine and cause
|
||||
an endless loop. Such as, to add "prefix" to all insert statements::
|
||||
|
||||
from sqlalchemy.sql.expression import Insert
|
||||
|
||||
@@ -172,14 +189,16 @@ overriding routine and cause an endless loop. Such as, to add "prefix" to all
|
||||
def prefix_inserts(insert, compiler, **kw):
|
||||
return compiler.visit_insert(insert.prefix_with("some prefix"), **kw)
|
||||
|
||||
The above compiler will prefix all INSERT statements with "some prefix" when compiled.
|
||||
The above compiler will prefix all INSERT statements with "some prefix" when
|
||||
compiled.
|
||||
|
||||
.. _type_compilation_extension:
|
||||
|
||||
Changing Compilation of Types
|
||||
=============================
|
||||
|
||||
``compiler`` works for types, too, such as below where we implement the MS-SQL specific 'max' keyword for ``String``/``VARCHAR``::
|
||||
``compiler`` works for types, too, such as below where we implement the
|
||||
MS-SQL specific 'max' keyword for ``String``/``VARCHAR``::
|
||||
|
||||
@compiles(String, 'mssql')
|
||||
@compiles(VARCHAR, 'mssql')
|
||||
@@ -219,7 +238,7 @@ A synopsis is as follows:
|
||||
class timestamp(ColumnElement):
|
||||
type = TIMESTAMP()
|
||||
|
||||
* :class:`~sqlalchemy.sql.expression.FunctionElement` - This is a hybrid of a
|
||||
* :class:`~sqlalchemy.sql.functions.FunctionElement` - This is a hybrid of a
|
||||
``ColumnElement`` and a "from clause" like object, and represents a SQL
|
||||
function or stored procedure type of call. Since most databases support
|
||||
statements along the line of "SELECT FROM <some function>"
|
||||
@@ -248,10 +267,10 @@ A synopsis is as follows:
|
||||
``execute_at()`` method, allowing the construct to be invoked during CREATE
|
||||
TABLE and DROP TABLE sequences.
|
||||
|
||||
* :class:`~sqlalchemy.sql.expression.Executable` - This is a mixin which should be
|
||||
used with any expression class that represents a "standalone" SQL statement that
|
||||
can be passed directly to an ``execute()`` method. It is already implicit
|
||||
within ``DDLElement`` and ``FunctionElement``.
|
||||
* :class:`~sqlalchemy.sql.expression.Executable` - This is a mixin which
|
||||
should be used with any expression class that represents a "standalone"
|
||||
SQL statement that can be passed directly to an ``execute()`` method. It
|
||||
is already implicit within ``DDLElement`` and ``FunctionElement``.
|
||||
|
||||
Further Examples
|
||||
================
|
||||
@@ -259,12 +278,13 @@ Further Examples
|
||||
"UTC timestamp" function
|
||||
-------------------------
|
||||
|
||||
A function that works like "CURRENT_TIMESTAMP" except applies the appropriate conversions
|
||||
so that the time is in UTC time. Timestamps are best stored in relational databases
|
||||
as UTC, without time zones. UTC so that your database doesn't think time has gone
|
||||
backwards in the hour when daylight savings ends, without timezones because timezones
|
||||
are like character encodings - they're best applied only at the endpoints of an
|
||||
application (i.e. convert to UTC upon user input, re-apply desired timezone upon display).
|
||||
A function that works like "CURRENT_TIMESTAMP" except applies the
|
||||
appropriate conversions so that the time is in UTC time. Timestamps are best
|
||||
stored in relational databases as UTC, without time zones. UTC so that your
|
||||
database doesn't think time has gone backwards in the hour when daylight
|
||||
savings ends, without timezones because timezones are like character
|
||||
encodings - they're best applied only at the endpoints of an application
|
||||
(i.e. convert to UTC upon user input, re-apply desired timezone upon display).
|
||||
|
||||
For Postgresql and Microsoft SQL Server::
|
||||
|
||||
@@ -298,10 +318,10 @@ Example usage::
|
||||
"GREATEST" function
|
||||
-------------------
|
||||
|
||||
The "GREATEST" function is given any number of arguments and returns the one that is
|
||||
of the highest value - it's equivalent to Python's ``max`` function. A SQL
|
||||
standard version versus a CASE based version which only accommodates two
|
||||
arguments::
|
||||
The "GREATEST" function is given any number of arguments and returns the one
|
||||
that is of the highest value - it's equivalent to Python's ``max``
|
||||
function. A SQL standard version versus a CASE based version which only
|
||||
accommodates two arguments::
|
||||
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
@@ -339,7 +359,8 @@ Example usage::
|
||||
"false" expression
|
||||
------------------
|
||||
|
||||
Render a "false" constant expression, rendering as "0" on platforms that don't have a "false" constant::
|
||||
Render a "false" constant expression, rendering as "0" on platforms that
|
||||
don't have a "false" constant::
|
||||
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
@@ -367,9 +388,14 @@ Example usage::
|
||||
)
|
||||
|
||||
"""
|
||||
from sqlalchemy import exc
|
||||
from .. import exc
|
||||
from ..sql import visitors
|
||||
|
||||
|
||||
def compiles(class_, *specs):
|
||||
"""Register a function as a compiler for a
|
||||
given :class:`.ClauseElement` type."""
|
||||
|
||||
def decorate(fn):
|
||||
existing = class_.__dict__.get('_compiler_dispatcher', None)
|
||||
existing_dispatch = class_.__dict__.get('_compiler_dispatch')
|
||||
@@ -380,7 +406,8 @@ def compiles(class_, *specs):
|
||||
existing.specs['default'] = existing_dispatch
|
||||
|
||||
# TODO: why is the lambda needed ?
|
||||
setattr(class_, '_compiler_dispatch', lambda *arg, **kw: existing(*arg, **kw))
|
||||
setattr(class_, '_compiler_dispatch',
|
||||
lambda *arg, **kw: existing(*arg, **kw))
|
||||
setattr(class_, '_compiler_dispatcher', existing)
|
||||
|
||||
if specs:
|
||||
@@ -392,6 +419,18 @@ def compiles(class_, *specs):
|
||||
return fn
|
||||
return decorate
|
||||
|
||||
|
||||
def deregister(class_):
|
||||
"""Remove all custom compilers associated with a given
|
||||
:class:`.ClauseElement` type."""
|
||||
|
||||
if hasattr(class_, '_compiler_dispatcher'):
|
||||
# regenerate default _compiler_dispatch
|
||||
visitors._generate_dispatch(class_)
|
||||
# remove custom directive
|
||||
del class_._compiler_dispatcher
|
||||
|
||||
|
||||
class _dispatcher(object):
|
||||
def __init__(self):
|
||||
self.specs = {}
|
||||
@@ -407,4 +446,3 @@ class _dispatcher(object):
|
||||
"%s construct has no default "
|
||||
"compilation handler." % type(element))
|
||||
return fn(element, compiler, **kw)
|
||||
|
||||
|
||||
Executable → Regular
+348
-800
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,512 @@
|
||||
# ext/declarative/api.py
|
||||
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
"""Public API functions and helpers for declarative."""
|
||||
|
||||
|
||||
from ...schema import Table, MetaData
|
||||
from ...orm import synonym as _orm_synonym, mapper,\
|
||||
comparable_property,\
|
||||
interfaces, properties
|
||||
from ...orm.util import polymorphic_union
|
||||
from ...orm.base import _mapper_or_none
|
||||
from ...util import compat
|
||||
from ... import exc
|
||||
import weakref
|
||||
|
||||
from .base import _as_declarative, \
|
||||
_declarative_constructor,\
|
||||
_DeferredMapperConfig, _add_attribute
|
||||
from .clsregistry import _class_resolver
|
||||
|
||||
|
||||
def instrument_declarative(cls, registry, metadata):
|
||||
"""Given a class, configure the class declaratively,
|
||||
using the given registry, which can be any dictionary, and
|
||||
MetaData object.
|
||||
|
||||
"""
|
||||
if '_decl_class_registry' in cls.__dict__:
|
||||
raise exc.InvalidRequestError(
|
||||
"Class %r already has been "
|
||||
"instrumented declaratively" % cls)
|
||||
cls._decl_class_registry = registry
|
||||
cls.metadata = metadata
|
||||
_as_declarative(cls, cls.__name__, cls.__dict__)
|
||||
|
||||
|
||||
def has_inherited_table(cls):
|
||||
"""Given a class, return True if any of the classes it inherits from has a
|
||||
mapped table, otherwise return False.
|
||||
"""
|
||||
for class_ in cls.__mro__[1:]:
|
||||
if getattr(class_, '__table__', None) is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class DeclarativeMeta(type):
|
||||
def __init__(cls, classname, bases, dict_):
|
||||
if '_decl_class_registry' not in cls.__dict__:
|
||||
_as_declarative(cls, classname, cls.__dict__)
|
||||
type.__init__(cls, classname, bases, dict_)
|
||||
|
||||
def __setattr__(cls, key, value):
|
||||
_add_attribute(cls, key, value)
|
||||
|
||||
|
||||
def synonym_for(name, map_column=False):
|
||||
"""Decorator, make a Python @property a query synonym for a column.
|
||||
|
||||
A decorator version of :func:`~sqlalchemy.orm.synonym`. The function being
|
||||
decorated is the 'descriptor', otherwise passes its arguments through to
|
||||
synonym()::
|
||||
|
||||
@synonym_for('col')
|
||||
@property
|
||||
def prop(self):
|
||||
return 'special sauce'
|
||||
|
||||
The regular ``synonym()`` is also usable directly in a declarative setting
|
||||
and may be convenient for read/write properties::
|
||||
|
||||
prop = synonym('col', descriptor=property(_read_prop, _write_prop))
|
||||
|
||||
"""
|
||||
def decorate(fn):
|
||||
return _orm_synonym(name, map_column=map_column, descriptor=fn)
|
||||
return decorate
|
||||
|
||||
|
||||
def comparable_using(comparator_factory):
|
||||
"""Decorator, allow a Python @property to be used in query criteria.
|
||||
|
||||
This is a decorator front end to
|
||||
:func:`~sqlalchemy.orm.comparable_property` that passes
|
||||
through the comparator_factory and the function being decorated::
|
||||
|
||||
@comparable_using(MyComparatorType)
|
||||
@property
|
||||
def prop(self):
|
||||
return 'special sauce'
|
||||
|
||||
The regular ``comparable_property()`` is also usable directly in a
|
||||
declarative setting and may be convenient for read/write properties::
|
||||
|
||||
prop = comparable_property(MyComparatorType)
|
||||
|
||||
"""
|
||||
def decorate(fn):
|
||||
return comparable_property(comparator_factory, fn)
|
||||
return decorate
|
||||
|
||||
|
||||
class declared_attr(interfaces._MappedAttribute, property):
|
||||
"""Mark a class-level method as representing the definition of
|
||||
a mapped property or special declarative member name.
|
||||
|
||||
@declared_attr turns the attribute into a scalar-like
|
||||
property that can be invoked from the uninstantiated class.
|
||||
Declarative treats attributes specifically marked with
|
||||
@declared_attr as returning a construct that is specific
|
||||
to mapping or declarative table configuration. The name
|
||||
of the attribute is that of what the non-dynamic version
|
||||
of the attribute would be.
|
||||
|
||||
@declared_attr is more often than not applicable to mixins,
|
||||
to define relationships that are to be applied to different
|
||||
implementors of the class::
|
||||
|
||||
class ProvidesUser(object):
|
||||
"A mixin that adds a 'user' relationship to classes."
|
||||
|
||||
@declared_attr
|
||||
def user(self):
|
||||
return relationship("User")
|
||||
|
||||
It also can be applied to mapped classes, such as to provide
|
||||
a "polymorphic" scheme for inheritance::
|
||||
|
||||
class Employee(Base):
|
||||
id = Column(Integer, primary_key=True)
|
||||
type = Column(String(50), nullable=False)
|
||||
|
||||
@declared_attr
|
||||
def __tablename__(cls):
|
||||
return cls.__name__.lower()
|
||||
|
||||
@declared_attr
|
||||
def __mapper_args__(cls):
|
||||
if cls.__name__ == 'Employee':
|
||||
return {
|
||||
"polymorphic_on":cls.type,
|
||||
"polymorphic_identity":"Employee"
|
||||
}
|
||||
else:
|
||||
return {"polymorphic_identity":cls.__name__}
|
||||
|
||||
.. versionchanged:: 0.8 :class:`.declared_attr` can be used with
|
||||
non-ORM or extension attributes, such as user-defined attributes
|
||||
or :func:`.association_proxy` objects, which will be assigned
|
||||
to the class at class construction time.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, fget, *arg, **kw):
|
||||
super(declared_attr, self).__init__(fget, *arg, **kw)
|
||||
self.__doc__ = fget.__doc__
|
||||
|
||||
def __get__(desc, self, cls):
|
||||
return desc.fget(cls)
|
||||
|
||||
|
||||
def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
|
||||
name='Base', constructor=_declarative_constructor,
|
||||
class_registry=None,
|
||||
metaclass=DeclarativeMeta):
|
||||
"""Construct a base class for declarative class definitions.
|
||||
|
||||
The new base class will be given a metaclass that produces
|
||||
appropriate :class:`~sqlalchemy.schema.Table` objects and makes
|
||||
the appropriate :func:`~sqlalchemy.orm.mapper` calls based on the
|
||||
information provided declaratively in the class and any subclasses
|
||||
of the class.
|
||||
|
||||
:param bind: An optional
|
||||
:class:`~sqlalchemy.engine.Connectable`, will be assigned
|
||||
the ``bind`` attribute on the :class:`~sqlalchemy.schema.MetaData`
|
||||
instance.
|
||||
|
||||
:param metadata:
|
||||
An optional :class:`~sqlalchemy.schema.MetaData` instance. All
|
||||
:class:`~sqlalchemy.schema.Table` objects implicitly declared by
|
||||
subclasses of the base will share this MetaData. A MetaData instance
|
||||
will be created if none is provided. The
|
||||
:class:`~sqlalchemy.schema.MetaData` instance will be available via the
|
||||
`metadata` attribute of the generated declarative base class.
|
||||
|
||||
:param mapper:
|
||||
An optional callable, defaults to :func:`~sqlalchemy.orm.mapper`. Will
|
||||
be used to map subclasses to their Tables.
|
||||
|
||||
:param cls:
|
||||
Defaults to :class:`object`. A type to use as the base for the generated
|
||||
declarative base class. May be a class or tuple of classes.
|
||||
|
||||
:param name:
|
||||
Defaults to ``Base``. The display name for the generated
|
||||
class. Customizing this is not required, but can improve clarity in
|
||||
tracebacks and debugging.
|
||||
|
||||
:param constructor:
|
||||
Defaults to
|
||||
:func:`~sqlalchemy.ext.declarative._declarative_constructor`, an
|
||||
__init__ implementation that assigns \**kwargs for declared
|
||||
fields and relationships to an instance. If ``None`` is supplied,
|
||||
no __init__ will be provided and construction will fall back to
|
||||
cls.__init__ by way of the normal Python semantics.
|
||||
|
||||
:param class_registry: optional dictionary that will serve as the
|
||||
registry of class names-> mapped classes when string names
|
||||
are used to identify classes inside of :func:`.relationship`
|
||||
and others. Allows two or more declarative base classes
|
||||
to share the same registry of class names for simplified
|
||||
inter-base relationships.
|
||||
|
||||
:param metaclass:
|
||||
Defaults to :class:`.DeclarativeMeta`. A metaclass or __metaclass__
|
||||
compatible callable to use as the meta type of the generated
|
||||
declarative base class.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:func:`.as_declarative`
|
||||
|
||||
"""
|
||||
lcl_metadata = metadata or MetaData()
|
||||
if bind:
|
||||
lcl_metadata.bind = bind
|
||||
|
||||
if class_registry is None:
|
||||
class_registry = weakref.WeakValueDictionary()
|
||||
|
||||
bases = not isinstance(cls, tuple) and (cls,) or cls
|
||||
class_dict = dict(_decl_class_registry=class_registry,
|
||||
metadata=lcl_metadata)
|
||||
|
||||
if constructor:
|
||||
class_dict['__init__'] = constructor
|
||||
if mapper:
|
||||
class_dict['__mapper_cls__'] = mapper
|
||||
|
||||
return metaclass(name, bases, class_dict)
|
||||
|
||||
def as_declarative(**kw):
|
||||
"""
|
||||
Class decorator for :func:`.declarative_base`.
|
||||
|
||||
Provides a syntactical shortcut to the ``cls`` argument
|
||||
sent to :func:`.declarative_base`, allowing the base class
|
||||
to be converted in-place to a "declarative" base::
|
||||
|
||||
from sqlalchemy.ext.declarative import as_declarative
|
||||
|
||||
@as_declarative()
|
||||
class Base(object):
|
||||
@declared_attr
|
||||
def __tablename__(cls):
|
||||
return cls.__name__.lower()
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
class MyMappedClass(Base):
|
||||
# ...
|
||||
|
||||
All keyword arguments passed to :func:`.as_declarative` are passed
|
||||
along to :func:`.declarative_base`.
|
||||
|
||||
.. versionadded:: 0.8.3
|
||||
|
||||
.. seealso::
|
||||
|
||||
:func:`.declarative_base`
|
||||
|
||||
"""
|
||||
def decorate(cls):
|
||||
kw['cls'] = cls
|
||||
kw['name'] = cls.__name__
|
||||
return declarative_base(**kw)
|
||||
|
||||
return decorate
|
||||
|
||||
class ConcreteBase(object):
|
||||
"""A helper class for 'concrete' declarative mappings.
|
||||
|
||||
:class:`.ConcreteBase` will use the :func:`.polymorphic_union`
|
||||
function automatically, against all tables mapped as a subclass
|
||||
to this class. The function is called via the
|
||||
``__declare_last__()`` function, which is essentially
|
||||
a hook for the :meth:`.after_configured` event.
|
||||
|
||||
:class:`.ConcreteBase` produces a mapped
|
||||
table for the class itself. Compare to :class:`.AbstractConcreteBase`,
|
||||
which does not.
|
||||
|
||||
Example::
|
||||
|
||||
from sqlalchemy.ext.declarative import ConcreteBase
|
||||
|
||||
class Employee(ConcreteBase, Base):
|
||||
__tablename__ = 'employee'
|
||||
employee_id = Column(Integer, primary_key=True)
|
||||
name = Column(String(50))
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity':'employee',
|
||||
'concrete':True}
|
||||
|
||||
class Manager(Employee):
|
||||
__tablename__ = 'manager'
|
||||
employee_id = Column(Integer, primary_key=True)
|
||||
name = Column(String(50))
|
||||
manager_data = Column(String(40))
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity':'manager',
|
||||
'concrete':True}
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _create_polymorphic_union(cls, mappers):
|
||||
return polymorphic_union(dict(
|
||||
(mp.polymorphic_identity, mp.local_table)
|
||||
for mp in mappers
|
||||
), 'type', 'pjoin')
|
||||
|
||||
@classmethod
|
||||
def __declare_last__(cls):
|
||||
m = cls.__mapper__
|
||||
if m.with_polymorphic:
|
||||
return
|
||||
|
||||
mappers = list(m.self_and_descendants)
|
||||
pjoin = cls._create_polymorphic_union(mappers)
|
||||
m._set_with_polymorphic(("*", pjoin))
|
||||
m._set_polymorphic_on(pjoin.c.type)
|
||||
|
||||
|
||||
class AbstractConcreteBase(ConcreteBase):
|
||||
"""A helper class for 'concrete' declarative mappings.
|
||||
|
||||
:class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union`
|
||||
function automatically, against all tables mapped as a subclass
|
||||
to this class. The function is called via the
|
||||
``__declare_last__()`` function, which is essentially
|
||||
a hook for the :meth:`.after_configured` event.
|
||||
|
||||
:class:`.AbstractConcreteBase` does not produce a mapped
|
||||
table for the class itself. Compare to :class:`.ConcreteBase`,
|
||||
which does.
|
||||
|
||||
Example::
|
||||
|
||||
from sqlalchemy.ext.declarative import AbstractConcreteBase
|
||||
|
||||
class Employee(AbstractConcreteBase, Base):
|
||||
pass
|
||||
|
||||
class Manager(Employee):
|
||||
__tablename__ = 'manager'
|
||||
employee_id = Column(Integer, primary_key=True)
|
||||
name = Column(String(50))
|
||||
manager_data = Column(String(40))
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity':'manager',
|
||||
'concrete':True}
|
||||
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
@classmethod
|
||||
def __declare_last__(cls):
|
||||
if hasattr(cls, '__mapper__'):
|
||||
return
|
||||
|
||||
# can't rely on 'self_and_descendants' here
|
||||
# since technically an immediate subclass
|
||||
# might not be mapped, but a subclass
|
||||
# may be.
|
||||
mappers = []
|
||||
stack = list(cls.__subclasses__())
|
||||
while stack:
|
||||
klass = stack.pop()
|
||||
stack.extend(klass.__subclasses__())
|
||||
mn = _mapper_or_none(klass)
|
||||
if mn is not None:
|
||||
mappers.append(mn)
|
||||
pjoin = cls._create_polymorphic_union(mappers)
|
||||
cls.__mapper__ = m = mapper(cls, pjoin, polymorphic_on=pjoin.c.type)
|
||||
|
||||
for scls in cls.__subclasses__():
|
||||
sm = _mapper_or_none(scls)
|
||||
if sm.concrete and cls in scls.__bases__:
|
||||
sm._set_concrete_base(m)
|
||||
|
||||
|
||||
class DeferredReflection(object):
|
||||
"""A helper class for construction of mappings based on
|
||||
a deferred reflection step.
|
||||
|
||||
Normally, declarative can be used with reflection by
|
||||
setting a :class:`.Table` object using autoload=True
|
||||
as the ``__table__`` attribute on a declarative class.
|
||||
The caveat is that the :class:`.Table` must be fully
|
||||
reflected, or at the very least have a primary key column,
|
||||
at the point at which a normal declarative mapping is
|
||||
constructed, meaning the :class:`.Engine` must be available
|
||||
at class declaration time.
|
||||
|
||||
The :class:`.DeferredReflection` mixin moves the construction
|
||||
of mappers to be at a later point, after a specific
|
||||
method is called which first reflects all :class:`.Table`
|
||||
objects created so far. Classes can define it as such::
|
||||
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.ext.declarative import DeferredReflection
|
||||
Base = declarative_base()
|
||||
|
||||
class MyClass(DeferredReflection, Base):
|
||||
__tablename__ = 'mytable'
|
||||
|
||||
Above, ``MyClass`` is not yet mapped. After a series of
|
||||
classes have been defined in the above fashion, all tables
|
||||
can be reflected and mappings created using
|
||||
:meth:`.prepare`::
|
||||
|
||||
engine = create_engine("someengine://...")
|
||||
DeferredReflection.prepare(engine)
|
||||
|
||||
The :class:`.DeferredReflection` mixin can be applied to individual
|
||||
classes, used as the base for the declarative base itself,
|
||||
or used in a custom abstract class. Using an abstract base
|
||||
allows that only a subset of classes to be prepared for a
|
||||
particular prepare step, which is necessary for applications
|
||||
that use more than one engine. For example, if an application
|
||||
has two engines, you might use two bases, and prepare each
|
||||
separately, e.g.::
|
||||
|
||||
class ReflectedOne(DeferredReflection, Base):
|
||||
__abstract__ = True
|
||||
|
||||
class ReflectedTwo(DeferredReflection, Base):
|
||||
__abstract__ = True
|
||||
|
||||
class MyClass(ReflectedOne):
|
||||
__tablename__ = 'mytable'
|
||||
|
||||
class MyOtherClass(ReflectedOne):
|
||||
__tablename__ = 'myothertable'
|
||||
|
||||
class YetAnotherClass(ReflectedTwo):
|
||||
__tablename__ = 'yetanothertable'
|
||||
|
||||
# ... etc.
|
||||
|
||||
Above, the class hierarchies for ``ReflectedOne`` and
|
||||
``ReflectedTwo`` can be configured separately::
|
||||
|
||||
ReflectedOne.prepare(engine_one)
|
||||
ReflectedTwo.prepare(engine_two)
|
||||
|
||||
.. versionadded:: 0.8
|
||||
|
||||
"""
|
||||
@classmethod
|
||||
def prepare(cls, engine):
|
||||
"""Reflect all :class:`.Table` objects for all current
|
||||
:class:`.DeferredReflection` subclasses"""
|
||||
|
||||
to_map = _DeferredMapperConfig.classes_for_base(cls)
|
||||
for thingy in to_map:
|
||||
cls._sa_decl_prepare(thingy.local_table, engine)
|
||||
thingy.map()
|
||||
mapper = thingy.cls.__mapper__
|
||||
metadata = mapper.class_.metadata
|
||||
for rel in mapper._props.values():
|
||||
if isinstance(rel, properties.RelationshipProperty) and \
|
||||
rel.secondary is not None:
|
||||
if isinstance(rel.secondary, Table):
|
||||
cls._reflect_table(rel.secondary, engine)
|
||||
elif isinstance(rel.secondary, _class_resolver):
|
||||
rel.secondary._resolvers += (
|
||||
cls._sa_deferred_table_resolver(engine, metadata),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _sa_deferred_table_resolver(cls, engine, metadata):
|
||||
def _resolve(key):
|
||||
t1 = Table(key, metadata)
|
||||
cls._reflect_table(t1, engine)
|
||||
return t1
|
||||
return _resolve
|
||||
|
||||
@classmethod
|
||||
def _sa_decl_prepare(cls, local_table, engine):
|
||||
# autoload Table, which is already
|
||||
# present in the metadata. This
|
||||
# will fill in db-loaded columns
|
||||
# into the existing Table object.
|
||||
if local_table is not None:
|
||||
cls._reflect_table(local_table, engine)
|
||||
|
||||
@classmethod
|
||||
def _reflect_table(cls, table, engine):
|
||||
Table(table.name,
|
||||
table.metadata,
|
||||
extend_existing=True,
|
||||
autoload_replace=False,
|
||||
autoload=True,
|
||||
autoload_with=engine,
|
||||
schema=table.schema)
|
||||
@@ -0,0 +1,506 @@
|
||||
# ext/declarative/base.py
|
||||
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
"""Internal implementation for declarative."""
|
||||
|
||||
from ...schema import Table, Column
|
||||
from ...orm import mapper, class_mapper, synonym
|
||||
from ...orm.interfaces import MapperProperty
|
||||
from ...orm.properties import ColumnProperty, CompositeProperty
|
||||
from ...orm.attributes import QueryableAttribute
|
||||
from ...orm.base import _is_mapped_class
|
||||
from ... import util, exc
|
||||
from ...sql import expression
|
||||
from ... import event
|
||||
from . import clsregistry
|
||||
import collections
|
||||
import weakref
|
||||
|
||||
def _declared_mapping_info(cls):
|
||||
# deferred mapping
|
||||
if _DeferredMapperConfig.has_cls(cls):
|
||||
return _DeferredMapperConfig.config_for_cls(cls)
|
||||
# regular mapping
|
||||
elif _is_mapped_class(cls):
|
||||
return class_mapper(cls, configure=False)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _as_declarative(cls, classname, dict_):
|
||||
from .api import declared_attr
|
||||
|
||||
# dict_ will be a dictproxy, which we can't write to, and we need to!
|
||||
dict_ = dict(dict_)
|
||||
|
||||
column_copies = {}
|
||||
potential_columns = {}
|
||||
|
||||
mapper_args_fn = None
|
||||
table_args = inherited_table_args = None
|
||||
tablename = None
|
||||
|
||||
declarative_props = (declared_attr, util.classproperty)
|
||||
|
||||
for base in cls.__mro__:
|
||||
_is_declarative_inherits = hasattr(base, '_decl_class_registry')
|
||||
|
||||
if '__declare_last__' in base.__dict__:
|
||||
@event.listens_for(mapper, "after_configured")
|
||||
def go():
|
||||
cls.__declare_last__()
|
||||
if '__abstract__' in base.__dict__:
|
||||
if (base is cls or
|
||||
(base in cls.__bases__ and not _is_declarative_inherits)
|
||||
):
|
||||
return
|
||||
|
||||
class_mapped = _declared_mapping_info(base) is not None
|
||||
|
||||
for name, obj in vars(base).items():
|
||||
if name == '__mapper_args__':
|
||||
if not mapper_args_fn and (
|
||||
not class_mapped or
|
||||
isinstance(obj, declarative_props)
|
||||
):
|
||||
# don't even invoke __mapper_args__ until
|
||||
# after we've determined everything about the
|
||||
# mapped table.
|
||||
mapper_args_fn = lambda: cls.__mapper_args__
|
||||
elif name == '__tablename__':
|
||||
if not tablename and (
|
||||
not class_mapped or
|
||||
isinstance(obj, declarative_props)
|
||||
):
|
||||
tablename = cls.__tablename__
|
||||
elif name == '__table_args__':
|
||||
if not table_args and (
|
||||
not class_mapped or
|
||||
isinstance(obj, declarative_props)
|
||||
):
|
||||
table_args = cls.__table_args__
|
||||
if not isinstance(table_args, (tuple, dict, type(None))):
|
||||
raise exc.ArgumentError(
|
||||
"__table_args__ value must be a tuple, "
|
||||
"dict, or None")
|
||||
if base is not cls:
|
||||
inherited_table_args = True
|
||||
elif class_mapped:
|
||||
if isinstance(obj, declarative_props):
|
||||
util.warn("Regular (i.e. not __special__) "
|
||||
"attribute '%s.%s' uses @declared_attr, "
|
||||
"but owning class %s is mapped - "
|
||||
"not applying to subclass %s."
|
||||
% (base.__name__, name, base, cls))
|
||||
continue
|
||||
elif base is not cls:
|
||||
# we're a mixin.
|
||||
if isinstance(obj, Column):
|
||||
if getattr(cls, name) is not obj:
|
||||
# if column has been overridden
|
||||
# (like by the InstrumentedAttribute of the
|
||||
# superclass), skip
|
||||
continue
|
||||
if obj.foreign_keys:
|
||||
raise exc.InvalidRequestError(
|
||||
"Columns with foreign keys to other columns "
|
||||
"must be declared as @declared_attr callables "
|
||||
"on declarative mixin classes. ")
|
||||
if name not in dict_ and not (
|
||||
'__table__' in dict_ and
|
||||
(obj.name or name) in dict_['__table__'].c
|
||||
) and name not in potential_columns:
|
||||
potential_columns[name] = \
|
||||
column_copies[obj] = \
|
||||
obj.copy()
|
||||
column_copies[obj]._creation_order = \
|
||||
obj._creation_order
|
||||
elif isinstance(obj, MapperProperty):
|
||||
raise exc.InvalidRequestError(
|
||||
"Mapper properties (i.e. deferred,"
|
||||
"column_property(), relationship(), etc.) must "
|
||||
"be declared as @declared_attr callables "
|
||||
"on declarative mixin classes.")
|
||||
elif isinstance(obj, declarative_props):
|
||||
dict_[name] = ret = \
|
||||
column_copies[obj] = getattr(cls, name)
|
||||
if isinstance(ret, (Column, MapperProperty)) and \
|
||||
ret.doc is None:
|
||||
ret.doc = obj.__doc__
|
||||
|
||||
# apply inherited columns as we should
|
||||
for k, v in potential_columns.items():
|
||||
dict_[k] = v
|
||||
|
||||
if inherited_table_args and not tablename:
|
||||
table_args = None
|
||||
|
||||
clsregistry.add_class(classname, cls)
|
||||
our_stuff = util.OrderedDict()
|
||||
|
||||
for k in list(dict_):
|
||||
|
||||
# TODO: improve this ? all dunders ?
|
||||
if k in ('__table__', '__tablename__', '__mapper_args__'):
|
||||
continue
|
||||
|
||||
value = dict_[k]
|
||||
if isinstance(value, declarative_props):
|
||||
value = getattr(cls, k)
|
||||
|
||||
elif isinstance(value, QueryableAttribute) and \
|
||||
value.class_ is not cls and \
|
||||
value.key != k:
|
||||
# detect a QueryableAttribute that's already mapped being
|
||||
# assigned elsewhere in userland, turn into a synonym()
|
||||
value = synonym(value.key)
|
||||
setattr(cls, k, value)
|
||||
|
||||
|
||||
if (isinstance(value, tuple) and len(value) == 1 and
|
||||
isinstance(value[0], (Column, MapperProperty))):
|
||||
util.warn("Ignoring declarative-like tuple value of attribute "
|
||||
"%s: possibly a copy-and-paste error with a comma "
|
||||
"left at the end of the line?" % k)
|
||||
continue
|
||||
if not isinstance(value, (Column, MapperProperty)):
|
||||
if not k.startswith('__'):
|
||||
dict_.pop(k)
|
||||
setattr(cls, k, value)
|
||||
continue
|
||||
if k == 'metadata':
|
||||
raise exc.InvalidRequestError(
|
||||
"Attribute name 'metadata' is reserved "
|
||||
"for the MetaData instance when using a "
|
||||
"declarative base class."
|
||||
)
|
||||
prop = clsregistry._deferred_relationship(cls, value)
|
||||
our_stuff[k] = prop
|
||||
|
||||
# set up attributes in the order they were created
|
||||
our_stuff.sort(key=lambda key: our_stuff[key]._creation_order)
|
||||
|
||||
# extract columns from the class dict
|
||||
declared_columns = set()
|
||||
name_to_prop_key = collections.defaultdict(set)
|
||||
for key, c in list(our_stuff.items()):
|
||||
if isinstance(c, (ColumnProperty, CompositeProperty)):
|
||||
for col in c.columns:
|
||||
if isinstance(col, Column) and \
|
||||
col.table is None:
|
||||
_undefer_column_name(key, col)
|
||||
if not isinstance(c, CompositeProperty):
|
||||
name_to_prop_key[col.name].add(key)
|
||||
declared_columns.add(col)
|
||||
elif isinstance(c, Column):
|
||||
_undefer_column_name(key, c)
|
||||
name_to_prop_key[c.name].add(key)
|
||||
declared_columns.add(c)
|
||||
# if the column is the same name as the key,
|
||||
# remove it from the explicit properties dict.
|
||||
# the normal rules for assigning column-based properties
|
||||
# will take over, including precedence of columns
|
||||
# in multi-column ColumnProperties.
|
||||
if key == c.key:
|
||||
del our_stuff[key]
|
||||
|
||||
for name, keys in name_to_prop_key.items():
|
||||
if len(keys) > 1:
|
||||
util.warn(
|
||||
"On class %r, Column object %r named directly multiple times, "
|
||||
"only one will be used: %s" %
|
||||
(classname, name, (", ".join(sorted(keys))))
|
||||
)
|
||||
|
||||
declared_columns = sorted(
|
||||
declared_columns, key=lambda c: c._creation_order)
|
||||
table = None
|
||||
|
||||
if hasattr(cls, '__table_cls__'):
|
||||
table_cls = util.unbound_method_to_callable(cls.__table_cls__)
|
||||
else:
|
||||
table_cls = Table
|
||||
|
||||
if '__table__' not in dict_:
|
||||
if tablename is not None:
|
||||
|
||||
args, table_kw = (), {}
|
||||
if table_args:
|
||||
if isinstance(table_args, dict):
|
||||
table_kw = table_args
|
||||
elif isinstance(table_args, tuple):
|
||||
if isinstance(table_args[-1], dict):
|
||||
args, table_kw = table_args[0:-1], table_args[-1]
|
||||
else:
|
||||
args = table_args
|
||||
|
||||
autoload = dict_.get('__autoload__')
|
||||
if autoload:
|
||||
table_kw['autoload'] = True
|
||||
|
||||
cls.__table__ = table = table_cls(
|
||||
tablename, cls.metadata,
|
||||
*(tuple(declared_columns) + tuple(args)),
|
||||
**table_kw)
|
||||
else:
|
||||
table = cls.__table__
|
||||
if declared_columns:
|
||||
for c in declared_columns:
|
||||
if not table.c.contains_column(c):
|
||||
raise exc.ArgumentError(
|
||||
"Can't add additional column %r when "
|
||||
"specifying __table__" % c.key
|
||||
)
|
||||
|
||||
if hasattr(cls, '__mapper_cls__'):
|
||||
mapper_cls = util.unbound_method_to_callable(cls.__mapper_cls__)
|
||||
else:
|
||||
mapper_cls = mapper
|
||||
|
||||
for c in cls.__bases__:
|
||||
if _declared_mapping_info(c) is not None:
|
||||
inherits = c
|
||||
break
|
||||
else:
|
||||
inherits = None
|
||||
|
||||
if table is None and inherits is None:
|
||||
raise exc.InvalidRequestError(
|
||||
"Class %r does not have a __table__ or __tablename__ "
|
||||
"specified and does not inherit from an existing "
|
||||
"table-mapped class." % cls
|
||||
)
|
||||
elif inherits:
|
||||
inherited_mapper = _declared_mapping_info(inherits)
|
||||
inherited_table = inherited_mapper.local_table
|
||||
inherited_mapped_table = inherited_mapper.mapped_table
|
||||
|
||||
if table is None:
|
||||
# single table inheritance.
|
||||
# ensure no table args
|
||||
if table_args:
|
||||
raise exc.ArgumentError(
|
||||
"Can't place __table_args__ on an inherited class "
|
||||
"with no table."
|
||||
)
|
||||
# add any columns declared here to the inherited table.
|
||||
for c in declared_columns:
|
||||
if c.primary_key:
|
||||
raise exc.ArgumentError(
|
||||
"Can't place primary key columns on an inherited "
|
||||
"class with no table."
|
||||
)
|
||||
if c.name in inherited_table.c:
|
||||
if inherited_table.c[c.name] is c:
|
||||
continue
|
||||
raise exc.ArgumentError(
|
||||
"Column '%s' on class %s conflicts with "
|
||||
"existing column '%s'" %
|
||||
(c, cls, inherited_table.c[c.name])
|
||||
)
|
||||
inherited_table.append_column(c)
|
||||
if inherited_mapped_table is not None and \
|
||||
inherited_mapped_table is not inherited_table:
|
||||
inherited_mapped_table._refresh_for_new_column(c)
|
||||
|
||||
defer_map = hasattr(cls, '_sa_decl_prepare')
|
||||
if defer_map:
|
||||
cfg_cls = _DeferredMapperConfig
|
||||
else:
|
||||
cfg_cls = _MapperConfig
|
||||
mt = cfg_cls(mapper_cls,
|
||||
cls, table,
|
||||
inherits,
|
||||
declared_columns,
|
||||
column_copies,
|
||||
our_stuff,
|
||||
mapper_args_fn)
|
||||
if not defer_map:
|
||||
mt.map()
|
||||
|
||||
|
||||
class _MapperConfig(object):
|
||||
|
||||
mapped_table = None
|
||||
|
||||
def __init__(self, mapper_cls,
|
||||
cls,
|
||||
table,
|
||||
inherits,
|
||||
declared_columns,
|
||||
column_copies,
|
||||
properties, mapper_args_fn):
|
||||
self.mapper_cls = mapper_cls
|
||||
self.cls = cls
|
||||
self.local_table = table
|
||||
self.inherits = inherits
|
||||
self.properties = properties
|
||||
self.mapper_args_fn = mapper_args_fn
|
||||
self.declared_columns = declared_columns
|
||||
self.column_copies = column_copies
|
||||
|
||||
|
||||
def _prepare_mapper_arguments(self):
|
||||
properties = self.properties
|
||||
if self.mapper_args_fn:
|
||||
mapper_args = self.mapper_args_fn()
|
||||
else:
|
||||
mapper_args = {}
|
||||
|
||||
# make sure that column copies are used rather
|
||||
# than the original columns from any mixins
|
||||
for k in ('version_id_col', 'polymorphic_on',):
|
||||
if k in mapper_args:
|
||||
v = mapper_args[k]
|
||||
mapper_args[k] = self.column_copies.get(v, v)
|
||||
|
||||
assert 'inherits' not in mapper_args, \
|
||||
"Can't specify 'inherits' explicitly with declarative mappings"
|
||||
|
||||
if self.inherits:
|
||||
mapper_args['inherits'] = self.inherits
|
||||
|
||||
if self.inherits and not mapper_args.get('concrete', False):
|
||||
# single or joined inheritance
|
||||
# exclude any cols on the inherited table which are
|
||||
# not mapped on the parent class, to avoid
|
||||
# mapping columns specific to sibling/nephew classes
|
||||
inherited_mapper = _declared_mapping_info(self.inherits)
|
||||
inherited_table = inherited_mapper.local_table
|
||||
|
||||
if 'exclude_properties' not in mapper_args:
|
||||
mapper_args['exclude_properties'] = exclude_properties = \
|
||||
set([c.key for c in inherited_table.c
|
||||
if c not in inherited_mapper._columntoproperty])
|
||||
exclude_properties.difference_update(
|
||||
[c.key for c in self.declared_columns])
|
||||
|
||||
# look through columns in the current mapper that
|
||||
# are keyed to a propname different than the colname
|
||||
# (if names were the same, we'd have popped it out above,
|
||||
# in which case the mapper makes this combination).
|
||||
# See if the superclass has a similar column property.
|
||||
# If so, join them together.
|
||||
for k, col in list(properties.items()):
|
||||
if not isinstance(col, expression.ColumnElement):
|
||||
continue
|
||||
if k in inherited_mapper._props:
|
||||
p = inherited_mapper._props[k]
|
||||
if isinstance(p, ColumnProperty):
|
||||
# note here we place the subclass column
|
||||
# first. See [ticket:1892] for background.
|
||||
properties[k] = [col] + p.columns
|
||||
result_mapper_args = mapper_args.copy()
|
||||
result_mapper_args['properties'] = properties
|
||||
return result_mapper_args
|
||||
|
||||
def map(self):
|
||||
mapper_args = self._prepare_mapper_arguments()
|
||||
self.cls.__mapper__ = self.mapper_cls(
|
||||
self.cls,
|
||||
self.local_table,
|
||||
**mapper_args
|
||||
)
|
||||
|
||||
class _DeferredMapperConfig(_MapperConfig):
|
||||
_configs = util.OrderedDict()
|
||||
|
||||
@property
|
||||
def cls(self):
|
||||
return self._cls()
|
||||
|
||||
@cls.setter
|
||||
def cls(self, class_):
|
||||
self._cls = weakref.ref(class_, self._remove_config_cls)
|
||||
self._configs[self._cls] = self
|
||||
|
||||
@classmethod
|
||||
def _remove_config_cls(cls, ref):
|
||||
cls._configs.pop(ref, None)
|
||||
|
||||
@classmethod
|
||||
def has_cls(cls, class_):
|
||||
# 2.6 fails on weakref if class_ is an old style class
|
||||
return isinstance(class_, type) and \
|
||||
weakref.ref(class_) in cls._configs
|
||||
|
||||
@classmethod
|
||||
def config_for_cls(cls, class_):
|
||||
return cls._configs[weakref.ref(class_)]
|
||||
|
||||
|
||||
@classmethod
|
||||
def classes_for_base(cls, base_cls):
|
||||
return [m for m in cls._configs.values()
|
||||
if issubclass(m.cls, base_cls)]
|
||||
|
||||
def map(self):
|
||||
self._configs.pop(self._cls, None)
|
||||
super(_DeferredMapperConfig, self).map()
|
||||
|
||||
|
||||
def _add_attribute(cls, key, value):
|
||||
"""add an attribute to an existing declarative class.
|
||||
|
||||
This runs through the logic to determine MapperProperty,
|
||||
adds it to the Mapper, adds a column to the mapped Table, etc.
|
||||
|
||||
"""
|
||||
|
||||
if '__mapper__' in cls.__dict__:
|
||||
if isinstance(value, Column):
|
||||
_undefer_column_name(key, value)
|
||||
cls.__table__.append_column(value)
|
||||
cls.__mapper__.add_property(key, value)
|
||||
elif isinstance(value, ColumnProperty):
|
||||
for col in value.columns:
|
||||
if isinstance(col, Column) and col.table is None:
|
||||
_undefer_column_name(key, col)
|
||||
cls.__table__.append_column(col)
|
||||
cls.__mapper__.add_property(key, value)
|
||||
elif isinstance(value, MapperProperty):
|
||||
cls.__mapper__.add_property(
|
||||
key,
|
||||
clsregistry._deferred_relationship(cls, value)
|
||||
)
|
||||
elif isinstance(value, QueryableAttribute) and value.key != key:
|
||||
# detect a QueryableAttribute that's already mapped being
|
||||
# assigned elsewhere in userland, turn into a synonym()
|
||||
value = synonym(value.key)
|
||||
cls.__mapper__.add_property(
|
||||
key,
|
||||
clsregistry._deferred_relationship(cls, value)
|
||||
)
|
||||
else:
|
||||
type.__setattr__(cls, key, value)
|
||||
else:
|
||||
type.__setattr__(cls, key, value)
|
||||
|
||||
|
||||
def _declarative_constructor(self, **kwargs):
|
||||
"""A simple constructor that allows initialization from kwargs.
|
||||
|
||||
Sets attributes on the constructed instance using the names and
|
||||
values in ``kwargs``.
|
||||
|
||||
Only keys that are present as
|
||||
attributes of the instance's class are allowed. These could be,
|
||||
for example, any mapped columns or relationships.
|
||||
"""
|
||||
cls_ = type(self)
|
||||
for k in kwargs:
|
||||
if not hasattr(cls_, k):
|
||||
raise TypeError(
|
||||
"%r is an invalid keyword argument for %s" %
|
||||
(k, cls_.__name__))
|
||||
setattr(self, k, kwargs[k])
|
||||
_declarative_constructor.__name__ = '__init__'
|
||||
|
||||
|
||||
def _undefer_column_name(key, column):
|
||||
if column.key is None:
|
||||
column.key = key
|
||||
if column.name is None:
|
||||
column.name = key
|
||||
@@ -0,0 +1,305 @@
|
||||
# ext/declarative/clsregistry.py
|
||||
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
"""Routines to handle the string class registry used by declarative.
|
||||
|
||||
This system allows specification of classes and expressions used in
|
||||
:func:`.relationship` using strings.
|
||||
|
||||
"""
|
||||
from ...orm.properties import ColumnProperty, RelationshipProperty, \
|
||||
SynonymProperty
|
||||
from ...schema import _get_table_key
|
||||
from ...orm import class_mapper, interfaces
|
||||
from ... import util
|
||||
from ... import exc
|
||||
import weakref
|
||||
|
||||
# strong references to registries which we place in
|
||||
# the _decl_class_registry, which is usually weak referencing.
|
||||
# the internal registries here link to classes with weakrefs and remove
|
||||
# themselves when all references to contained classes are removed.
|
||||
_registries = set()
|
||||
|
||||
|
||||
def add_class(classname, cls):
|
||||
"""Add a class to the _decl_class_registry associated with the
|
||||
given declarative class.
|
||||
|
||||
"""
|
||||
if classname in cls._decl_class_registry:
|
||||
# class already exists.
|
||||
existing = cls._decl_class_registry[classname]
|
||||
if not isinstance(existing, _MultipleClassMarker):
|
||||
existing = \
|
||||
cls._decl_class_registry[classname] = \
|
||||
_MultipleClassMarker([cls, existing])
|
||||
else:
|
||||
cls._decl_class_registry[classname] = cls
|
||||
|
||||
try:
|
||||
root_module = cls._decl_class_registry['_sa_module_registry']
|
||||
except KeyError:
|
||||
cls._decl_class_registry['_sa_module_registry'] = \
|
||||
root_module = _ModuleMarker('_sa_module_registry', None)
|
||||
|
||||
tokens = cls.__module__.split(".")
|
||||
|
||||
# build up a tree like this:
|
||||
# modulename: myapp.snacks.nuts
|
||||
#
|
||||
# myapp->snack->nuts->(classes)
|
||||
# snack->nuts->(classes)
|
||||
# nuts->(classes)
|
||||
#
|
||||
# this allows partial token paths to be used.
|
||||
while tokens:
|
||||
token = tokens.pop(0)
|
||||
module = root_module.get_module(token)
|
||||
for token in tokens:
|
||||
module = module.get_module(token)
|
||||
module.add_class(classname, cls)
|
||||
|
||||
|
||||
class _MultipleClassMarker(object):
|
||||
"""refers to multiple classes of the same name
|
||||
within _decl_class_registry.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, classes, on_remove=None):
|
||||
self.on_remove = on_remove
|
||||
self.contents = set([
|
||||
weakref.ref(item, self._remove_item) for item in classes])
|
||||
_registries.add(self)
|
||||
|
||||
def __iter__(self):
|
||||
return (ref() for ref in self.contents)
|
||||
|
||||
def attempt_get(self, path, key):
|
||||
if len(self.contents) > 1:
|
||||
raise exc.InvalidRequestError(
|
||||
"Multiple classes found for path \"%s\" "
|
||||
"in the registry of this declarative "
|
||||
"base. Please use a fully module-qualified path." %
|
||||
(".".join(path + [key]))
|
||||
)
|
||||
else:
|
||||
ref = list(self.contents)[0]
|
||||
cls = ref()
|
||||
if cls is None:
|
||||
raise NameError(key)
|
||||
return cls
|
||||
|
||||
def _remove_item(self, ref):
|
||||
self.contents.remove(ref)
|
||||
if not self.contents:
|
||||
_registries.discard(self)
|
||||
if self.on_remove:
|
||||
self.on_remove()
|
||||
|
||||
def add_item(self, item):
|
||||
modules = set([cls().__module__ for cls in self.contents])
|
||||
if item.__module__ in modules:
|
||||
util.warn(
|
||||
"This declarative base already contains a class with the "
|
||||
"same class name and module name as %s.%s, and will "
|
||||
"be replaced in the string-lookup table." % (
|
||||
item.__module__,
|
||||
item.__name__
|
||||
)
|
||||
)
|
||||
self.contents.add(weakref.ref(item, self._remove_item))
|
||||
|
||||
|
||||
class _ModuleMarker(object):
|
||||
""""refers to a module name within
|
||||
_decl_class_registry.
|
||||
|
||||
"""
|
||||
def __init__(self, name, parent):
|
||||
self.parent = parent
|
||||
self.name = name
|
||||
self.contents = {}
|
||||
self.mod_ns = _ModNS(self)
|
||||
if self.parent:
|
||||
self.path = self.parent.path + [self.name]
|
||||
else:
|
||||
self.path = []
|
||||
_registries.add(self)
|
||||
|
||||
def __contains__(self, name):
|
||||
return name in self.contents
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.contents[name]
|
||||
|
||||
def _remove_item(self, name):
|
||||
self.contents.pop(name, None)
|
||||
if not self.contents and self.parent is not None:
|
||||
self.parent._remove_item(self.name)
|
||||
_registries.discard(self)
|
||||
|
||||
def resolve_attr(self, key):
|
||||
return getattr(self.mod_ns, key)
|
||||
|
||||
def get_module(self, name):
|
||||
if name not in self.contents:
|
||||
marker = _ModuleMarker(name, self)
|
||||
self.contents[name] = marker
|
||||
else:
|
||||
marker = self.contents[name]
|
||||
return marker
|
||||
|
||||
def add_class(self, name, cls):
|
||||
if name in self.contents:
|
||||
existing = self.contents[name]
|
||||
existing.add_item(cls)
|
||||
else:
|
||||
existing = self.contents[name] = \
|
||||
_MultipleClassMarker([cls],
|
||||
on_remove=lambda: self._remove_item(name))
|
||||
|
||||
|
||||
class _ModNS(object):
|
||||
def __init__(self, parent):
|
||||
self.__parent = parent
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
value = self.__parent.contents[key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if value is not None:
|
||||
if isinstance(value, _ModuleMarker):
|
||||
return value.mod_ns
|
||||
else:
|
||||
assert isinstance(value, _MultipleClassMarker)
|
||||
return value.attempt_get(self.__parent.path, key)
|
||||
raise AttributeError("Module %r has no mapped classes "
|
||||
"registered under the name %r" % (self.__parent.name, key))
|
||||
|
||||
|
||||
class _GetColumns(object):
|
||||
def __init__(self, cls):
|
||||
self.cls = cls
|
||||
|
||||
def __getattr__(self, key):
|
||||
mp = class_mapper(self.cls, configure=False)
|
||||
if mp:
|
||||
if key not in mp.all_orm_descriptors:
|
||||
raise exc.InvalidRequestError(
|
||||
"Class %r does not have a mapped column named %r"
|
||||
% (self.cls, key))
|
||||
|
||||
desc = mp.all_orm_descriptors[key]
|
||||
if desc.extension_type is interfaces.NOT_EXTENSION:
|
||||
prop = desc.property
|
||||
if isinstance(prop, SynonymProperty):
|
||||
key = prop.name
|
||||
elif not isinstance(prop, ColumnProperty):
|
||||
raise exc.InvalidRequestError(
|
||||
"Property %r is not an instance of"
|
||||
" ColumnProperty (i.e. does not correspond"
|
||||
" directly to a Column)." % key)
|
||||
return getattr(self.cls, key)
|
||||
|
||||
|
||||
class _GetTable(object):
|
||||
def __init__(self, key, metadata):
|
||||
self.key = key
|
||||
self.metadata = metadata
|
||||
|
||||
def __getattr__(self, key):
|
||||
return self.metadata.tables[
|
||||
_get_table_key(key, self.key)
|
||||
]
|
||||
|
||||
|
||||
def _determine_container(key, value):
|
||||
if isinstance(value, _MultipleClassMarker):
|
||||
value = value.attempt_get([], key)
|
||||
return _GetColumns(value)
|
||||
|
||||
|
||||
class _class_resolver(object):
|
||||
def __init__(self, cls, prop, fallback, arg):
|
||||
self.cls = cls
|
||||
self.prop = prop
|
||||
self.arg = self._declarative_arg = arg
|
||||
self.fallback = fallback
|
||||
self._dict = util.PopulateDict(self._access_cls)
|
||||
self._resolvers = ()
|
||||
|
||||
def _access_cls(self, key):
|
||||
cls = self.cls
|
||||
if key in cls._decl_class_registry:
|
||||
return _determine_container(key, cls._decl_class_registry[key])
|
||||
elif key in cls.metadata.tables:
|
||||
return cls.metadata.tables[key]
|
||||
elif key in cls.metadata._schemas:
|
||||
return _GetTable(key, cls.metadata)
|
||||
elif '_sa_module_registry' in cls._decl_class_registry and \
|
||||
key in cls._decl_class_registry['_sa_module_registry']:
|
||||
registry = cls._decl_class_registry['_sa_module_registry']
|
||||
return registry.resolve_attr(key)
|
||||
elif self._resolvers:
|
||||
for resolv in self._resolvers:
|
||||
value = resolv(key)
|
||||
if value is not None:
|
||||
return value
|
||||
|
||||
return self.fallback[key]
|
||||
|
||||
def __call__(self):
|
||||
try:
|
||||
x = eval(self.arg, globals(), self._dict)
|
||||
|
||||
if isinstance(x, _GetColumns):
|
||||
return x.cls
|
||||
else:
|
||||
return x
|
||||
except NameError as n:
|
||||
raise exc.InvalidRequestError(
|
||||
"When initializing mapper %s, expression %r failed to "
|
||||
"locate a name (%r). If this is a class name, consider "
|
||||
"adding this relationship() to the %r class after "
|
||||
"both dependent classes have been defined." %
|
||||
(self.prop.parent, self.arg, n.args[0], self.cls)
|
||||
)
|
||||
|
||||
|
||||
def _resolver(cls, prop):
|
||||
import sqlalchemy
|
||||
from sqlalchemy.orm import foreign, remote
|
||||
|
||||
fallback = sqlalchemy.__dict__.copy()
|
||||
fallback.update({'foreign': foreign, 'remote': remote})
|
||||
|
||||
def resolve_arg(arg):
|
||||
return _class_resolver(cls, prop, fallback, arg)
|
||||
return resolve_arg
|
||||
|
||||
|
||||
def _deferred_relationship(cls, prop):
|
||||
|
||||
if isinstance(prop, RelationshipProperty):
|
||||
resolve_arg = _resolver(cls, prop)
|
||||
|
||||
for attr in ('argument', 'order_by', 'primaryjoin', 'secondaryjoin',
|
||||
'secondary', '_user_defined_foreign_keys', 'remote_side'):
|
||||
v = getattr(prop, attr)
|
||||
if isinstance(v, util.string_types):
|
||||
setattr(prop, attr, resolve_arg(v))
|
||||
|
||||
if prop.backref and isinstance(prop.backref, tuple):
|
||||
key, kwargs = prop.backref
|
||||
for attr in ('primaryjoin', 'secondaryjoin', 'secondary',
|
||||
'foreign_keys', 'remote_side', 'order_by'):
|
||||
if attr in kwargs and isinstance(kwargs[attr], str):
|
||||
kwargs[attr] = resolve_arg(kwargs[attr])
|
||||
|
||||
return prop
|
||||
@@ -1,5 +1,5 @@
|
||||
# ext/horizontal_shard.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -14,13 +14,13 @@ the source distribution.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy import exc as sa_exc
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy.orm.query import Query
|
||||
from .. import util
|
||||
from ..orm.session import Session
|
||||
from ..orm.query import Query
|
||||
|
||||
__all__ = ['ShardedSession', 'ShardedQuery']
|
||||
|
||||
|
||||
class ShardedQuery(Query):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ShardedQuery, self).__init__(*args, **kwargs)
|
||||
@@ -72,28 +72,29 @@ class ShardedQuery(Query):
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class ShardedSession(Session):
|
||||
def __init__(self, shard_chooser, id_chooser, query_chooser, shards=None,
|
||||
query_cls=ShardedQuery, **kwargs):
|
||||
"""Construct a ShardedSession.
|
||||
|
||||
:param shard_chooser: A callable which, passed a Mapper, a mapped instance, and possibly a
|
||||
SQL clause, returns a shard ID. This id may be based off of the
|
||||
attributes present within the object, or on some round-robin
|
||||
scheme. If the scheme is based on a selection, it should set
|
||||
whatever state on the instance to mark it in the future as
|
||||
:param shard_chooser: A callable which, passed a Mapper, a mapped
|
||||
instance, and possibly a SQL clause, returns a shard ID. This id
|
||||
may be based off of the attributes present within the object, or on
|
||||
some round-robin scheme. If the scheme is based on a selection, it
|
||||
should set whatever state on the instance to mark it in the future as
|
||||
participating in that shard.
|
||||
|
||||
:param id_chooser: A callable, passed a query and a tuple of identity values, which
|
||||
should return a list of shard ids where the ID might reside. The
|
||||
databases will be queried in the order of this listing.
|
||||
:param id_chooser: A callable, passed a query and a tuple of identity
|
||||
values, which should return a list of shard ids where the ID might
|
||||
reside. The databases will be queried in the order of this listing.
|
||||
|
||||
:param query_chooser: For a given Query, returns the list of shard_ids where the query
|
||||
should be issued. Results from all shards returned will be combined
|
||||
together into a single listing.
|
||||
:param query_chooser: For a given Query, returns the list of shard_ids
|
||||
where the query should be issued. Results from all shards returned
|
||||
will be combined together into a single listing.
|
||||
|
||||
:param shards: A dictionary of string shard names to :class:`~sqlalchemy.engine.base.Engine`
|
||||
objects.
|
||||
:param shards: A dictionary of string shard names
|
||||
to :class:`~sqlalchemy.engine.Engine` objects.
|
||||
|
||||
"""
|
||||
super(ShardedSession, self).__init__(query_cls=query_cls, **kwargs)
|
||||
@@ -117,12 +118,11 @@ class ShardedSession(Session):
|
||||
shard_id=shard_id,
|
||||
instance=instance).contextual_connect(**kwargs)
|
||||
|
||||
def get_bind(self, mapper, shard_id=None, instance=None, clause=None, **kw):
|
||||
def get_bind(self, mapper, shard_id=None,
|
||||
instance=None, clause=None, **kw):
|
||||
if shard_id is None:
|
||||
shard_id = self.shard_chooser(mapper, instance, clause=clause)
|
||||
return self.__binds[shard_id]
|
||||
|
||||
def bind_shard(self, shard_id, bind):
|
||||
self.__binds[shard_id] = bind
|
||||
|
||||
|
||||
|
||||
+156
-95
@@ -1,5 +1,5 @@
|
||||
# ext/hybrid.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -9,10 +9,10 @@
|
||||
"hybrid" means the attribute has distinct behaviors defined at the
|
||||
class level and at the instance level.
|
||||
|
||||
The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of method
|
||||
decorator, is around 50 lines of code and has almost no dependencies on the rest
|
||||
of SQLAlchemy. It can, in theory, work with any descriptor-based expression
|
||||
system.
|
||||
The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of
|
||||
method decorator, is around 50 lines of code and has almost no
|
||||
dependencies on the rest of SQLAlchemy. It can, in theory, work with
|
||||
any descriptor-based expression system.
|
||||
|
||||
Consider a mapping ``Interval``, representing integer ``start`` and ``end``
|
||||
values. We can define higher level functions on mapped classes that produce
|
||||
@@ -51,9 +51,10 @@ as the class itself::
|
||||
def intersects(self, other):
|
||||
return self.contains(other.start) | self.contains(other.end)
|
||||
|
||||
Above, the ``length`` property returns the difference between the ``end`` and
|
||||
``start`` attributes. With an instance of ``Interval``, this subtraction occurs
|
||||
in Python, using normal Python descriptor mechanics::
|
||||
Above, the ``length`` property returns the difference between the
|
||||
``end`` and ``start`` attributes. With an instance of ``Interval``,
|
||||
this subtraction occurs in Python, using normal Python descriptor
|
||||
mechanics::
|
||||
|
||||
>>> i1 = Interval(5, 10)
|
||||
>>> i1.length
|
||||
@@ -82,11 +83,12 @@ locate attributes, so can also be used with hybrid attributes::
|
||||
FROM interval
|
||||
WHERE interval."end" - interval.start = :param_1
|
||||
|
||||
The ``Interval`` class example also illustrates two methods, ``contains()`` and ``intersects()``,
|
||||
decorated with :class:`.hybrid_method`.
|
||||
This decorator applies the same idea to methods that :class:`.hybrid_property` applies
|
||||
to attributes. The methods return boolean values, and take advantage
|
||||
of the Python ``|`` and ``&`` bitwise operators to produce equivalent instance-level and
|
||||
The ``Interval`` class example also illustrates two methods,
|
||||
``contains()`` and ``intersects()``, decorated with
|
||||
:class:`.hybrid_method`. This decorator applies the same idea to
|
||||
methods that :class:`.hybrid_property` applies to attributes. The
|
||||
methods return boolean values, and take advantage of the Python ``|``
|
||||
and ``&`` bitwise operators to produce equivalent instance-level and
|
||||
SQL expression-level boolean behavior::
|
||||
|
||||
>>> i1.contains(6)
|
||||
@@ -118,12 +120,15 @@ SQL expression-level boolean behavior::
|
||||
Defining Expression Behavior Distinct from Attribute Behavior
|
||||
--------------------------------------------------------------
|
||||
|
||||
Our usage of the ``&`` and ``|`` bitwise operators above was fortunate, considering
|
||||
our functions operated on two boolean values to return a new one. In many cases, the construction
|
||||
of an in-Python function and a SQLAlchemy SQL expression have enough differences that two
|
||||
separate Python expressions should be defined. The :mod:`~sqlalchemy.ext.hybrid` decorators
|
||||
define the :meth:`.hybrid_property.expression` modifier for this purpose. As an example we'll
|
||||
define the radius of the interval, which requires the usage of the absolute value function::
|
||||
Our usage of the ``&`` and ``|`` bitwise operators above was
|
||||
fortunate, considering our functions operated on two boolean values to
|
||||
return a new one. In many cases, the construction of an in-Python
|
||||
function and a SQLAlchemy SQL expression have enough differences that
|
||||
two separate Python expressions should be defined. The
|
||||
:mod:`~sqlalchemy.ext.hybrid` decorators define the
|
||||
:meth:`.hybrid_property.expression` modifier for this purpose. As an
|
||||
example we'll define the radius of the interval, which requires the
|
||||
usage of the absolute value function::
|
||||
|
||||
from sqlalchemy import func
|
||||
|
||||
@@ -138,8 +143,9 @@ define the radius of the interval, which requires the usage of the absolute valu
|
||||
def radius(cls):
|
||||
return func.abs(cls.length) / 2
|
||||
|
||||
Above the Python function ``abs()`` is used for instance-level operations, the SQL function
|
||||
``ABS()`` is used via the :attr:`.func` object for class-level expressions::
|
||||
Above the Python function ``abs()`` is used for instance-level
|
||||
operations, the SQL function ``ABS()`` is used via the :attr:`.func`
|
||||
object for class-level expressions::
|
||||
|
||||
>>> i1.radius
|
||||
2
|
||||
@@ -153,8 +159,8 @@ Above the Python function ``abs()`` is used for instance-level operations, the S
|
||||
Defining Setters
|
||||
----------------
|
||||
|
||||
Hybrid properties can also define setter methods. If we wanted ``length`` above, when
|
||||
set, to modify the endpoint value::
|
||||
Hybrid properties can also define setter methods. If we wanted
|
||||
``length`` above, when set, to modify the endpoint value::
|
||||
|
||||
class Interval(object):
|
||||
# ...
|
||||
@@ -223,7 +229,7 @@ mapping which relates a ``User`` to a ``SavingsAccount``::
|
||||
account = Account(owner=self)
|
||||
else:
|
||||
account = self.accounts[0]
|
||||
account.balance = balance
|
||||
account.balance = value
|
||||
|
||||
@balance.expression
|
||||
def balance(cls):
|
||||
@@ -234,8 +240,8 @@ The above hybrid property ``balance`` works with the first
|
||||
in-Python getter/setter methods can treat ``accounts`` as a Python
|
||||
list available on ``self``.
|
||||
|
||||
However, at the expression level, it's expected that the ``User`` class will be used
|
||||
in an appropriate context such that an appropriate join to
|
||||
However, at the expression level, it's expected that the ``User`` class will
|
||||
be used in an appropriate context such that an appropriate join to
|
||||
``SavingsAccount`` will be present::
|
||||
|
||||
>>> print Session().query(User, User.balance).\\
|
||||
@@ -262,11 +268,10 @@ Correlated Subquery Relationship Hybrid
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
We can, of course, forego being dependent on the enclosing query's usage
|
||||
of joins in favor of the correlated
|
||||
subquery, which can portably be packed into a single colunn expression.
|
||||
A correlated subquery is more portable, but often performs more poorly
|
||||
at the SQL level.
|
||||
Using the same technique illustrated at :ref:`mapper_column_property_sql_expressions`,
|
||||
of joins in favor of the correlated subquery, which can portably be packed
|
||||
into a single column expression. A correlated subquery is more portable, but
|
||||
often performs more poorly at the SQL level. Using the same technique
|
||||
illustrated at :ref:`mapper_column_property_sql_expressions`,
|
||||
we can adjust our ``SavingsAccount`` example to aggregate the balances for
|
||||
*all* accounts, and use a correlated subquery for the column expression::
|
||||
|
||||
@@ -316,10 +321,11 @@ a correlated SELECT::
|
||||
Building Custom Comparators
|
||||
---------------------------
|
||||
|
||||
The hybrid property also includes a helper that allows construction of custom comparators.
|
||||
A comparator object allows one to customize the behavior of each SQLAlchemy expression
|
||||
operator individually. They are useful when creating custom types that have
|
||||
some highly idiosyncratic behavior on the SQL side.
|
||||
The hybrid property also includes a helper that allows construction of
|
||||
custom comparators. A comparator object allows one to customize the
|
||||
behavior of each SQLAlchemy expression operator individually. They
|
||||
are useful when creating custom types that have some highly
|
||||
idiosyncratic behavior on the SQL side.
|
||||
|
||||
The example class below allows case-insensitive comparisons on the attribute
|
||||
named ``word_insensitive``::
|
||||
@@ -356,9 +362,10 @@ SQL function to both sides::
|
||||
FROM searchword
|
||||
WHERE lower(searchword.word) = lower(:lower_1)
|
||||
|
||||
The ``CaseInsensitiveComparator`` above implements part of the :class:`.ColumnOperators`
|
||||
interface. A "coercion" operation like lowercasing can be applied to all comparison operations
|
||||
(i.e. ``eq``, ``lt``, ``gt``, etc.) using :meth:`.Operators.operate`::
|
||||
The ``CaseInsensitiveComparator`` above implements part of the
|
||||
:class:`.ColumnOperators` interface. A "coercion" operation like
|
||||
lowercasing can be applied to all comparison operations (i.e. ``eq``,
|
||||
``lt``, ``gt``, etc.) using :meth:`.Operators.operate`::
|
||||
|
||||
class CaseInsensitiveComparator(Comparator):
|
||||
def operate(self, op, other):
|
||||
@@ -367,17 +374,20 @@ interface. A "coercion" operation like lowercasing can be applied to all compa
|
||||
Hybrid Value Objects
|
||||
--------------------
|
||||
|
||||
Note in our previous example, if we were to compare the ``word_insensitive`` attribute of
|
||||
a ``SearchWord`` instance to a plain Python string, the plain Python string would not
|
||||
be coerced to lower case - the ``CaseInsensitiveComparator`` we built, being returned
|
||||
by ``@word_insensitive.comparator``, only applies to the SQL side.
|
||||
Note in our previous example, if we were to compare the
|
||||
``word_insensitive`` attribute of a ``SearchWord`` instance to a plain
|
||||
Python string, the plain Python string would not be coerced to lower
|
||||
case - the ``CaseInsensitiveComparator`` we built, being returned by
|
||||
``@word_insensitive.comparator``, only applies to the SQL side.
|
||||
|
||||
A more comprehensive form of the custom comparator is to construct a *Hybrid Value Object*.
|
||||
This technique applies the target value or expression to a value object which is then
|
||||
returned by the accessor in all cases. The value object allows control
|
||||
of all operations upon the value as well as how compared values are treated, both
|
||||
on the SQL expression side as well as the Python value side. Replacing the
|
||||
previous ``CaseInsensitiveComparator`` class with a new ``CaseInsensitiveWord`` class::
|
||||
A more comprehensive form of the custom comparator is to construct a
|
||||
*Hybrid Value Object*. This technique applies the target value or
|
||||
expression to a value object which is then returned by the accessor in
|
||||
all cases. The value object allows control of all operations upon
|
||||
the value as well as how compared values are treated, both on the SQL
|
||||
expression side as well as the Python value side. Replacing the
|
||||
previous ``CaseInsensitiveComparator`` class with a new
|
||||
``CaseInsensitiveWord`` class::
|
||||
|
||||
class CaseInsensitiveWord(Comparator):
|
||||
"Hybrid value representing a lower case representation of a word."
|
||||
@@ -404,12 +414,13 @@ previous ``CaseInsensitiveComparator`` class with a new ``CaseInsensitiveWord``
|
||||
key = 'word'
|
||||
"Label to apply to Query tuple results"
|
||||
|
||||
Above, the ``CaseInsensitiveWord`` object represents ``self.word``, which may be a SQL function,
|
||||
or may be a Python native. By overriding ``operate()`` and ``__clause_element__()``
|
||||
to work in terms of ``self.word``, all comparison operations will work against the
|
||||
Above, the ``CaseInsensitiveWord`` object represents ``self.word``,
|
||||
which may be a SQL function, or may be a Python native. By
|
||||
overriding ``operate()`` and ``__clause_element__()`` to work in terms
|
||||
of ``self.word``, all comparison operations will work against the
|
||||
"converted" form of ``word``, whether it be SQL side or Python side.
|
||||
Our ``SearchWord`` class can now deliver the ``CaseInsensitiveWord`` object unconditionally
|
||||
from a single hybrid call::
|
||||
Our ``SearchWord`` class can now deliver the ``CaseInsensitiveWord``
|
||||
object unconditionally from a single hybrid call::
|
||||
|
||||
class SearchWord(Base):
|
||||
__tablename__ = 'searchword'
|
||||
@@ -420,9 +431,10 @@ from a single hybrid call::
|
||||
def word_insensitive(self):
|
||||
return CaseInsensitiveWord(self.word)
|
||||
|
||||
The ``word_insensitive`` attribute now has case-insensitive comparison behavior
|
||||
universally, including SQL expression vs. Python expression (note the Python value is
|
||||
converted to lower case on the Python side here)::
|
||||
The ``word_insensitive`` attribute now has case-insensitive comparison
|
||||
behavior universally, including SQL expression vs. Python expression
|
||||
(note the Python value is converted to lower case on the Python side
|
||||
here)::
|
||||
|
||||
>>> print Session().query(SearchWord).filter_by(word_insensitive="Trucks")
|
||||
SELECT searchword.id AS searchword_id, searchword.word AS searchword_word
|
||||
@@ -439,7 +451,8 @@ SQL expression versus SQL expression::
|
||||
... filter(
|
||||
... sw1.word_insensitive > sw2.word_insensitive
|
||||
... )
|
||||
SELECT lower(searchword_1.word) AS lower_1, lower(searchword_2.word) AS lower_2
|
||||
SELECT lower(searchword_1.word) AS lower_1,
|
||||
lower(searchword_2.word) AS lower_2
|
||||
FROM searchword AS searchword_1, searchword AS searchword_2
|
||||
WHERE lower(searchword_1.word) > lower(searchword_2.word)
|
||||
|
||||
@@ -453,30 +466,36 @@ Python only expression::
|
||||
>>> print ws1.word_insensitive
|
||||
someword
|
||||
|
||||
The Hybrid Value pattern is very useful for any kind of value that may have multiple representations,
|
||||
such as timestamps, time deltas, units of measurement, currencies and encrypted passwords.
|
||||
The Hybrid Value pattern is very useful for any kind of value that may
|
||||
have multiple representations, such as timestamps, time deltas, units
|
||||
of measurement, currencies and encrypted passwords.
|
||||
|
||||
See Also:
|
||||
.. seealso::
|
||||
|
||||
`Hybrids and Value Agnostic Types <http://techspot.zzzeek.org/2011/10/21/hybrids-and-value-agnostic-types/>`_ - on the techspot.zzzeek.org blog
|
||||
`Hybrids and Value Agnostic Types
|
||||
<http://techspot.zzzeek.org/2011/10/21/hybrids-and-value-agnostic-types/>`_ -
|
||||
on the techspot.zzzeek.org blog
|
||||
|
||||
`Value Agnostic Types, Part II <http://techspot.zzzeek.org/2011/10/29/value-agnostic-types-part-ii/>`_ - on the techspot.zzzeek.org blog
|
||||
`Value Agnostic Types, Part II
|
||||
<http://techspot.zzzeek.org/2011/10/29/value-agnostic-types-part-ii/>`_ -
|
||||
on the techspot.zzzeek.org blog
|
||||
|
||||
.. _hybrid_transformers:
|
||||
|
||||
Building Transformers
|
||||
----------------------
|
||||
|
||||
A *transformer* is an object which can receive a :class:`.Query` object and return a
|
||||
new one. The :class:`.Query` object includes a method :meth:`.with_transformation`
|
||||
that simply returns a new :class:`.Query` transformed by the given function.
|
||||
A *transformer* is an object which can receive a :class:`.Query`
|
||||
object and return a new one. The :class:`.Query` object includes a
|
||||
method :meth:`.with_transformation` that returns a new :class:`.Query`
|
||||
transformed by the given function.
|
||||
|
||||
We can combine this with the :class:`.Comparator` class to produce one type
|
||||
of recipe which can both set up the FROM clause of a query as well as assign
|
||||
filtering criterion.
|
||||
|
||||
Consider a mapped class ``Node``, which assembles using adjacency list into a hierarchical
|
||||
tree pattern::
|
||||
Consider a mapped class ``Node``, which assembles using adjacency list
|
||||
into a hierarchical tree pattern::
|
||||
|
||||
from sqlalchemy import Column, Integer, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
@@ -489,8 +508,9 @@ tree pattern::
|
||||
parent_id = Column(Integer, ForeignKey('node.id'))
|
||||
parent = relationship("Node", remote_side=id)
|
||||
|
||||
Suppose we wanted to add an accessor ``grandparent``. This would return the ``parent`` of
|
||||
``Node.parent``. When we have an instance of ``Node``, this is simple::
|
||||
Suppose we wanted to add an accessor ``grandparent``. This would
|
||||
return the ``parent`` of ``Node.parent``. When we have an instance of
|
||||
``Node``, this is simple::
|
||||
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
|
||||
@@ -501,11 +521,13 @@ Suppose we wanted to add an accessor ``grandparent``. This would return the ``p
|
||||
def grandparent(self):
|
||||
return self.parent.parent
|
||||
|
||||
For the expression, things are not so clear. We'd need to construct a :class:`.Query` where we
|
||||
:meth:`~.Query.join` twice along ``Node.parent`` to get to the ``grandparent``. We can instead
|
||||
return a transforming callable that we'll combine with the :class:`.Comparator` class
|
||||
to receive any :class:`.Query` object, and return a new one that's joined to the ``Node.parent``
|
||||
attribute and filtered based on the given criterion::
|
||||
For the expression, things are not so clear. We'd need to construct
|
||||
a :class:`.Query` where we :meth:`~.Query.join` twice along
|
||||
``Node.parent`` to get to the ``grandparent``. We can instead return
|
||||
a transforming callable that we'll combine with the
|
||||
:class:`.Comparator` class to receive any :class:`.Query` object, and
|
||||
return a new one that's joined to the ``Node.parent`` attribute and
|
||||
filtered based on the given criterion::
|
||||
|
||||
from sqlalchemy.ext.hybrid import Comparator
|
||||
|
||||
@@ -534,15 +556,17 @@ attribute and filtered based on the given criterion::
|
||||
def grandparent(cls):
|
||||
return GrandparentTransformer(cls)
|
||||
|
||||
The ``GrandparentTransformer`` overrides the core :meth:`.Operators.operate` method
|
||||
at the base of the :class:`.Comparator` hierarchy to return a query-transforming
|
||||
callable, which then runs the given comparison operation in a particular context.
|
||||
Such as, in the example above, the ``operate`` method is called, given the
|
||||
:attr:`.Operators.eq` callable as well as the right side of the comparison
|
||||
``Node(id=5)``. A function ``transform`` is then returned which will transform
|
||||
a :class:`.Query` first to join to ``Node.parent``, then to compare ``parent_alias``
|
||||
using :attr:`.Operators.eq` against the left and right sides, passing into
|
||||
:class:`.Query.filter`:
|
||||
The ``GrandparentTransformer`` overrides the core
|
||||
:meth:`.Operators.operate` method at the base of the
|
||||
:class:`.Comparator` hierarchy to return a query-transforming
|
||||
callable, which then runs the given comparison operation in a
|
||||
particular context. Such as, in the example above, the ``operate``
|
||||
method is called, given the :attr:`.Operators.eq` callable as well as
|
||||
the right side of the comparison ``Node(id=5)``. A function
|
||||
``transform`` is then returned which will transform a :class:`.Query`
|
||||
first to join to ``Node.parent``, then to compare ``parent_alias``
|
||||
using :attr:`.Operators.eq` against the left and right sides, passing
|
||||
into :class:`.Query.filter`:
|
||||
|
||||
.. sourcecode:: pycon+sql
|
||||
|
||||
@@ -605,15 +629,43 @@ While it's only recommended for advanced and/or patient developers,
|
||||
there's probably a whole lot of amazing things it can be used for.
|
||||
|
||||
"""
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.orm import attributes, interfaces
|
||||
from .. import util
|
||||
from ..orm import attributes, interfaces
|
||||
|
||||
class hybrid_method(object):
|
||||
HYBRID_METHOD = util.symbol('HYBRID_METHOD')
|
||||
"""Symbol indicating an :class:`_InspectionAttr` that's
|
||||
of type :class:`.hybrid_method`.
|
||||
|
||||
Is assigned to the :attr:`._InspectionAttr.extension_type`
|
||||
attibute.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`.Mapper.all_orm_attributes`
|
||||
|
||||
"""
|
||||
|
||||
HYBRID_PROPERTY = util.symbol('HYBRID_PROPERTY')
|
||||
"""Symbol indicating an :class:`_InspectionAttr` that's
|
||||
of type :class:`.hybrid_method`.
|
||||
|
||||
Is assigned to the :attr:`._InspectionAttr.extension_type`
|
||||
attibute.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`.Mapper.all_orm_attributes`
|
||||
|
||||
"""
|
||||
|
||||
class hybrid_method(interfaces._InspectionAttr):
|
||||
"""A decorator which allows definition of a Python object method with both
|
||||
instance-level and class-level behavior.
|
||||
|
||||
"""
|
||||
|
||||
is_attribute = True
|
||||
extension_type = HYBRID_METHOD
|
||||
|
||||
def __init__(self, func, expr=None):
|
||||
"""Create a new :class:`.hybrid_method`.
|
||||
@@ -642,17 +694,22 @@ class hybrid_method(object):
|
||||
return self.func.__get__(instance, owner)
|
||||
|
||||
def expression(self, expr):
|
||||
"""Provide a modifying decorator that defines a SQL-expression producing method."""
|
||||
"""Provide a modifying decorator that defines a
|
||||
SQL-expression producing method."""
|
||||
|
||||
self.expr = expr
|
||||
return self
|
||||
|
||||
class hybrid_property(object):
|
||||
|
||||
class hybrid_property(interfaces._InspectionAttr):
|
||||
"""A decorator which allows definition of a Python descriptor with both
|
||||
instance-level and class-level behavior.
|
||||
|
||||
"""
|
||||
|
||||
is_attribute = True
|
||||
extension_type = HYBRID_PROPERTY
|
||||
|
||||
def __init__(self, fget, fset=None, fdel=None, expr=None):
|
||||
"""Create a new :class:`.hybrid_property`.
|
||||
|
||||
@@ -699,19 +756,22 @@ class hybrid_property(object):
|
||||
return self
|
||||
|
||||
def deleter(self, fdel):
|
||||
"""Provide a modifying decorator that defines a value-deletion method."""
|
||||
"""Provide a modifying decorator that defines a
|
||||
value-deletion method."""
|
||||
|
||||
self.fdel = fdel
|
||||
return self
|
||||
|
||||
def expression(self, expr):
|
||||
"""Provide a modifying decorator that defines a SQL-expression producing method."""
|
||||
"""Provide a modifying decorator that defines a SQL-expression
|
||||
producing method."""
|
||||
|
||||
self.expr = expr
|
||||
return self
|
||||
|
||||
def comparator(self, comparator):
|
||||
"""Provide a modifying decorator that defines a custom comparator producing method.
|
||||
"""Provide a modifying decorator that defines a custom
|
||||
comparator producing method.
|
||||
|
||||
The return value of the decorated method should be an instance of
|
||||
:class:`~.hybrid.Comparator`.
|
||||
@@ -720,6 +780,7 @@ class hybrid_property(object):
|
||||
|
||||
proxy_attr = attributes.\
|
||||
create_proxied_attribute(self)
|
||||
|
||||
def expr(owner):
|
||||
return proxy_attr(owner, self.__name__, self, comparator(owner))
|
||||
self.expr = expr
|
||||
@@ -727,9 +788,11 @@ class hybrid_property(object):
|
||||
|
||||
|
||||
class Comparator(interfaces.PropComparator):
|
||||
"""A helper class that allows easy construction of custom :class:`~.orm.interfaces.PropComparator`
|
||||
"""A helper class that allows easy construction of custom
|
||||
:class:`~.orm.interfaces.PropComparator`
|
||||
classes for usage with hybrids."""
|
||||
|
||||
property = None
|
||||
|
||||
def __init__(self, expression):
|
||||
self.expression = expression
|
||||
@@ -740,8 +803,6 @@ class Comparator(interfaces.PropComparator):
|
||||
expr = expr.__clause_element__()
|
||||
return expr
|
||||
|
||||
def adapted(self, adapter):
|
||||
def adapt_to_entity(self, adapt_to_entity):
|
||||
# interesting....
|
||||
return self
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
"""Extensible class instrumentation.
|
||||
|
||||
The :mod:`sqlalchemy.ext.instrumentation` package provides for alternate
|
||||
systems of class instrumentation within the ORM. Class instrumentation
|
||||
refers to how the ORM places attributes on the class which maintain
|
||||
data and track changes to that data, as well as event hooks installed
|
||||
on the class.
|
||||
|
||||
.. note::
|
||||
The extension package is provided for the benefit of integration
|
||||
with other object management packages, which already perform
|
||||
their own instrumentation. It is not intended for general use.
|
||||
|
||||
For examples of how the instrumentation extension is used,
|
||||
see the example :ref:`examples_instrumentation`.
|
||||
|
||||
.. versionchanged:: 0.8
|
||||
The :mod:`sqlalchemy.orm.instrumentation` was split out so
|
||||
that all functionality having to do with non-standard
|
||||
instrumentation was moved out to :mod:`sqlalchemy.ext.instrumentation`.
|
||||
When imported, the module installs itself within
|
||||
:mod:`sqlalchemy.orm.instrumentation` so that it
|
||||
takes effect, including recognition of
|
||||
``__sa_instrumentation_manager__`` on mapped classes, as
|
||||
well :data:`.instrumentation_finders`
|
||||
being used to determine class instrumentation resolution.
|
||||
|
||||
"""
|
||||
from ..orm import instrumentation as orm_instrumentation
|
||||
from ..orm.instrumentation import (
|
||||
ClassManager, InstrumentationFactory, _default_state_getter,
|
||||
_default_dict_getter, _default_manager_getter
|
||||
)
|
||||
from ..orm import attributes, collections, base as orm_base
|
||||
from .. import util
|
||||
from ..orm import exc as orm_exc
|
||||
import weakref
|
||||
|
||||
INSTRUMENTATION_MANAGER = '__sa_instrumentation_manager__'
|
||||
"""Attribute, elects custom instrumentation when present on a mapped class.
|
||||
|
||||
Allows a class to specify a slightly or wildly different technique for
|
||||
tracking changes made to mapped attributes and collections.
|
||||
|
||||
Only one instrumentation implementation is allowed in a given object
|
||||
inheritance hierarchy.
|
||||
|
||||
The value of this attribute must be a callable and will be passed a class
|
||||
object. The callable must return one of:
|
||||
|
||||
- An instance of an InstrumentationManager or subclass
|
||||
- An object implementing all or some of InstrumentationManager (TODO)
|
||||
- A dictionary of callables, implementing all or some of the above (TODO)
|
||||
- An instance of a ClassManager or subclass
|
||||
|
||||
This attribute is consulted by SQLAlchemy instrumentation
|
||||
resolution, once the :mod:`sqlalchemy.ext.instrumentation` module
|
||||
has been imported. If custom finders are installed in the global
|
||||
instrumentation_finders list, they may or may not choose to honor this
|
||||
attribute.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def find_native_user_instrumentation_hook(cls):
|
||||
"""Find user-specified instrumentation management for a class."""
|
||||
return getattr(cls, INSTRUMENTATION_MANAGER, None)
|
||||
|
||||
instrumentation_finders = [find_native_user_instrumentation_hook]
|
||||
"""An extensible sequence of callables which return instrumentation
|
||||
implementations
|
||||
|
||||
When a class is registered, each callable will be passed a class object.
|
||||
If None is returned, the
|
||||
next finder in the sequence is consulted. Otherwise the return must be an
|
||||
instrumentation factory that follows the same guidelines as
|
||||
sqlalchemy.ext.instrumentation.INSTRUMENTATION_MANAGER.
|
||||
|
||||
By default, the only finder is find_native_user_instrumentation_hook, which
|
||||
searches for INSTRUMENTATION_MANAGER. If all finders return None, standard
|
||||
ClassManager instrumentation is used.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ExtendedInstrumentationRegistry(InstrumentationFactory):
|
||||
"""Extends :class:`.InstrumentationFactory` with additional
|
||||
bookkeeping, to accommodate multiple types of
|
||||
class managers.
|
||||
|
||||
"""
|
||||
_manager_finders = weakref.WeakKeyDictionary()
|
||||
_state_finders = weakref.WeakKeyDictionary()
|
||||
_dict_finders = weakref.WeakKeyDictionary()
|
||||
_extended = False
|
||||
|
||||
def _locate_extended_factory(self, class_):
|
||||
for finder in instrumentation_finders:
|
||||
factory = finder(class_)
|
||||
if factory is not None:
|
||||
manager = self._extended_class_manager(class_, factory)
|
||||
return manager, factory
|
||||
else:
|
||||
return None, None
|
||||
|
||||
def _check_conflicts(self, class_, factory):
|
||||
existing_factories = self._collect_management_factories_for(class_).\
|
||||
difference([factory])
|
||||
if existing_factories:
|
||||
raise TypeError(
|
||||
"multiple instrumentation implementations specified "
|
||||
"in %s inheritance hierarchy: %r" % (
|
||||
class_.__name__, list(existing_factories)))
|
||||
|
||||
def _extended_class_manager(self, class_, factory):
|
||||
manager = factory(class_)
|
||||
if not isinstance(manager, ClassManager):
|
||||
manager = _ClassInstrumentationAdapter(class_, manager)
|
||||
|
||||
if factory != ClassManager and not self._extended:
|
||||
# somebody invoked a custom ClassManager.
|
||||
# reinstall global "getter" functions with the more
|
||||
# expensive ones.
|
||||
self._extended = True
|
||||
_install_instrumented_lookups()
|
||||
|
||||
self._manager_finders[class_] = manager.manager_getter()
|
||||
self._state_finders[class_] = manager.state_getter()
|
||||
self._dict_finders[class_] = manager.dict_getter()
|
||||
return manager
|
||||
|
||||
def _collect_management_factories_for(self, cls):
|
||||
"""Return a collection of factories in play or specified for a
|
||||
hierarchy.
|
||||
|
||||
Traverses the entire inheritance graph of a cls and returns a
|
||||
collection of instrumentation factories for those classes. Factories
|
||||
are extracted from active ClassManagers, if available, otherwise
|
||||
instrumentation_finders is consulted.
|
||||
|
||||
"""
|
||||
hierarchy = util.class_hierarchy(cls)
|
||||
factories = set()
|
||||
for member in hierarchy:
|
||||
manager = self.manager_of_class(member)
|
||||
if manager is not None:
|
||||
factories.add(manager.factory)
|
||||
else:
|
||||
for finder in instrumentation_finders:
|
||||
factory = finder(member)
|
||||
if factory is not None:
|
||||
break
|
||||
else:
|
||||
factory = None
|
||||
factories.add(factory)
|
||||
factories.discard(None)
|
||||
return factories
|
||||
|
||||
def unregister(self, class_):
|
||||
if class_ in self._manager_finders:
|
||||
del self._manager_finders[class_]
|
||||
del self._state_finders[class_]
|
||||
del self._dict_finders[class_]
|
||||
super(ExtendedInstrumentationRegistry, self).unregister(class_)
|
||||
|
||||
def manager_of_class(self, cls):
|
||||
if cls is None:
|
||||
return None
|
||||
return self._manager_finders.get(cls, _default_manager_getter)(cls)
|
||||
|
||||
def state_of(self, instance):
|
||||
if instance is None:
|
||||
raise AttributeError("None has no persistent state.")
|
||||
return self._state_finders.get(
|
||||
instance.__class__, _default_state_getter)(instance)
|
||||
|
||||
def dict_of(self, instance):
|
||||
if instance is None:
|
||||
raise AttributeError("None has no persistent state.")
|
||||
return self._dict_finders.get(
|
||||
instance.__class__, _default_dict_getter)(instance)
|
||||
|
||||
|
||||
orm_instrumentation._instrumentation_factory = \
|
||||
_instrumentation_factory = ExtendedInstrumentationRegistry()
|
||||
orm_instrumentation.instrumentation_finders = instrumentation_finders
|
||||
|
||||
|
||||
class InstrumentationManager(object):
|
||||
"""User-defined class instrumentation extension.
|
||||
|
||||
:class:`.InstrumentationManager` can be subclassed in order
|
||||
to change
|
||||
how class instrumentation proceeds. This class exists for
|
||||
the purposes of integration with other object management
|
||||
frameworks which would like to entirely modify the
|
||||
instrumentation methodology of the ORM, and is not intended
|
||||
for regular usage. For interception of class instrumentation
|
||||
events, see :class:`.InstrumentationEvents`.
|
||||
|
||||
The API for this class should be considered as semi-stable,
|
||||
and may change slightly with new releases.
|
||||
|
||||
.. versionchanged:: 0.8
|
||||
:class:`.InstrumentationManager` was moved from
|
||||
:mod:`sqlalchemy.orm.instrumentation` to
|
||||
:mod:`sqlalchemy.ext.instrumentation`.
|
||||
|
||||
"""
|
||||
|
||||
# r4361 added a mandatory (cls) constructor to this interface.
|
||||
# given that, perhaps class_ should be dropped from all of these
|
||||
# signatures.
|
||||
|
||||
def __init__(self, class_):
|
||||
pass
|
||||
|
||||
def manage(self, class_, manager):
|
||||
setattr(class_, '_default_class_manager', manager)
|
||||
|
||||
def dispose(self, class_, manager):
|
||||
delattr(class_, '_default_class_manager')
|
||||
|
||||
def manager_getter(self, class_):
|
||||
def get(cls):
|
||||
return cls._default_class_manager
|
||||
return get
|
||||
|
||||
def instrument_attribute(self, class_, key, inst):
|
||||
pass
|
||||
|
||||
def post_configure_attribute(self, class_, key, inst):
|
||||
pass
|
||||
|
||||
def install_descriptor(self, class_, key, inst):
|
||||
setattr(class_, key, inst)
|
||||
|
||||
def uninstall_descriptor(self, class_, key):
|
||||
delattr(class_, key)
|
||||
|
||||
def install_member(self, class_, key, implementation):
|
||||
setattr(class_, key, implementation)
|
||||
|
||||
def uninstall_member(self, class_, key):
|
||||
delattr(class_, key)
|
||||
|
||||
def instrument_collection_class(self, class_, key, collection_class):
|
||||
return collections.prepare_instrumentation(collection_class)
|
||||
|
||||
def get_instance_dict(self, class_, instance):
|
||||
return instance.__dict__
|
||||
|
||||
def initialize_instance_dict(self, class_, instance):
|
||||
pass
|
||||
|
||||
def install_state(self, class_, instance, state):
|
||||
setattr(instance, '_default_state', state)
|
||||
|
||||
def remove_state(self, class_, instance):
|
||||
delattr(instance, '_default_state')
|
||||
|
||||
def state_getter(self, class_):
|
||||
return lambda instance: getattr(instance, '_default_state')
|
||||
|
||||
def dict_getter(self, class_):
|
||||
return lambda inst: self.get_instance_dict(class_, inst)
|
||||
|
||||
|
||||
class _ClassInstrumentationAdapter(ClassManager):
|
||||
"""Adapts a user-defined InstrumentationManager to a ClassManager."""
|
||||
|
||||
def __init__(self, class_, override):
|
||||
self._adapted = override
|
||||
self._get_state = self._adapted.state_getter(class_)
|
||||
self._get_dict = self._adapted.dict_getter(class_)
|
||||
|
||||
ClassManager.__init__(self, class_)
|
||||
|
||||
def manage(self):
|
||||
self._adapted.manage(self.class_, self)
|
||||
|
||||
def dispose(self):
|
||||
self._adapted.dispose(self.class_)
|
||||
|
||||
def manager_getter(self):
|
||||
return self._adapted.manager_getter(self.class_)
|
||||
|
||||
def instrument_attribute(self, key, inst, propagated=False):
|
||||
ClassManager.instrument_attribute(self, key, inst, propagated)
|
||||
if not propagated:
|
||||
self._adapted.instrument_attribute(self.class_, key, inst)
|
||||
|
||||
def post_configure_attribute(self, key):
|
||||
super(_ClassInstrumentationAdapter, self).post_configure_attribute(key)
|
||||
self._adapted.post_configure_attribute(self.class_, key, self[key])
|
||||
|
||||
def install_descriptor(self, key, inst):
|
||||
self._adapted.install_descriptor(self.class_, key, inst)
|
||||
|
||||
def uninstall_descriptor(self, key):
|
||||
self._adapted.uninstall_descriptor(self.class_, key)
|
||||
|
||||
def install_member(self, key, implementation):
|
||||
self._adapted.install_member(self.class_, key, implementation)
|
||||
|
||||
def uninstall_member(self, key):
|
||||
self._adapted.uninstall_member(self.class_, key)
|
||||
|
||||
def instrument_collection_class(self, key, collection_class):
|
||||
return self._adapted.instrument_collection_class(
|
||||
self.class_, key, collection_class)
|
||||
|
||||
def initialize_collection(self, key, state, factory):
|
||||
delegate = getattr(self._adapted, 'initialize_collection', None)
|
||||
if delegate:
|
||||
return delegate(key, state, factory)
|
||||
else:
|
||||
return ClassManager.initialize_collection(self, key,
|
||||
state, factory)
|
||||
|
||||
def new_instance(self, state=None):
|
||||
instance = self.class_.__new__(self.class_)
|
||||
self.setup_instance(instance, state)
|
||||
return instance
|
||||
|
||||
def _new_state_if_none(self, instance):
|
||||
"""Install a default InstanceState if none is present.
|
||||
|
||||
A private convenience method used by the __init__ decorator.
|
||||
"""
|
||||
if self.has_state(instance):
|
||||
return False
|
||||
else:
|
||||
return self.setup_instance(instance)
|
||||
|
||||
def setup_instance(self, instance, state=None):
|
||||
self._adapted.initialize_instance_dict(self.class_, instance)
|
||||
|
||||
if state is None:
|
||||
state = self._state_constructor(instance, self)
|
||||
|
||||
# the given instance is assumed to have no state
|
||||
self._adapted.install_state(self.class_, instance, state)
|
||||
return state
|
||||
|
||||
def teardown_instance(self, instance):
|
||||
self._adapted.remove_state(self.class_, instance)
|
||||
|
||||
def has_state(self, instance):
|
||||
try:
|
||||
self._get_state(instance)
|
||||
except orm_exc.NO_STATE:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def state_getter(self):
|
||||
return self._get_state
|
||||
|
||||
def dict_getter(self):
|
||||
return self._get_dict
|
||||
|
||||
|
||||
def _install_instrumented_lookups():
|
||||
"""Replace global class/object management functions
|
||||
with ExtendedInstrumentationRegistry implementations, which
|
||||
allow multiple types of class managers to be present,
|
||||
at the cost of performance.
|
||||
|
||||
This function is called only by ExtendedInstrumentationRegistry
|
||||
and unit tests specific to this behavior.
|
||||
|
||||
The _reinstall_default_lookups() function can be called
|
||||
after this one to re-establish the default functions.
|
||||
|
||||
"""
|
||||
_install_lookups(
|
||||
dict(
|
||||
instance_state=_instrumentation_factory.state_of,
|
||||
instance_dict=_instrumentation_factory.dict_of,
|
||||
manager_of_class=_instrumentation_factory.manager_of_class
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _reinstall_default_lookups():
|
||||
"""Restore simplified lookups."""
|
||||
_install_lookups(
|
||||
dict(
|
||||
instance_state=_default_state_getter,
|
||||
instance_dict=_default_dict_getter,
|
||||
manager_of_class=_default_manager_getter
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _install_lookups(lookups):
|
||||
global instance_state, instance_dict, manager_of_class
|
||||
instance_state = lookups['instance_state']
|
||||
instance_dict = lookups['instance_dict']
|
||||
manager_of_class = lookups['manager_of_class']
|
||||
orm_base.instance_state = attributes.instance_state = \
|
||||
orm_instrumentation.instance_state = instance_state
|
||||
orm_base.instance_dict = attributes.instance_dict = \
|
||||
orm_instrumentation.instance_dict = instance_dict
|
||||
orm_base.manager_of_class = attributes.manager_of_class = \
|
||||
orm_instrumentation.manager_of_class = manager_of_class
|
||||
+116
-78
@@ -1,5 +1,5 @@
|
||||
# ext/mutable.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -7,13 +7,9 @@
|
||||
"""Provide support for tracking of in-place changes to scalar values,
|
||||
which are propagated into ORM change events on owning parent objects.
|
||||
|
||||
The :mod:`sqlalchemy.ext.mutable` extension replaces SQLAlchemy's legacy approach to in-place
|
||||
mutations of scalar values, established by the :class:`.types.MutableType`
|
||||
class as well as the ``mutable=True`` type flag, with a system that allows
|
||||
change events to be propagated from the value to the owning parent, thereby
|
||||
removing the need for the ORM to maintain copies of values as well as the very
|
||||
expensive requirement of scanning through all "mutable" values on each flush
|
||||
call, looking for changes.
|
||||
.. versionadded:: 0.7 :mod:`sqlalchemy.ext.mutable` replaces SQLAlchemy's
|
||||
legacy approach to in-place mutations of scalar values; see
|
||||
:ref:`07_migration_mutation_extension`.
|
||||
|
||||
.. _mutable_scalars:
|
||||
|
||||
@@ -43,27 +39,27 @@ JSON strings before being persisted::
|
||||
value = json.loads(value)
|
||||
return value
|
||||
|
||||
The usage of ``json`` is only for the purposes of example. The :mod:`sqlalchemy.ext.mutable`
|
||||
extension can be used
|
||||
The usage of ``json`` is only for the purposes of example. The
|
||||
:mod:`sqlalchemy.ext.mutable` extension can be used
|
||||
with any type whose target Python type may be mutable, including
|
||||
:class:`.PickleType`, :class:`.postgresql.ARRAY`, etc.
|
||||
|
||||
When using the :mod:`sqlalchemy.ext.mutable` extension, the value itself
|
||||
tracks all parents which reference it. Here we will replace the usage
|
||||
of plain Python dictionaries with a dict subclass that implements
|
||||
the :class:`.Mutable` mixin::
|
||||
tracks all parents which reference it. Below, we illustrate the a simple
|
||||
version of the :class:`.MutableDict` dictionary object, which applies
|
||||
the :class:`.Mutable` mixin to a plain Python dictionary::
|
||||
|
||||
import collections
|
||||
from sqlalchemy.ext.mutable import Mutable
|
||||
|
||||
class MutationDict(Mutable, dict):
|
||||
class MutableDict(Mutable, dict):
|
||||
@classmethod
|
||||
def coerce(cls, key, value):
|
||||
"Convert plain dictionaries to MutationDict."
|
||||
"Convert plain dictionaries to MutableDict."
|
||||
|
||||
if not isinstance(value, MutationDict):
|
||||
if not isinstance(value, MutableDict):
|
||||
if isinstance(value, dict):
|
||||
return MutationDict(value)
|
||||
return MutableDict(value)
|
||||
|
||||
# this call will raise ValueError
|
||||
return Mutable.coerce(key, value)
|
||||
@@ -84,23 +80,23 @@ the :class:`.Mutable` mixin::
|
||||
|
||||
The above dictionary class takes the approach of subclassing the Python
|
||||
built-in ``dict`` to produce a dict
|
||||
subclass which routes all mutation events through ``__setitem__``. There are
|
||||
many variants on this approach, such as subclassing ``UserDict.UserDict``,
|
||||
the newer ``collections.MutableMapping``, etc. The part that's important to this
|
||||
example is that the :meth:`.Mutable.changed` method is called whenever an in-place change to the
|
||||
datastructure takes place.
|
||||
subclass which routes all mutation events through ``__setitem__``. There are
|
||||
variants on this approach, such as subclassing ``UserDict.UserDict`` or
|
||||
``collections.MutableMapping``; the part that's important to this example is
|
||||
that the :meth:`.Mutable.changed` method is called whenever an in-place
|
||||
change to the datastructure takes place.
|
||||
|
||||
We also redefine the :meth:`.Mutable.coerce` method which will be used to
|
||||
convert any values that are not instances of ``MutationDict``, such
|
||||
convert any values that are not instances of ``MutableDict``, such
|
||||
as the plain dictionaries returned by the ``json`` module, into the
|
||||
appropriate type. Defining this method is optional; we could just as well created our
|
||||
``JSONEncodedDict`` such that it always returns an instance of ``MutationDict``,
|
||||
and additionally ensured that all calling code uses ``MutationDict``
|
||||
explicitly. When :meth:`.Mutable.coerce` is not overridden, any values
|
||||
applied to a parent object which are not instances of the mutable type
|
||||
will raise a ``ValueError``.
|
||||
appropriate type. Defining this method is optional; we could just as well
|
||||
created our ``JSONEncodedDict`` such that it always returns an instance
|
||||
of ``MutableDict``, and additionally ensured that all calling code
|
||||
uses ``MutableDict`` explicitly. When :meth:`.Mutable.coerce` is not
|
||||
overridden, any values applied to a parent object which are not instances
|
||||
of the mutable type will raise a ``ValueError``.
|
||||
|
||||
Our new ``MutationDict`` type offers a class method
|
||||
Our new ``MutableDict`` type offers a class method
|
||||
:meth:`~.Mutable.as_mutable` which we can use within column metadata
|
||||
to associate with types. This method grabs the given type object or
|
||||
class and associates a listener that will detect all future mappings
|
||||
@@ -111,7 +107,7 @@ attribute. Such as, with classical table metadata::
|
||||
|
||||
my_data = Table('my_data', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('data', MutationDict.as_mutable(JSONEncodedDict))
|
||||
Column('data', MutableDict.as_mutable(JSONEncodedDict))
|
||||
)
|
||||
|
||||
Above, :meth:`~.Mutable.as_mutable` returns an instance of ``JSONEncodedDict``
|
||||
@@ -139,7 +135,7 @@ There's no difference in usage when using declarative::
|
||||
class MyDataClass(Base):
|
||||
__tablename__ = 'my_data'
|
||||
id = Column(Integer, primary_key=True)
|
||||
data = Column(MutationDict.as_mutable(JSONEncodedDict))
|
||||
data = Column(MutableDict.as_mutable(JSONEncodedDict))
|
||||
|
||||
Any in-place changes to the ``MyDataClass.data`` member
|
||||
will flag the attribute as "dirty" on the parent object::
|
||||
@@ -155,13 +151,14 @@ will flag the attribute as "dirty" on the parent object::
|
||||
>>> assert m1 in sess.dirty
|
||||
True
|
||||
|
||||
The ``MutationDict`` can be associated with all future instances
|
||||
of ``JSONEncodedDict`` in one step, using :meth:`~.Mutable.associate_with`. This
|
||||
is similar to :meth:`~.Mutable.as_mutable` except it will intercept
|
||||
all occurrences of ``MutationDict`` in all mappings unconditionally, without
|
||||
The ``MutableDict`` can be associated with all future instances
|
||||
of ``JSONEncodedDict`` in one step, using
|
||||
:meth:`~.Mutable.associate_with`. This is similar to
|
||||
:meth:`~.Mutable.as_mutable` except it will intercept all occurrences
|
||||
of ``MutableDict`` in all mappings unconditionally, without
|
||||
the need to declare it individually::
|
||||
|
||||
MutationDict.associate_with(JSONEncodedDict)
|
||||
MutableDict.associate_with(JSONEncodedDict)
|
||||
|
||||
class MyDataClass(Base):
|
||||
__tablename__ = 'my_data'
|
||||
@@ -181,7 +178,7 @@ callbacks. In our case, this is a good thing, since if this dictionary were
|
||||
picklable, it could lead to an excessively large pickle size for our value
|
||||
objects that are pickled by themselves outside of the context of the parent.
|
||||
The developer responsibility here is only to provide a ``__getstate__`` method
|
||||
that excludes the :meth:`~.MutableBase._parents` collection from the pickle
|
||||
that excludes the :meth:`~MutableBase._parents` collection from the pickle
|
||||
stream::
|
||||
|
||||
class MyMutableType(Mutable):
|
||||
@@ -193,7 +190,7 @@ stream::
|
||||
With our dictionary example, we need to return the contents of the dict itself
|
||||
(and also restore them on __setstate__)::
|
||||
|
||||
class MutationDict(Mutable, dict):
|
||||
class MutableDict(Mutable, dict):
|
||||
# ....
|
||||
|
||||
def __getstate__(self):
|
||||
@@ -331,7 +328,7 @@ Supporting Pickling
|
||||
|
||||
As is the case with :class:`.Mutable`, the :class:`.MutableComposite` helper
|
||||
class uses a ``weakref.WeakKeyDictionary`` available via the
|
||||
:meth:`.MutableBase._parents` attribute which isn't picklable. If we need to
|
||||
:meth:`MutableBase._parents` attribute which isn't picklable. If we need to
|
||||
pickle instances of ``Point`` or its owning class ``Vertex``, we at least need
|
||||
to define a ``__getstate__`` that doesn't include the ``_parents`` dictionary.
|
||||
Below we define both a ``__getstate__`` and a ``__setstate__`` that package up
|
||||
@@ -348,17 +345,21 @@ the minimal form of our ``Point`` class::
|
||||
|
||||
As with :class:`.Mutable`, the :class:`.MutableComposite` augments the
|
||||
pickling process of the parent's object-relational state so that the
|
||||
:meth:`.MutableBase._parents` collection is restored to all ``Point`` objects.
|
||||
:meth:`MutableBase._parents` collection is restored to all ``Point`` objects.
|
||||
|
||||
"""
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from sqlalchemy import event, types
|
||||
from sqlalchemy.orm import mapper, object_mapper, Mapper
|
||||
from sqlalchemy.util import memoized_property
|
||||
from ..orm.attributes import flag_modified
|
||||
from .. import event, types
|
||||
from ..orm import mapper, object_mapper, Mapper
|
||||
from ..util import memoized_property
|
||||
import weakref
|
||||
|
||||
|
||||
class MutableBase(object):
|
||||
"""Common base class to :class:`.Mutable` and :class:`.MutableComposite`."""
|
||||
"""Common base class to :class:`.Mutable`
|
||||
and :class:`.MutableComposite`.
|
||||
|
||||
"""
|
||||
|
||||
@memoized_property
|
||||
def _parents(self):
|
||||
@@ -398,7 +399,8 @@ class MutableBase(object):
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
raise ValueError("Attribute '%s' does not accept objects of type %s" % (key, type(value)))
|
||||
msg = "Attribute '%s' does not accept objects of type %s"
|
||||
raise ValueError(msg % (key, type(value)))
|
||||
|
||||
@classmethod
|
||||
def _listen_on_attribute(cls, attribute, coerce, parent_cls):
|
||||
@@ -456,12 +458,17 @@ class MutableBase(object):
|
||||
for val in state_dict['ext.mutable.values']:
|
||||
val._parents[state.obj()] = key
|
||||
|
||||
event.listen(parent_cls, 'load', load,
|
||||
raw=True, propagate=True)
|
||||
event.listen(parent_cls, 'refresh', load,
|
||||
raw=True, propagate=True)
|
||||
event.listen(attribute, 'set', set,
|
||||
raw=True, retval=True, propagate=True)
|
||||
event.listen(parent_cls, 'pickle', pickle,
|
||||
raw=True, propagate=True)
|
||||
event.listen(parent_cls, 'unpickle', unpickle,
|
||||
raw=True, propagate=True)
|
||||
|
||||
event.listen(parent_cls, 'load', load, raw=True, propagate=True)
|
||||
event.listen(parent_cls, 'refresh', load, raw=True, propagate=True)
|
||||
event.listen(attribute, 'set', set, raw=True, retval=True, propagate=True)
|
||||
event.listen(parent_cls, 'pickle', pickle, raw=True, propagate=True)
|
||||
event.listen(parent_cls, 'unpickle', unpickle, raw=True, propagate=True)
|
||||
|
||||
class Mutable(MutableBase):
|
||||
"""Mixin that defines transparent propagation of change
|
||||
@@ -490,23 +497,23 @@ class Mutable(MutableBase):
|
||||
"""Associate this wrapper with all future mapped columns
|
||||
of the given type.
|
||||
|
||||
This is a convenience method that calls ``associate_with_attribute`` automatically.
|
||||
This is a convenience method that calls
|
||||
``associate_with_attribute`` automatically.
|
||||
|
||||
.. warning::
|
||||
|
||||
The listeners established by this method are *global*
|
||||
to all mappers, and are *not* garbage collected. Only use
|
||||
:meth:`.associate_with` for types that are permanent to an application,
|
||||
not with ad-hoc types else this will cause unbounded growth
|
||||
in memory usage.
|
||||
:meth:`.associate_with` for types that are permanent to an
|
||||
application, not with ad-hoc types else this will cause unbounded
|
||||
growth in memory usage.
|
||||
|
||||
"""
|
||||
|
||||
def listen_for_type(mapper, class_):
|
||||
for prop in mapper.iterate_properties:
|
||||
if hasattr(prop, 'columns'):
|
||||
if isinstance(prop.columns[0].type, sqltype):
|
||||
cls.associate_with_attribute(getattr(class_, prop.key))
|
||||
for prop in mapper.column_attrs:
|
||||
if isinstance(prop.columns[0].type, sqltype):
|
||||
cls.associate_with_attribute(getattr(class_, prop.key))
|
||||
|
||||
event.listen(mapper, 'mapper_configured', listen_for_type)
|
||||
|
||||
@@ -526,12 +533,12 @@ class Mutable(MutableBase):
|
||||
)
|
||||
|
||||
Note that the returned type is always an instance, even if a class
|
||||
is given, and that only columns which are declared specifically with that
|
||||
type instance receive additional instrumentation.
|
||||
is given, and that only columns which are declared specifically with
|
||||
that type instance receive additional instrumentation.
|
||||
|
||||
To associate a particular mutable type with all occurrences of a
|
||||
particular type, use the :meth:`.Mutable.associate_with` classmethod
|
||||
of the particular :meth:`.Mutable` subclass to establish a global
|
||||
of the particular :class:`.Mutable` subclass to establish a global
|
||||
association.
|
||||
|
||||
.. warning::
|
||||
@@ -546,15 +553,16 @@ class Mutable(MutableBase):
|
||||
sqltype = types.to_instance(sqltype)
|
||||
|
||||
def listen_for_type(mapper, class_):
|
||||
for prop in mapper.iterate_properties:
|
||||
if hasattr(prop, 'columns'):
|
||||
if prop.columns[0].type is sqltype:
|
||||
cls.associate_with_attribute(getattr(class_, prop.key))
|
||||
for prop in mapper.column_attrs:
|
||||
if prop.columns[0].type is sqltype:
|
||||
cls.associate_with_attribute(getattr(class_, prop.key))
|
||||
|
||||
event.listen(mapper, 'mapper_configured', listen_for_type)
|
||||
|
||||
return sqltype
|
||||
|
||||
|
||||
|
||||
class MutableComposite(MutableBase):
|
||||
"""Mixin that defines transparent propagation of change
|
||||
events on a SQLAlchemy "composite" object to its
|
||||
@@ -562,14 +570,6 @@ class MutableComposite(MutableBase):
|
||||
|
||||
See the example in :ref:`mutable_composites` for usage information.
|
||||
|
||||
.. warning::
|
||||
|
||||
The listeners established by the :class:`.MutableComposite`
|
||||
class are *global* to all mappers, and are *not* garbage collected. Only use
|
||||
:class:`.MutableComposite` for types that are permanent to an application,
|
||||
not with ad-hoc types else this will cause unbounded growth
|
||||
in memory usage.
|
||||
|
||||
"""
|
||||
|
||||
def changed(self):
|
||||
@@ -583,14 +583,52 @@ class MutableComposite(MutableBase):
|
||||
prop._attribute_keys):
|
||||
setattr(parent, attr_name, value)
|
||||
|
||||
|
||||
def _setup_composite_listener():
|
||||
def _listen_for_type(mapper, class_):
|
||||
for prop in mapper.iterate_properties:
|
||||
if (hasattr(prop, 'composite_class') and
|
||||
issubclass(prop.composite_class, MutableComposite)):
|
||||
isinstance(prop.composite_class, type) and
|
||||
issubclass(prop.composite_class, MutableComposite)):
|
||||
prop.composite_class._listen_on_attribute(
|
||||
getattr(class_, prop.key), False, class_)
|
||||
if not Mapper.dispatch.mapper_configured._contains(Mapper, _listen_for_type):
|
||||
if not event.contains(Mapper, "mapper_configured", _listen_for_type):
|
||||
event.listen(Mapper, 'mapper_configured', _listen_for_type)
|
||||
_setup_composite_listener()
|
||||
|
||||
|
||||
class MutableDict(Mutable, dict):
|
||||
"""A dictionary type that implements :class:`.Mutable`.
|
||||
|
||||
.. versionadded:: 0.8
|
||||
|
||||
"""
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Detect dictionary set events and emit change events."""
|
||||
dict.__setitem__(self, key, value)
|
||||
self.changed()
|
||||
|
||||
def __delitem__(self, key):
|
||||
"""Detect dictionary del events and emit change events."""
|
||||
dict.__delitem__(self, key)
|
||||
self.changed()
|
||||
|
||||
def clear(self):
|
||||
dict.clear(self)
|
||||
self.changed()
|
||||
|
||||
@classmethod
|
||||
def coerce(cls, key, value):
|
||||
"""Convert plain dictionary to MutableDict."""
|
||||
if not isinstance(value, MutableDict):
|
||||
if isinstance(value, dict):
|
||||
return MutableDict(value)
|
||||
return Mutable.coerce(key, value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def __getstate__(self):
|
||||
return dict(self)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.update(state)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ext/orderinglist.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -107,8 +107,8 @@ or some other integer, provide ``count_from=1``.
|
||||
|
||||
|
||||
"""
|
||||
from sqlalchemy.orm.collections import collection
|
||||
from sqlalchemy import util
|
||||
from ..orm.collections import collection
|
||||
from .. import util
|
||||
|
||||
__all__ = ['ordering_list']
|
||||
|
||||
@@ -324,7 +324,7 @@ class OrderingList(list):
|
||||
if stop < 0:
|
||||
stop += len(self)
|
||||
|
||||
for i in xrange(start, stop, step):
|
||||
for i in range(start, stop, step):
|
||||
self.__setitem__(i, entity[i])
|
||||
else:
|
||||
self._order_entity(index, entity, True)
|
||||
@@ -334,7 +334,6 @@ class OrderingList(list):
|
||||
super(OrderingList, self).__delitem__(index)
|
||||
self._reorder()
|
||||
|
||||
# Py2K
|
||||
def __setslice__(self, start, end, values):
|
||||
super(OrderingList, self).__setslice__(start, end, values)
|
||||
self._reorder()
|
||||
@@ -342,13 +341,12 @@ class OrderingList(list):
|
||||
def __delslice__(self, start, end):
|
||||
super(OrderingList, self).__delslice__(start, end)
|
||||
self._reorder()
|
||||
# end Py2K
|
||||
|
||||
def __reduce__(self):
|
||||
return _reconstitute, (self.__class__, self.__dict__, list(self))
|
||||
|
||||
for func_name, func in locals().items():
|
||||
if (util.callable(func) and func.func_name == func_name and
|
||||
for func_name, func in list(locals().items()):
|
||||
if (util.callable(func) and func.__name__ == func_name and
|
||||
not func.__doc__ and hasattr(list, func_name)):
|
||||
func.__doc__ = getattr(list, func_name).__doc__
|
||||
del func_name, func
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ext/serializer.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -39,46 +39,32 @@ The serializer module is only appropriate for query structures. It is not
|
||||
needed for:
|
||||
|
||||
* instances of user-defined classes. These contain no references to engines,
|
||||
sessions or expression constructs in the typical case and can be serialized directly.
|
||||
sessions or expression constructs in the typical case and can be serialized
|
||||
directly.
|
||||
|
||||
* Table metadata that is to be loaded entirely from the serialized structure (i.e. is
|
||||
not already declared in the application). Regular pickle.loads()/dumps() can
|
||||
be used to fully dump any ``MetaData`` object, typically one which was reflected
|
||||
from an existing database at some previous point in time. The serializer module
|
||||
is specifically for the opposite case, where the Table metadata is already present
|
||||
in memory.
|
||||
* Table metadata that is to be loaded entirely from the serialized structure
|
||||
(i.e. is not already declared in the application). Regular
|
||||
pickle.loads()/dumps() can be used to fully dump any ``MetaData`` object,
|
||||
typically one which was reflected from an existing database at some previous
|
||||
point in time. The serializer module is specifically for the opposite case,
|
||||
where the Table metadata is already present in memory.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import class_mapper, Query
|
||||
from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy.orm.mapper import Mapper
|
||||
from sqlalchemy.orm.attributes import QueryableAttribute
|
||||
from sqlalchemy import Table, Column
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.util import pickle
|
||||
from ..orm import class_mapper
|
||||
from ..orm.session import Session
|
||||
from ..orm.mapper import Mapper
|
||||
from ..orm.interfaces import MapperProperty
|
||||
from ..orm.attributes import QueryableAttribute
|
||||
from .. import Table, Column
|
||||
from ..engine import Engine
|
||||
from ..util import pickle, byte_buffer, b64encode, b64decode, text_type
|
||||
import re
|
||||
import base64
|
||||
# Py3K
|
||||
#from io import BytesIO as byte_buffer
|
||||
# Py2K
|
||||
from cStringIO import StringIO as byte_buffer
|
||||
# end Py2K
|
||||
|
||||
# Py3K
|
||||
#def b64encode(x):
|
||||
# return base64.b64encode(x).decode('ascii')
|
||||
#def b64decode(x):
|
||||
# return base64.b64decode(x.encode('ascii'))
|
||||
# Py2K
|
||||
b64encode = base64.b64encode
|
||||
b64decode = base64.b64decode
|
||||
# end Py2K
|
||||
|
||||
__all__ = ['Serializer', 'Deserializer', 'dumps', 'loads']
|
||||
|
||||
|
||||
|
||||
def Serializer(*args, **kw):
|
||||
pickler = pickle.Pickler(*args, **kw)
|
||||
|
||||
@@ -90,10 +76,13 @@ def Serializer(*args, **kw):
|
||||
id = "attribute:" + key + ":" + b64encode(pickle.dumps(cls))
|
||||
elif isinstance(obj, Mapper) and not obj.non_primary:
|
||||
id = "mapper:" + b64encode(pickle.dumps(obj.class_))
|
||||
elif isinstance(obj, MapperProperty) and not obj.parent.non_primary:
|
||||
id = "mapperprop:" + b64encode(pickle.dumps(obj.parent.class_)) + \
|
||||
":" + obj.key
|
||||
elif isinstance(obj, Table):
|
||||
id = "table:" + str(obj)
|
||||
id = "table:" + text_type(obj.key)
|
||||
elif isinstance(obj, Column) and isinstance(obj.table, Table):
|
||||
id = "column:" + str(obj.table) + ":" + obj.key
|
||||
id = "column:" + text_type(obj.table.key) + ":" + text_type(obj.key)
|
||||
elif isinstance(obj, Session):
|
||||
id = "session:"
|
||||
elif isinstance(obj, Engine):
|
||||
@@ -105,7 +94,9 @@ def Serializer(*args, **kw):
|
||||
pickler.persistent_id = persistent_id
|
||||
return pickler
|
||||
|
||||
our_ids = re.compile(r'(mapper|table|column|session|attribute|engine):(.*)')
|
||||
our_ids = re.compile(
|
||||
r'(mapperprop|mapper|table|column|session|attribute|engine):(.*)')
|
||||
|
||||
|
||||
def Deserializer(file, metadata=None, scoped_session=None, engine=None):
|
||||
unpickler = pickle.Unpickler(file)
|
||||
@@ -121,7 +112,7 @@ def Deserializer(file, metadata=None, scoped_session=None, engine=None):
|
||||
return None
|
||||
|
||||
def persistent_load(id):
|
||||
m = our_ids.match(id)
|
||||
m = our_ids.match(text_type(id))
|
||||
if not m:
|
||||
return None
|
||||
else:
|
||||
@@ -133,6 +124,10 @@ def Deserializer(file, metadata=None, scoped_session=None, engine=None):
|
||||
elif type_ == "mapper":
|
||||
cls = pickle.loads(b64decode(args))
|
||||
return class_mapper(cls)
|
||||
elif type_ == "mapperprop":
|
||||
mapper, keyname = args.split(':')
|
||||
cls = pickle.loads(b64decode(mapper))
|
||||
return class_mapper(cls).attrs[keyname]
|
||||
elif type_ == "table":
|
||||
return metadata.tables[args]
|
||||
elif type_ == "column":
|
||||
@@ -147,15 +142,15 @@ def Deserializer(file, metadata=None, scoped_session=None, engine=None):
|
||||
unpickler.persistent_load = persistent_load
|
||||
return unpickler
|
||||
|
||||
|
||||
def dumps(obj, protocol=0):
|
||||
buf = byte_buffer()
|
||||
pickler = Serializer(buf, protocol)
|
||||
pickler.dump(obj)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def loads(data, metadata=None, scoped_session=None, engine=None):
|
||||
buf = byte_buffer(data)
|
||||
unpickler = Deserializer(buf, metadata, scoped_session, engine)
|
||||
return unpickler.load()
|
||||
|
||||
|
||||
|
||||
@@ -1,811 +0,0 @@
|
||||
# ext/sqlsoup.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""
|
||||
|
||||
.. versionchanged:: 0.8
|
||||
SQLSoup is now its own project. Documentation
|
||||
and project status are available at:
|
||||
http://pypi.python.org/pypi/sqlsoup and
|
||||
http://readthedocs.org/docs/sqlsoup\ .
|
||||
SQLSoup will no longer be included with SQLAlchemy.
|
||||
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
SqlSoup provides a convenient way to access existing database
|
||||
tables without having to declare table or mapper classes ahead
|
||||
of time. It is built on top of the SQLAlchemy ORM and provides a
|
||||
super-minimalistic interface to an existing database.
|
||||
|
||||
SqlSoup effectively provides a coarse grained, alternative
|
||||
interface to working with the SQLAlchemy ORM, providing a "self
|
||||
configuring" interface for extremely rudimental operations. It's
|
||||
somewhat akin to a "super novice mode" version of the ORM. While
|
||||
SqlSoup can be very handy, users are strongly encouraged to use
|
||||
the full ORM for non-trivial applications.
|
||||
|
||||
Suppose we have a database with users, books, and loans tables
|
||||
(corresponding to the PyWebOff dataset, if you're curious).
|
||||
|
||||
Creating a SqlSoup gateway is just like creating an SQLAlchemy
|
||||
engine::
|
||||
|
||||
>>> from sqlalchemy.ext.sqlsoup import SqlSoup
|
||||
>>> db = SqlSoup('sqlite:///:memory:')
|
||||
|
||||
or, you can re-use an existing engine::
|
||||
|
||||
>>> db = SqlSoup(engine)
|
||||
|
||||
You can optionally specify a schema within the database for your
|
||||
SqlSoup::
|
||||
|
||||
>>> db.schema = myschemaname
|
||||
|
||||
Loading objects
|
||||
===============
|
||||
|
||||
Loading objects is as easy as this::
|
||||
|
||||
>>> users = db.users.all()
|
||||
>>> users.sort()
|
||||
>>> users
|
||||
[
|
||||
MappedUsers(name=u'Joe Student',email=u'student@example.edu',
|
||||
password=u'student',classname=None,admin=0),
|
||||
MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
|
||||
password=u'basepair',classname=None,admin=1)
|
||||
]
|
||||
|
||||
Of course, letting the database do the sort is better::
|
||||
|
||||
>>> db.users.order_by(db.users.name).all()
|
||||
[
|
||||
MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
|
||||
password=u'basepair',classname=None,admin=1),
|
||||
MappedUsers(name=u'Joe Student',email=u'student@example.edu',
|
||||
password=u'student',classname=None,admin=0)
|
||||
]
|
||||
|
||||
Field access is intuitive::
|
||||
|
||||
>>> users[0].email
|
||||
u'student@example.edu'
|
||||
|
||||
Of course, you don't want to load all users very often. Let's
|
||||
add a WHERE clause. Let's also switch the order_by to DESC while
|
||||
we're at it::
|
||||
|
||||
>>> from sqlalchemy import or_, and_, desc
|
||||
>>> where = or_(db.users.name=='Bhargan Basepair', db.users.email=='student@example.edu')
|
||||
>>> db.users.filter(where).order_by(desc(db.users.name)).all()
|
||||
[
|
||||
MappedUsers(name=u'Joe Student',email=u'student@example.edu',
|
||||
password=u'student',classname=None,admin=0),
|
||||
MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
|
||||
password=u'basepair',classname=None,admin=1)
|
||||
]
|
||||
|
||||
You can also use .first() (to retrieve only the first object
|
||||
from a query) or .one() (like .first when you expect exactly one
|
||||
user -- it will raise an exception if more were returned)::
|
||||
|
||||
>>> db.users.filter(db.users.name=='Bhargan Basepair').one()
|
||||
MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
|
||||
password=u'basepair',classname=None,admin=1)
|
||||
|
||||
Since name is the primary key, this is equivalent to
|
||||
|
||||
>>> db.users.get('Bhargan Basepair')
|
||||
MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
|
||||
password=u'basepair',classname=None,admin=1)
|
||||
|
||||
This is also equivalent to
|
||||
|
||||
>>> db.users.filter_by(name='Bhargan Basepair').one()
|
||||
MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
|
||||
password=u'basepair',classname=None,admin=1)
|
||||
|
||||
filter_by is like filter, but takes kwargs instead of full
|
||||
clause expressions. This makes it more concise for simple
|
||||
queries like this, but you can't do complex queries like the
|
||||
or\_ above or non-equality based comparisons this way.
|
||||
|
||||
Full query documentation
|
||||
------------------------
|
||||
|
||||
Get, filter, filter_by, order_by, limit, and the rest of the
|
||||
query methods are explained in detail in
|
||||
:ref:`ormtutorial_querying`.
|
||||
|
||||
Modifying objects
|
||||
=================
|
||||
|
||||
Modifying objects is intuitive::
|
||||
|
||||
>>> user = _
|
||||
>>> user.email = 'basepair+nospam@example.edu'
|
||||
>>> db.commit()
|
||||
|
||||
(SqlSoup leverages the sophisticated SQLAlchemy unit-of-work
|
||||
code, so multiple updates to a single object will be turned into
|
||||
a single ``UPDATE`` statement when you commit.)
|
||||
|
||||
To finish covering the basics, let's insert a new loan, then
|
||||
delete it::
|
||||
|
||||
>>> book_id = db.books.filter_by(title='Regional Variation in Moss').first().id
|
||||
>>> db.loans.insert(book_id=book_id, user_name=user.name)
|
||||
MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None)
|
||||
|
||||
>>> loan = db.loans.filter_by(book_id=2, user_name='Bhargan Basepair').one()
|
||||
>>> db.delete(loan)
|
||||
>>> db.commit()
|
||||
|
||||
You can also delete rows that have not been loaded as objects.
|
||||
Let's do our insert/delete cycle once more, this time using the
|
||||
loans table's delete method. (For SQLAlchemy experts: note that
|
||||
no flush() call is required since this delete acts at the SQL
|
||||
level, not at the Mapper level.) The same where-clause
|
||||
construction rules apply here as to the select methods::
|
||||
|
||||
>>> db.loans.insert(book_id=book_id, user_name=user.name)
|
||||
MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None)
|
||||
>>> db.loans.delete(db.loans.book_id==2)
|
||||
|
||||
You can similarly update multiple rows at once. This will change the
|
||||
book_id to 1 in all loans whose book_id is 2::
|
||||
|
||||
>>> db.loans.filter_by(db.loans.book_id==2).update({'book_id':1})
|
||||
>>> db.loans.filter_by(book_id=1).all()
|
||||
[MappedLoans(book_id=1,user_name=u'Joe Student',
|
||||
loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
|
||||
|
||||
|
||||
Joins
|
||||
=====
|
||||
|
||||
Occasionally, you will want to pull out a lot of data from related
|
||||
tables all at once. In this situation, it is far more efficient to
|
||||
have the database perform the necessary join. (Here we do not have *a
|
||||
lot of data* but hopefully the concept is still clear.) SQLAlchemy is
|
||||
smart enough to recognize that loans has a foreign key to users, and
|
||||
uses that as the join condition automatically::
|
||||
|
||||
>>> join1 = db.join(db.users, db.loans, isouter=True)
|
||||
>>> join1.filter_by(name='Joe Student').all()
|
||||
[
|
||||
MappedJoin(name=u'Joe Student',email=u'student@example.edu',
|
||||
password=u'student',classname=None,admin=0,book_id=1,
|
||||
user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))
|
||||
]
|
||||
|
||||
If you're unfortunate enough to be using MySQL with the default MyISAM
|
||||
storage engine, you'll have to specify the join condition manually,
|
||||
since MyISAM does not store foreign keys. Here's the same join again,
|
||||
with the join condition explicitly specified::
|
||||
|
||||
>>> db.join(db.users, db.loans, db.users.name==db.loans.user_name, isouter=True)
|
||||
<class 'sqlalchemy.ext.sqlsoup.MappedJoin'>
|
||||
|
||||
You can compose arbitrarily complex joins by combining Join objects
|
||||
with tables or other joins. Here we combine our first join with the
|
||||
books table::
|
||||
|
||||
>>> join2 = db.join(join1, db.books)
|
||||
>>> join2.all()
|
||||
[
|
||||
MappedJoin(name=u'Joe Student',email=u'student@example.edu',
|
||||
password=u'student',classname=None,admin=0,book_id=1,
|
||||
user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0),
|
||||
id=1,title=u'Mustards I Have Known',published_year=u'1989',
|
||||
authors=u'Jones')
|
||||
]
|
||||
|
||||
If you join tables that have an identical column name, wrap your join
|
||||
with `with_labels`, to disambiguate columns with their table name
|
||||
(.c is short for .columns)::
|
||||
|
||||
>>> db.with_labels(join1).c.keys()
|
||||
[u'users_name', u'users_email', u'users_password',
|
||||
u'users_classname', u'users_admin', u'loans_book_id',
|
||||
u'loans_user_name', u'loans_loan_date']
|
||||
|
||||
You can also join directly to a labeled object::
|
||||
|
||||
>>> labeled_loans = db.with_labels(db.loans)
|
||||
>>> db.join(db.users, labeled_loans, isouter=True).c.keys()
|
||||
[u'name', u'email', u'password', u'classname',
|
||||
u'admin', u'loans_book_id', u'loans_user_name', u'loans_loan_date']
|
||||
|
||||
|
||||
Relationships
|
||||
=============
|
||||
|
||||
You can define relationships on SqlSoup classes:
|
||||
|
||||
>>> db.users.relate('loans', db.loans)
|
||||
|
||||
These can then be used like a normal SA property:
|
||||
|
||||
>>> db.users.get('Joe Student').loans
|
||||
[MappedLoans(book_id=1,user_name=u'Joe Student',
|
||||
loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
|
||||
|
||||
>>> db.users.filter(~db.users.loans.any()).all()
|
||||
[MappedUsers(name=u'Bhargan Basepair',
|
||||
email='basepair+nospam@example.edu',
|
||||
password=u'basepair',classname=None,admin=1)]
|
||||
|
||||
relate can take any options that the relationship function
|
||||
accepts in normal mapper definition:
|
||||
|
||||
>>> del db._cache['users']
|
||||
>>> db.users.relate('loans', db.loans, order_by=db.loans.loan_date, cascade='all, delete-orphan')
|
||||
|
||||
Advanced Use
|
||||
============
|
||||
|
||||
Sessions, Transactions and Application Integration
|
||||
---------------------------------------------------
|
||||
|
||||
.. note::
|
||||
|
||||
Please read and understand this section thoroughly
|
||||
before using SqlSoup in any web application.
|
||||
|
||||
SqlSoup uses a ScopedSession to provide thread-local sessions.
|
||||
You can get a reference to the current one like this::
|
||||
|
||||
>>> session = db.session
|
||||
|
||||
The default session is available at the module level in SQLSoup,
|
||||
via::
|
||||
|
||||
>>> from sqlalchemy.ext.sqlsoup import Session
|
||||
|
||||
The configuration of this session is ``autoflush=True``,
|
||||
``autocommit=False``. This means when you work with the SqlSoup
|
||||
object, you need to call ``db.commit()`` in order to have
|
||||
changes persisted. You may also call ``db.rollback()`` to roll
|
||||
things back.
|
||||
|
||||
Since the SqlSoup object's Session automatically enters into a
|
||||
transaction as soon as it's used, it is *essential* that you
|
||||
call ``commit()`` or ``rollback()`` on it when the work within a
|
||||
thread completes. This means all the guidelines for web
|
||||
application integration at :ref:`session_lifespan` must be
|
||||
followed.
|
||||
|
||||
The SqlSoup object can have any session or scoped session
|
||||
configured onto it. This is of key importance when integrating
|
||||
with existing code or frameworks such as Pylons. If your
|
||||
application already has a ``Session`` configured, pass it to
|
||||
your SqlSoup object::
|
||||
|
||||
>>> from myapplication import Session
|
||||
>>> db = SqlSoup(session=Session)
|
||||
|
||||
If the ``Session`` is configured with ``autocommit=True``, use
|
||||
``flush()`` instead of ``commit()`` to persist changes - in this
|
||||
case, the ``Session`` closes out its transaction immediately and
|
||||
no external management is needed. ``rollback()`` is also not
|
||||
available. Configuring a new SQLSoup object in "autocommit" mode
|
||||
looks like::
|
||||
|
||||
>>> from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
>>> db = SqlSoup('sqlite://', session=scoped_session(sessionmaker(autoflush=False, expire_on_commit=False, autocommit=True)))
|
||||
|
||||
|
||||
Mapping arbitrary Selectables
|
||||
-----------------------------
|
||||
|
||||
SqlSoup can map any SQLAlchemy :class:`.Selectable` with the map
|
||||
method. Let's map an :func:`.expression.select` object that uses an aggregate
|
||||
function; we'll use the SQLAlchemy :class:`.Table` that SqlSoup
|
||||
introspected as the basis. (Since we're not mapping to a simple
|
||||
table or join, we need to tell SQLAlchemy how to find the
|
||||
*primary key* which just needs to be unique within the select,
|
||||
and not necessarily correspond to a *real* PK in the database.)::
|
||||
|
||||
>>> from sqlalchemy import select, func
|
||||
>>> b = db.books._table
|
||||
>>> s = select([b.c.published_year, func.count('*').label('n')], from_obj=[b], group_by=[b.c.published_year])
|
||||
>>> s = s.alias('years_with_count')
|
||||
>>> years_with_count = db.map(s, primary_key=[s.c.published_year])
|
||||
>>> years_with_count.filter_by(published_year='1989').all()
|
||||
[MappedBooks(published_year=u'1989',n=1)]
|
||||
|
||||
Obviously if we just wanted to get a list of counts associated with
|
||||
book years once, raw SQL is going to be less work. The advantage of
|
||||
mapping a Select is reusability, both standalone and in Joins. (And if
|
||||
you go to full SQLAlchemy, you can perform mappings like this directly
|
||||
to your object models.)
|
||||
|
||||
An easy way to save mapped selectables like this is to just hang them on
|
||||
your db object::
|
||||
|
||||
>>> db.years_with_count = years_with_count
|
||||
|
||||
Python is flexible like that!
|
||||
|
||||
Raw SQL
|
||||
-------
|
||||
|
||||
SqlSoup works fine with SQLAlchemy's text construct, described
|
||||
in :ref:`sqlexpression_text`. You can also execute textual SQL
|
||||
directly using the `execute()` method, which corresponds to the
|
||||
`execute()` method on the underlying `Session`. Expressions here
|
||||
are expressed like ``text()`` constructs, using named parameters
|
||||
with colons::
|
||||
|
||||
>>> rp = db.execute('select name, email from users where name like :name order by name', name='%Bhargan%')
|
||||
>>> for name, email in rp.fetchall(): print name, email
|
||||
Bhargan Basepair basepair+nospam@example.edu
|
||||
|
||||
Or you can get at the current transaction's connection using
|
||||
`connection()`. This is the raw connection object which can
|
||||
accept any sort of SQL expression or raw SQL string passed to
|
||||
the database::
|
||||
|
||||
>>> conn = db.connection()
|
||||
>>> conn.execute("'select name, email from users where name like ? order by name'", '%Bhargan%')
|
||||
|
||||
Dynamic table names
|
||||
-------------------
|
||||
|
||||
You can load a table whose name is specified at runtime with the
|
||||
entity() method:
|
||||
|
||||
>>> tablename = 'loans'
|
||||
>>> db.entity(tablename) == db.loans
|
||||
True
|
||||
|
||||
entity() also takes an optional schema argument. If none is
|
||||
specified, the default schema is used.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy import Table, MetaData, join
|
||||
from sqlalchemy import schema, sql, util
|
||||
from sqlalchemy.engine.base import Engine
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker, mapper, \
|
||||
class_mapper, relationship, session,\
|
||||
object_session, attributes
|
||||
from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE
|
||||
from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, ArgumentError
|
||||
from sqlalchemy.sql import expression
|
||||
|
||||
|
||||
__all__ = ['PKNotFoundError', 'SqlSoup']
|
||||
|
||||
Session = scoped_session(sessionmaker(autoflush=True, autocommit=False))
|
||||
|
||||
class AutoAdd(MapperExtension):
|
||||
def __init__(self, scoped_session):
|
||||
self.scoped_session = scoped_session
|
||||
|
||||
def instrument_class(self, mapper, class_):
|
||||
class_.__init__ = self._default__init__(mapper)
|
||||
|
||||
def _default__init__(ext, mapper):
|
||||
def __init__(self, **kwargs):
|
||||
for key, value in kwargs.iteritems():
|
||||
setattr(self, key, value)
|
||||
return __init__
|
||||
|
||||
def init_instance(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
session = self.scoped_session()
|
||||
state = attributes.instance_state(instance)
|
||||
session._save_impl(state)
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_failed(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
sess = object_session(instance)
|
||||
if sess:
|
||||
sess.expunge(instance)
|
||||
return EXT_CONTINUE
|
||||
|
||||
class PKNotFoundError(SQLAlchemyError):
|
||||
pass
|
||||
|
||||
def _ddl_error(cls):
|
||||
msg = 'SQLSoup can only modify mapped Tables (found: %s)' \
|
||||
% cls._table.__class__.__name__
|
||||
raise InvalidRequestError(msg)
|
||||
|
||||
# metaclass is necessary to expose class methods with getattr, e.g.
|
||||
# we want to pass db.users.select through to users._mapper.select
|
||||
class SelectableClassType(type):
|
||||
def insert(cls, **kwargs):
|
||||
_ddl_error(cls)
|
||||
|
||||
def __clause_element__(cls):
|
||||
return cls._table
|
||||
|
||||
def __getattr__(cls, attr):
|
||||
if attr == '_query':
|
||||
# called during mapper init
|
||||
raise AttributeError()
|
||||
return getattr(cls._query, attr)
|
||||
|
||||
class TableClassType(SelectableClassType):
|
||||
def insert(cls, **kwargs):
|
||||
o = cls()
|
||||
o.__dict__.update(kwargs)
|
||||
return o
|
||||
|
||||
def relate(cls, propname, *args, **kwargs):
|
||||
class_mapper(cls)._configure_property(propname, relationship(*args, **kwargs))
|
||||
|
||||
def _is_outer_join(selectable):
|
||||
if not isinstance(selectable, sql.Join):
|
||||
return False
|
||||
if selectable.isouter:
|
||||
return True
|
||||
return _is_outer_join(selectable.left) or _is_outer_join(selectable.right)
|
||||
|
||||
def _selectable_name(selectable):
|
||||
if isinstance(selectable, sql.Alias):
|
||||
return _selectable_name(selectable.element)
|
||||
elif isinstance(selectable, sql.Select):
|
||||
return ''.join(_selectable_name(s) for s in selectable.froms)
|
||||
elif isinstance(selectable, schema.Table):
|
||||
return selectable.name.capitalize()
|
||||
else:
|
||||
x = selectable.__class__.__name__
|
||||
if x[0] == '_':
|
||||
x = x[1:]
|
||||
return x
|
||||
|
||||
def _class_for_table(session, engine, selectable, base_cls, mapper_kwargs):
|
||||
selectable = expression._clause_element_as_expr(selectable)
|
||||
mapname = 'Mapped' + _selectable_name(selectable)
|
||||
# Py2K
|
||||
if isinstance(mapname, unicode):
|
||||
engine_encoding = engine.dialect.encoding
|
||||
mapname = mapname.encode(engine_encoding)
|
||||
# end Py2K
|
||||
|
||||
if isinstance(selectable, Table):
|
||||
klass = TableClassType(mapname, (base_cls,), {})
|
||||
else:
|
||||
klass = SelectableClassType(mapname, (base_cls,), {})
|
||||
|
||||
def _compare(self, o):
|
||||
L = list(self.__class__.c.keys())
|
||||
L.sort()
|
||||
t1 = [getattr(self, k) for k in L]
|
||||
try:
|
||||
t2 = [getattr(o, k) for k in L]
|
||||
except AttributeError:
|
||||
raise TypeError('unable to compare with %s' % o.__class__)
|
||||
return t1, t2
|
||||
|
||||
# python2/python3 compatible system of
|
||||
# __cmp__ - __lt__ + __eq__
|
||||
|
||||
def __lt__(self, o):
|
||||
t1, t2 = _compare(self, o)
|
||||
return t1 < t2
|
||||
|
||||
def __eq__(self, o):
|
||||
t1, t2 = _compare(self, o)
|
||||
return t1 == t2
|
||||
|
||||
def __repr__(self):
|
||||
L = ["%s=%r" % (key, getattr(self, key, ''))
|
||||
for key in self.__class__.c.keys()]
|
||||
return '%s(%s)' % (self.__class__.__name__, ','.join(L))
|
||||
|
||||
for m in ['__eq__', '__repr__', '__lt__']:
|
||||
setattr(klass, m, eval(m))
|
||||
klass._table = selectable
|
||||
klass.c = expression.ColumnCollection()
|
||||
mappr = mapper(klass,
|
||||
selectable,
|
||||
extension=AutoAdd(session),
|
||||
**mapper_kwargs)
|
||||
|
||||
for k in mappr.iterate_properties:
|
||||
klass.c[k.key] = k.columns[0]
|
||||
|
||||
klass._query = session.query_property()
|
||||
return klass
|
||||
|
||||
class SqlSoup(object):
|
||||
"""Represent an ORM-wrapped database resource."""
|
||||
|
||||
def __init__(self, engine_or_metadata, base=object, session=None):
|
||||
"""Initialize a new :class:`.SqlSoup`.
|
||||
|
||||
:param engine_or_metadata: a string database URL, :class:`.Engine`
|
||||
or :class:`.MetaData` object to associate with. If the
|
||||
argument is a :class:`.MetaData`, it should be *bound*
|
||||
to an :class:`.Engine`.
|
||||
:param base: a class which will serve as the default class for
|
||||
returned mapped classes. Defaults to ``object``.
|
||||
:param session: a :class:`.ScopedSession` or :class:`.Session` with
|
||||
which to associate ORM operations for this :class:`.SqlSoup` instance.
|
||||
If ``None``, a :class:`.ScopedSession` that's local to this
|
||||
module is used.
|
||||
|
||||
"""
|
||||
|
||||
self.session = session or Session
|
||||
self.base=base
|
||||
|
||||
if isinstance(engine_or_metadata, MetaData):
|
||||
self._metadata = engine_or_metadata
|
||||
elif isinstance(engine_or_metadata, (basestring, Engine)):
|
||||
self._metadata = MetaData(engine_or_metadata)
|
||||
else:
|
||||
raise ArgumentError("invalid engine or metadata argument %r" %
|
||||
engine_or_metadata)
|
||||
|
||||
self._cache = {}
|
||||
self.schema = None
|
||||
|
||||
@property
|
||||
def bind(self):
|
||||
"""The :class:`.Engine` associated with this :class:`.SqlSoup`."""
|
||||
return self._metadata.bind
|
||||
|
||||
engine = bind
|
||||
|
||||
def delete(self, instance):
|
||||
"""Mark an instance as deleted."""
|
||||
|
||||
self.session.delete(instance)
|
||||
|
||||
def execute(self, stmt, **params):
|
||||
"""Execute a SQL statement.
|
||||
|
||||
The statement may be a string SQL string,
|
||||
an :func:`.expression.select` construct, or an :func:`.expression.text`
|
||||
construct.
|
||||
|
||||
"""
|
||||
return self.session.execute(sql.text(stmt, bind=self.bind), **params)
|
||||
|
||||
@property
|
||||
def _underlying_session(self):
|
||||
if isinstance(self.session, session.Session):
|
||||
return self.session
|
||||
else:
|
||||
return self.session()
|
||||
|
||||
def connection(self):
|
||||
"""Return the current :class:`.Connection` in use by the current transaction."""
|
||||
|
||||
return self._underlying_session._connection_for_bind(self.bind)
|
||||
|
||||
def flush(self):
|
||||
"""Flush pending changes to the database.
|
||||
|
||||
See :meth:`.Session.flush`.
|
||||
|
||||
"""
|
||||
self.session.flush()
|
||||
|
||||
def rollback(self):
|
||||
"""Rollback the current transaction.
|
||||
|
||||
See :meth:`.Session.rollback`.
|
||||
|
||||
"""
|
||||
self.session.rollback()
|
||||
|
||||
def commit(self):
|
||||
"""Commit the current transaction.
|
||||
|
||||
See :meth:`.Session.commit`.
|
||||
|
||||
"""
|
||||
self.session.commit()
|
||||
|
||||
def clear(self):
|
||||
"""Synonym for :meth:`.SqlSoup.expunge_all`."""
|
||||
|
||||
self.session.expunge_all()
|
||||
|
||||
def expunge(self, instance):
|
||||
"""Remove an instance from the :class:`.Session`.
|
||||
|
||||
See :meth:`.Session.expunge`.
|
||||
|
||||
"""
|
||||
self.session.expunge(instance)
|
||||
|
||||
def expunge_all(self):
|
||||
"""Clear all objects from the current :class:`.Session`.
|
||||
|
||||
See :meth:`.Session.expunge_all`.
|
||||
|
||||
"""
|
||||
self.session.expunge_all()
|
||||
|
||||
def map_to(self, attrname, tablename=None, selectable=None,
|
||||
schema=None, base=None, mapper_args=util.immutabledict()):
|
||||
"""Configure a mapping to the given attrname.
|
||||
|
||||
This is the "master" method that can be used to create any
|
||||
configuration.
|
||||
|
||||
.. versionadded:: 0.6.6
|
||||
|
||||
:param attrname: String attribute name which will be
|
||||
established as an attribute on this :class:.`.SqlSoup`
|
||||
instance.
|
||||
:param base: a Python class which will be used as the
|
||||
base for the mapped class. If ``None``, the "base"
|
||||
argument specified by this :class:`.SqlSoup`
|
||||
instance's constructor will be used, which defaults to
|
||||
``object``.
|
||||
:param mapper_args: Dictionary of arguments which will
|
||||
be passed directly to :func:`.orm.mapper`.
|
||||
:param tablename: String name of a :class:`.Table` to be
|
||||
reflected. If a :class:`.Table` is already available,
|
||||
use the ``selectable`` argument. This argument is
|
||||
mutually exclusive versus the ``selectable`` argument.
|
||||
:param selectable: a :class:`.Table`, :class:`.Join`, or
|
||||
:class:`.Select` object which will be mapped. This
|
||||
argument is mutually exclusive versus the ``tablename``
|
||||
argument.
|
||||
:param schema: String schema name to use if the
|
||||
``tablename`` argument is present.
|
||||
|
||||
|
||||
"""
|
||||
if attrname in self._cache:
|
||||
raise InvalidRequestError(
|
||||
"Attribute '%s' is already mapped to '%s'" % (
|
||||
attrname,
|
||||
class_mapper(self._cache[attrname]).mapped_table
|
||||
))
|
||||
|
||||
if tablename is not None:
|
||||
if not isinstance(tablename, basestring):
|
||||
raise ArgumentError("'tablename' argument must be a string."
|
||||
)
|
||||
if selectable is not None:
|
||||
raise ArgumentError("'tablename' and 'selectable' "
|
||||
"arguments are mutually exclusive")
|
||||
|
||||
selectable = Table(tablename,
|
||||
self._metadata,
|
||||
autoload=True,
|
||||
autoload_with=self.bind,
|
||||
schema=schema or self.schema)
|
||||
elif schema:
|
||||
raise ArgumentError("'tablename' argument is required when "
|
||||
"using 'schema'.")
|
||||
elif selectable is not None:
|
||||
if not isinstance(selectable, expression.FromClause):
|
||||
raise ArgumentError("'selectable' argument must be a "
|
||||
"table, select, join, or other "
|
||||
"selectable construct.")
|
||||
else:
|
||||
raise ArgumentError("'tablename' or 'selectable' argument is "
|
||||
"required.")
|
||||
|
||||
if not selectable.primary_key.columns:
|
||||
if tablename:
|
||||
raise PKNotFoundError(
|
||||
"table '%s' does not have a primary "
|
||||
"key defined" % tablename)
|
||||
else:
|
||||
raise PKNotFoundError(
|
||||
"selectable '%s' does not have a primary "
|
||||
"key defined" % selectable)
|
||||
|
||||
mapped_cls = _class_for_table(
|
||||
self.session,
|
||||
self.engine,
|
||||
selectable,
|
||||
base or self.base,
|
||||
mapper_args
|
||||
)
|
||||
self._cache[attrname] = mapped_cls
|
||||
return mapped_cls
|
||||
|
||||
|
||||
def map(self, selectable, base=None, **mapper_args):
|
||||
"""Map a selectable directly.
|
||||
|
||||
.. versionchanged:: 0.6.6
|
||||
The class and its mapping are not cached and will
|
||||
be discarded once dereferenced.
|
||||
|
||||
:param selectable: an :func:`.expression.select` construct.
|
||||
:param base: a Python class which will be used as the
|
||||
base for the mapped class. If ``None``, the "base"
|
||||
argument specified by this :class:`.SqlSoup`
|
||||
instance's constructor will be used, which defaults to
|
||||
``object``.
|
||||
:param mapper_args: Dictionary of arguments which will
|
||||
be passed directly to :func:`.orm.mapper`.
|
||||
|
||||
"""
|
||||
|
||||
return _class_for_table(
|
||||
self.session,
|
||||
self.engine,
|
||||
selectable,
|
||||
base or self.base,
|
||||
mapper_args
|
||||
)
|
||||
|
||||
def with_labels(self, selectable, base=None, **mapper_args):
|
||||
"""Map a selectable directly, wrapping the
|
||||
selectable in a subquery with labels.
|
||||
|
||||
.. versionchanged:: 0.6.6
|
||||
The class and its mapping are not cached and will
|
||||
be discarded once dereferenced.
|
||||
|
||||
:param selectable: an :func:`.expression.select` construct.
|
||||
:param base: a Python class which will be used as the
|
||||
base for the mapped class. If ``None``, the "base"
|
||||
argument specified by this :class:`.SqlSoup`
|
||||
instance's constructor will be used, which defaults to
|
||||
``object``.
|
||||
:param mapper_args: Dictionary of arguments which will
|
||||
be passed directly to :func:`.orm.mapper`.
|
||||
|
||||
"""
|
||||
|
||||
# TODO give meaningful aliases
|
||||
return self.map(
|
||||
expression._clause_element_as_expr(selectable).
|
||||
select(use_labels=True).
|
||||
alias('foo'), base=base, **mapper_args)
|
||||
|
||||
def join(self, left, right, onclause=None, isouter=False,
|
||||
base=None, **mapper_args):
|
||||
"""Create an :func:`.expression.join` and map to it.
|
||||
|
||||
.. versionchanged:: 0.6.6
|
||||
The class and its mapping are not cached and will
|
||||
be discarded once dereferenced.
|
||||
|
||||
:param left: a mapped class or table object.
|
||||
:param right: a mapped class or table object.
|
||||
:param onclause: optional "ON" clause construct..
|
||||
:param isouter: if True, the join will be an OUTER join.
|
||||
:param base: a Python class which will be used as the
|
||||
base for the mapped class. If ``None``, the "base"
|
||||
argument specified by this :class:`.SqlSoup`
|
||||
instance's constructor will be used, which defaults to
|
||||
``object``.
|
||||
:param mapper_args: Dictionary of arguments which will
|
||||
be passed directly to :func:`.orm.mapper`.
|
||||
|
||||
"""
|
||||
|
||||
j = join(left, right, onclause=onclause, isouter=isouter)
|
||||
return self.map(j, base=base, **mapper_args)
|
||||
|
||||
def entity(self, attr, schema=None):
|
||||
"""Return the named entity from this :class:`.SqlSoup`, or
|
||||
create if not present.
|
||||
|
||||
For more generalized mapping, see :meth:`.map_to`.
|
||||
|
||||
"""
|
||||
try:
|
||||
return self._cache[attr]
|
||||
except KeyError, ke:
|
||||
return self.map_to(attr, tablename=attr, schema=schema)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return self.entity(attr)
|
||||
|
||||
def __repr__(self):
|
||||
return 'SqlSoup(%r)' % self._metadata
|
||||
|
||||
Reference in New Issue
Block a user