Update elixir

This commit is contained in:
Ruud
2012-05-11 12:50:51 +02:00
parent b2be9ef7c4
commit 74578db933
9 changed files with 59 additions and 547 deletions
+32 -165
View File
@@ -3,8 +3,6 @@ This module provides the ``Entity`` base class, as well as its metaclass
``EntityMeta``.
'''
from py23compat import sorted
import sys
import types
import warnings
@@ -25,11 +23,6 @@ from elixir import options
from elixir.properties import Property
DEBUG = False
try:
from sqlalchemy.orm import EXT_PASS
SA05orlater = False
except ImportError:
SA05orlater = True
__doc_all__ = ['Entity', 'EntityMeta']
@@ -205,6 +198,7 @@ class EntityDescriptor(object):
parent_desc = self.parent._descriptor
tablename = parent_desc.table_fullname
join_clauses = []
fk_columns = []
for pk_col in parent_desc.primary_keys:
colname = options.MULTIINHERITANCECOL_NAMEFORMAT % \
{'entity': self.parent.__name__.lower(),
@@ -214,12 +208,14 @@ class EntityDescriptor(object):
# a real column object when said column is not yet
# attached to a table
pk_col_name = "%s.%s" % (tablename, pk_col.key)
fk = ForeignKey(pk_col_name, ondelete='cascade')
col = Column(colname, pk_col.type, fk,
primary_key=True)
col = Column(colname, pk_col.type, primary_key=True)
fk_columns.append(col)
self.add_column(col)
join_clauses.append(col == pk_col)
self.join_condition = and_(*join_clauses)
self.add_constraint(
ForeignKeyConstraint(fk_columns,
parent_desc.primary_keys, ondelete='CASCADE'))
elif self.inheritance == 'concrete':
# Copy primary key columns from the parent.
for col in self.parent._descriptor.columns:
@@ -286,7 +282,7 @@ class EntityDescriptor(object):
self.add_constraint(
ForeignKeyConstraint(
[e.parent.key for e in con.elements],
[e._get_colspec() for e in con.elements],
[e.target_fullname for e in con.elements],
name=con.name, #TODO: modify it
onupdate=con.onupdate, ondelete=con.ondelete,
use_alter=con.use_alter))
@@ -370,6 +366,11 @@ class EntityDescriptor(object):
order = []
for colname in order_by:
#FIXME: get_column uses self.columns[key] instead of property
# names. self.columns correspond to the columns of the table if
# the table was already created and to self._columns otherwise,
# which is a ColumnCollection indexed on columns.key
# See ticket #108.
col = self.get_column(colname.strip('-'))
if colname.startswith('-'):
col = desc(col)
@@ -493,17 +494,13 @@ class EntityDescriptor(object):
(col.key, self.entity.__name__))
else:
del self._columns[col.key]
# are indexed on col.key
self._columns.add(col)
if col.primary_key:
self.has_pk = True
# Autosetup triggers shouldn't be active anymore at this point, so we
# can theoretically access the entity's table safely. But the problem
# is that if, for some reason, the trigger removal phase didn't
# happen, we'll get an infinite loop. So we just make sure we don't
# get one in any case.
table = type.__getattribute__(self.entity, 'table')
table = self.entity.table
if table is not None:
if check_duplicate and col.key in table.columns.keys():
raise Exception("Column '%s' already exist in table '%s' ! " %
@@ -595,6 +592,7 @@ class EntityDescriptor(object):
#------------------------
# some useful properties
@property
def table_fullname(self):
'''
Complete name of the table for the related entity.
@@ -605,8 +603,8 @@ class EntityDescriptor(object):
return "%s.%s" % (schema, self.tablename)
else:
return self.tablename
table_fullname = property(table_fullname)
@property
def columns(self):
if self.entity.table is not None:
return self.entity.table.columns
@@ -615,8 +613,8 @@ class EntityDescriptor(object):
# return the parent entity's columns (for example for order_by
# using a column defined in the parent.
return self._columns
columns = property(columns)
@property
def primary_keys(self):
"""
Returns the list of primary key columns of the entity.
@@ -630,15 +628,15 @@ class EntityDescriptor(object):
return self.parent._descriptor.primary_keys
else:
return [col for col in self.columns if col.primary_key]
primary_keys = property(primary_keys)
@property
def table(self):
if self.entity.table is not None:
return self.entity.table
else:
return FakeTable(self)
table = property(table)
@property
def primary_key_properties(self):
"""
Returns the list of (mapper) properties corresponding to the primary
@@ -653,30 +651,32 @@ class EntityDescriptor(object):
for prop in mapper.iterate_properties:
if isinstance(prop, ColumnProperty):
for col in prop.columns:
#XXX: Why is this extra loop necessary? What is this
# "proxy_set" supposed to mean?
for col in col.proxy_set:
col_to_prop[col] = prop
pk_cols = [c for c in mapper.mapped_table.c if c.primary_key]
self._pk_props = [col_to_prop[c] for c in pk_cols]
return self._pk_props
primary_key_properties = property(primary_key_properties)
class FakePK(object):
def __init__(self, descriptor):
self.descriptor = descriptor
@property
def columns(self):
return self.descriptor.primary_keys
columns = property(columns)
class FakeTable(object):
def __init__(self, descriptor):
self.descriptor = descriptor
self.primary_key = FakePK(descriptor)
@property
def columns(self):
return self.descriptor.columns
columns = property(columns)
@property
def fullname(self):
'''
Complete name of the table for the related entity.
@@ -687,46 +687,8 @@ class FakeTable(object):
return "%s.%s" % (schema, self.descriptor.tablename)
else:
return self.descriptor.tablename
fullname = property(fullname)
class TriggerProxy(object):
"""
A class that serves as a "trigger" ; accessing its attributes runs
the setup_all function.
Note that the `setup_all` is called on each access of the attribute.
"""
def __init__(self, class_, attrname):
self.class_ = class_
self.attrname = attrname
def __getattr__(self, name):
elixir.setup_all()
#FIXME: it's possible to get an infinite loop here if setup_all doesn't
#remove the triggers for this entity. This can happen if the entity is
#not in the `entities` list for some reason.
proxied_attr = getattr(self.class_, self.attrname)
return getattr(proxied_attr, name)
def __repr__(self):
proxied_attr = getattr(self.class_, self.attrname)
return "<TriggerProxy (%s)>" % (self.class_.__name__)
class TriggerAttribute(object):
def __init__(self, attrname):
self.attrname = attrname
def __get__(self, instance, owner):
#FIXME: it's possible to get an infinite loop here if setup_all doesn't
#remove the triggers for this entity. This can happen if the entity is
#not in the `entities` list for some reason.
elixir.setup_all()
return getattr(owner, self.attrname)
def is_entity(cls):
"""
Scan the bases classes of `cls` to see if any is an instance of
@@ -805,13 +767,6 @@ def instrument_class(cls):
# setup misc options here (like tablename etc.)
desc.setup_options()
# create trigger proxies
# TODO: support entity_name... It makes sense only for autoloaded
# tables for now, and would make more sense if we support "external"
# tables
if desc.autosetup:
_install_autosetup_triggers(cls)
class EntityMeta(type):
"""
@@ -823,11 +778,6 @@ class EntityMeta(type):
def __init__(cls, name, bases, dict_):
instrument_class(cls)
def __call__(cls, *args, **kwargs):
if cls._descriptor.autosetup and not hasattr(cls, '_setup_done'):
elixir.setup_all()
return type.__call__(cls, *args, **kwargs)
def __setattr__(cls, key, value):
if isinstance(value, Property):
if hasattr(cls, '_setup_done'):
@@ -839,84 +789,6 @@ class EntityMeta(type):
type.__setattr__(cls, key, value)
def _install_autosetup_triggers(cls, entity_name=None):
#TODO: move as much as possible of those "_private" values to the
# descriptor, so that we don't mess the initial class.
warnings.warn("The 'autosetup' option on entities is deprecated. "
"Please call setup_all() manually after all your entities have been "
"declared.", DeprecationWarning, stacklevel=4)
tablename = cls._descriptor.tablename
schema = cls._descriptor.table_options.get('schema', None)
cls._table_key = sqlalchemy.schema._get_table_key(tablename, schema)
table_proxy = TriggerProxy(cls, 'table')
md = cls._descriptor.metadata
md.tables[cls._table_key] = table_proxy
# We need to monkeypatch the metadata's table iterator method because
# otherwise it doesn't work if the setup is triggered by the
# metadata.create_all().
# This is because ManyToMany relationships add tables AFTER the list
# of tables that are going to be created is "computed"
# (metadata.tables.values()).
# see:
# - table_iterator method in MetaData class in sqlalchemy/schema.py
# - visit_metadata method in sqlalchemy/ansisql.py
if SA05orlater:
warnings.warn(
"The automatic setup via metadata.create_all() through "
"the autosetup option doesn't work with SQLAlchemy 0.5 and later!")
else:
# SA 0.6 does not use table_iterator anymore (it was already deprecated
# since SA 0.5.0)
original_table_iterator = md.table_iterator
if not hasattr(original_table_iterator,
'_non_elixir_patched_iterator'):
def table_iterator(*args, **kwargs):
elixir.setup_all()
return original_table_iterator(*args, **kwargs)
table_iterator.__doc__ = original_table_iterator.__doc__
table_iterator._non_elixir_patched_iterator = \
original_table_iterator
md.table_iterator = table_iterator
#TODO: we might want to add all columns that will be available as
#attributes on the class itself (in SA 0.4+). This is a pretty
#rare usecase, as people will normally hit the query attribute before the
#column attributes, but I've seen people hitting this problem...
for name in ('c', 'table', 'mapper', 'query'):
setattr(cls, name, TriggerAttribute(name))
cls._has_triggers = True
def _cleanup_autosetup_triggers(cls):
if not hasattr(cls, '_has_triggers'):
return
for name in ('table', 'mapper'):
setattr(cls, name, None)
for name in ('c', 'query'):
delattr(cls, name)
desc = cls._descriptor
md = desc.metadata
# the fake table could have already been removed (namely in a
# single table inheritance scenario)
md.tables.pop(cls._table_key, None)
# restore original table iterator if not done already
if not SA05orlater:
if hasattr(md.table_iterator, '_non_elixir_patched_iterator'):
md.table_iterator = \
md.table_iterator._non_elixir_patched_iterator
del cls._has_triggers
def setup_entities(entities):
'''Setup all entities in the list passed as argument'''
@@ -928,9 +800,6 @@ def setup_entities(entities):
if isinstance(attr, Property):
delattr(entity, name)
if entity._descriptor.autosetup:
_cleanup_autosetup_triggers(entity)
for method_name in (
'setup_autoload_table', 'create_pk_cols', 'setup_relkeys',
'before_table', 'setup_table', 'setup_reltables', 'after_table',
@@ -955,8 +824,7 @@ def setup_entities(entities):
def cleanup_entities(entities):
"""
Try to revert back the list of entities passed as argument to the state
they had just before their setup phase. It will not work entirely for
autosetup entities as we need to remove the autosetup triggers.
they had just before their setup phase.
As of now, this function is *not* functional in that it doesn't revert to
the exact same state the entities were before setup. For example, the
@@ -968,8 +836,6 @@ def cleanup_entities(entities):
"""
for entity in entities:
desc = entity._descriptor
if desc.autosetup:
_cleanup_autosetup_triggers(entity)
if hasattr(entity, '_setup_done'):
del entity._setup_done
@@ -1007,6 +873,7 @@ class EntityBase(object):
for key, value in kwargs.iteritems():
setattr(self, key, value)
@classmethod
def update_or_create(cls, data, surrogate=True):
pk_props = cls._descriptor.primary_key_properties
@@ -1016,17 +883,16 @@ class EntityBase(object):
record = cls.query.get(pk_tuple)
if record is None:
if surrogate:
raise Exception("cannot create surrogate with pk")
raise Exception("Cannot create surrogate with pk")
else:
record = cls()
else:
if surrogate:
record = cls()
else:
raise Exception("cannot create non surrogate without pk")
raise Exception("Cannot create non surrogate without pk")
record.from_dict(data)
return record
update_or_create = classmethod(update_or_create)
def from_dict(self, data):
"""
@@ -1105,10 +971,11 @@ class EntityBase(object):
# This bunch of session methods, along with all the query methods below
# only make sense when using a global/scoped/contextual session.
@property
def _global_session(self):
return self._descriptor.session.registry()
_global_session = property(_global_session)
#FIXME: remove all deprecated methods, possibly all of these
def merge(self, *args, **kwargs):
return self._global_session.merge(self, *args, **kwargs)
@@ -1126,6 +993,7 @@ class EntityBase(object):
return self._global_session.save_or_update(self, *args, **kwargs)
# query methods
@classmethod
def get_by(cls, *args, **kwargs):
"""
Returns the first instance of this class matching the given criteria.
@@ -1133,8 +1001,8 @@ class EntityBase(object):
session.query(MyClass).filter_by(...).first()
"""
return cls.query.filter_by(*args, **kwargs).first()
get_by = classmethod(get_by)
@classmethod
def get(cls, *args, **kwargs):
"""
Return the instance of this class based on the given identifier,
@@ -1142,7 +1010,6 @@ class EntityBase(object):
session.query(MyClass).get(...)
"""
return cls.query.get(*args, **kwargs)
get = classmethod(get)
class Entity(EntityBase):