From 74578db93317d2bce34df7ce1867b2abe3143e14 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 11 May 2012 12:50:51 +0200 Subject: [PATCH] Update elixir --- libs/elixir/__init__.py | 7 +- libs/elixir/collection.py | 4 +- libs/elixir/entity.py | 197 +++++--------------------- libs/elixir/events.py | 9 +- libs/elixir/ext/associable.py | 4 +- libs/elixir/ext/list.py | 251 ---------------------------------- libs/elixir/options.py | 11 -- libs/elixir/py23compat.py | 73 ---------- libs/elixir/relationships.py | 50 +++---- 9 files changed, 59 insertions(+), 547 deletions(-) delete mode 100644 libs/elixir/ext/list.py delete mode 100644 libs/elixir/py23compat.py diff --git a/libs/elixir/__init__.py b/libs/elixir/__init__.py index e92ad7fb..a242b538 100644 --- a/libs/elixir/__init__.py +++ b/libs/elixir/__init__.py @@ -38,7 +38,7 @@ from elixir.statements import Statement from elixir.collection import EntityCollection, GlobalEntityCollection -__version__ = '0.7.1' +__version__ = '0.8.0dev' __all__ = ['Entity', 'EntityBase', 'EntityMeta', 'EntityCollection', 'entities', @@ -85,11 +85,6 @@ def drop_all(*args, **kwargs): def setup_all(create_tables=False, *args, **kwargs): '''Setup the table and mapper of all entities in the default entity collection. - - This is called automatically if any entity of the collection is configured - with the `autosetup` option and it is first accessed, - instanciated (called) or the create_all method of a metadata containing - tables from any of those entities is called. ''' setup_entities(entities) diff --git a/libs/elixir/collection.py b/libs/elixir/collection.py index 7ba8b66e..78127e3e 100644 --- a/libs/elixir/collection.py +++ b/libs/elixir/collection.py @@ -4,8 +4,6 @@ Default entity collection implementation import sys import re -from elixir.py23compat import rsplit - class BaseCollection(list): def __init__(self, entities=None): list.__init__(self) @@ -24,7 +22,7 @@ class BaseCollection(list): root = entity._descriptor.resolve_root if root: full_path = '%s.%s' % (root, full_path) - module_path, classname = rsplit(full_path, '.', 1) + module_path, classname = full_path.rsplit('.', 1) module = sys.modules[module_path] res = getattr(module, classname, None) if res is None: diff --git a/libs/elixir/entity.py b/libs/elixir/entity.py index 50574575..87f5154c 100644 --- a/libs/elixir/entity.py +++ b/libs/elixir/entity.py @@ -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 "" % (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): diff --git a/libs/elixir/events.py b/libs/elixir/events.py index b160928e..293a8a4a 100644 --- a/libs/elixir/events.py +++ b/libs/elixir/events.py @@ -1,3 +1,5 @@ +from sqlalchemy.orm import reconstructor + __all__ = [ 'before_insert', 'after_insert', @@ -22,9 +24,4 @@ before_update = create_decorator('before_update') after_update = create_decorator('after_update') before_delete = create_decorator('before_delete') after_delete = create_decorator('after_delete') -try: - from sqlalchemy.orm import reconstructor -except ImportError: - def reconstructor(func): - raise Exception('The reconstructor method decorator is only ' - 'available with SQLAlchemy 0.5 and later') + diff --git a/libs/elixir/ext/associable.py b/libs/elixir/ext/associable.py index 2a37c4b8..b31c5a74 100644 --- a/libs/elixir/ext/associable.py +++ b/libs/elixir/ext/associable.py @@ -222,12 +222,12 @@ def associable(assoc_entity, plural_name=None, lazy=True): # add helper methods def select_by(cls, **kwargs): - return cls.query.join([attr_name, 'targets']) \ + return cls.query.join(attr_name, 'targets') \ .filter_by(**kwargs).all() setattr(entity, 'select_by_%s' % self.name, classmethod(select_by)) def select(cls, *args, **kwargs): - return cls.query.join([attr_name, 'targets']) \ + return cls.query.join(attr_name, 'targets') \ .filter(*args, **kwargs).all() setattr(entity, 'select_%s' % self.name, classmethod(select)) diff --git a/libs/elixir/ext/list.py b/libs/elixir/ext/list.py deleted file mode 100644 index 3f91c3f5..00000000 --- a/libs/elixir/ext/list.py +++ /dev/null @@ -1,251 +0,0 @@ -''' -This extension is DEPRECATED. Please use the orderinglist SQLAlchemy -extension instead. - -For details: -http://www.sqlalchemy.org/docs/05/reference/ext/orderinglist.html - -For an Elixir example: -http://elixir.ematia.de/trac/wiki/Recipes/UsingEntityForOrderedList -or -http://elixir.ematia.de/trac/browser/elixir/0.7.0/tests/test_o2m.py#L155 - - - -An ordered-list plugin for Elixir to help you make an entity be able to be -managed in a list-like way. Much inspiration comes from the Ruby on Rails -acts_as_list plugin, which is currently more full-featured than this plugin. - -Once you flag an entity with an `acts_as_list()` statement, a column will be -added to the entity called `position` which will be an integer column that is -managed for you by the plugin. You can pass an alternative column name to -the plugin using the `column_name` keyword argument. - -In addition, your entity will get a series of new methods attached to it, -including: - -+----------------------+------------------------------------------------------+ -| Method Name | Description | -+======================+======================================================+ -| ``move_lower`` | Move the item lower in the list | -+----------------------+------------------------------------------------------+ -| ``move_higher`` | Move the item higher in the list | -+----------------------+------------------------------------------------------+ -| ``move_to_bottom`` | Move the item to the bottom of the list | -+----------------------+------------------------------------------------------+ -| ``move_to_top`` | Move the item to the top of the list | -+----------------------+------------------------------------------------------+ -| ``move_to`` | Move the item to a specific position in the list | -+----------------------+------------------------------------------------------+ - - -Sometimes, your entities that represent list items will be a part of different -lists. To implement this behavior, simply pass the `acts_as_list` statement a -callable that returns a "qualifier" SQLAlchemy expression. This expression will -be added to the generated WHERE clauses used by the plugin. - -Example model usage: - -.. sourcecode:: python - - from elixir import * - from elixir.ext.list import acts_as_list - - class ToDo(Entity): - subject = Field(String(128)) - owner = ManyToOne('Person') - - def qualify(self): - return ToDo.owner_id == self.owner_id - - acts_as_list(qualifier=qualify) - - class Person(Entity): - name = Field(String(64)) - todos = OneToMany('ToDo', order_by='position') - - -The above example can then be used to manage ordered todo lists for people. -Note that you must set the `order_by` property on the `Person.todo` relation in -order for the relation to respect the ordering. Here is an example of using -this model in practice: - -.. sourcecode:: python - - p = Person.query.filter_by(name='Jonathan').one() - p.todos.append(ToDo(subject='Three')) - p.todos.append(ToDo(subject='Two')) - p.todos.append(ToDo(subject='One')) - session.commit(); session.clear() - - p = Person.query.filter_by(name='Jonathan').one() - p.todos[0].move_to_bottom() - p.todos[2].move_to_top() - session.commit(); session.clear() - - p = Person.query.filter_by(name='Jonathan').one() - assert p.todos[0].subject == 'One' - assert p.todos[1].subject == 'Two' - assert p.todos[2].subject == 'Three' - - -For more examples, refer to the unit tests for this plugin. -''' - -from elixir.statements import Statement -from elixir.events import before_insert, before_delete -from sqlalchemy import Column, Integer, select, func, literal, and_ -import warnings - -__all__ = ['acts_as_list'] -__doc_all__ = [] - - -def get_entity_where(instance): - clauses = [] - for column in instance.table.primary_key.columns: - instance_value = getattr(instance, column.name) - clauses.append(column == instance_value) - return and_(*clauses) - - -class ListEntityBuilder(object): - - def __init__(self, entity, qualifier=None, column_name='position'): - warnings.warn("The act_as_list extension is deprecated. Please use " - "SQLAlchemy's orderinglist extension instead", - DeprecationWarning, stacklevel=6) - self.entity = entity - self.qualifier_method = qualifier - self.column_name = column_name - - def create_non_pk_cols(self): - if self.entity._descriptor.autoload: - for c in self.entity.table.c: - if c.name == self.column_name: - self.position_column = c - if not hasattr(self, 'position_column'): - raise Exception( - "Could not find column '%s' in autoloaded table '%s', " - "needed by entity '%s'." % (self.column_name, - self.entity.table.name, self.entity.__name__)) - else: - self.position_column = Column(self.column_name, Integer) - self.entity._descriptor.add_column(self.position_column) - - def after_table(self): - position_column = self.position_column - position_column_name = self.column_name - - qualifier_method = self.qualifier_method - if not qualifier_method: - qualifier_method = lambda self: None - - def _init_position(self): - s = select( - [(func.max(position_column)+1).label('value')], - qualifier_method(self) - ).union( - select([literal(1).label('value')]) - ) - a = s.alias() - # we use a second func.max to get the maximum between 1 and the - # real max position if any exist - setattr(self, position_column_name, select([func.max(a.c.value)])) - - # Note that this method could be rewritten more simply like below, - # but because this extension is going to be deprecated anyway, - # I don't want to risk breaking something I don't want to maintain. -# setattr(self, position_column_name, select( -# [func.coalesce(func.max(position_column), 0) + 1], -# qualifier_method(self) -# )) - _init_position = before_insert(_init_position) - - def _shift_items(self): - self.table.update( - and_( - position_column > getattr(self, position_column_name), - qualifier_method(self) - ), - values={ - position_column : position_column - 1 - } - ).execute() - _shift_items = before_delete(_shift_items) - - def move_to_bottom(self): - # move the items that were above this item up one - self.table.update( - and_( - position_column >= getattr(self, position_column_name), - qualifier_method(self) - ), - values = { - position_column : position_column - 1 - } - ).execute() - - # move this item to the max position - # MySQL does not support the correlated subquery, so we need to - # execute the query (through scalar()). See ticket #34. - self.table.update( - get_entity_where(self), - values={ - position_column : select( - [func.max(position_column) + 1], - qualifier_method(self) - ).scalar() - } - ).execute() - - def move_to_top(self): - self.move_to(1) - - def move_to(self, position): - current_position = getattr(self, position_column_name) - - # determine which direction we're moving - if position < current_position: - where = and_( - position <= position_column, - position_column < current_position, - qualifier_method(self) - ) - modifier = 1 - elif position > current_position: - where = and_( - current_position < position_column, - position_column <= position, - qualifier_method(self) - ) - modifier = -1 - - # shift the items in between the current and new positions - self.table.update(where, values = { - position_column : position_column + modifier - }).execute() - - # update this item's position to the desired position - self.table.update(get_entity_where(self)) \ - .execute(**{position_column_name: position}) - - def move_lower(self): - # replace for ex.: p.todos.insert(x + 1, p.todos.pop(x)) - self.move_to(getattr(self, position_column_name) + 1) - - def move_higher(self): - self.move_to(getattr(self, position_column_name) - 1) - - - # attach new methods to entity - self.entity._init_position = _init_position - self.entity._shift_items = _shift_items - self.entity.move_lower = move_lower - self.entity.move_higher = move_higher - self.entity.move_to_bottom = move_to_bottom - self.entity.move_to_top = move_to_top - self.entity.move_to = move_to - - -acts_as_list = Statement(ListEntityBuilder) diff --git a/libs/elixir/options.py b/libs/elixir/options.py index 9284b04b..27d7d195 100644 --- a/libs/elixir/options.py +++ b/libs/elixir/options.py @@ -124,16 +124,6 @@ The list of supported arguments are as follows: | | module by setting the ``__session__`` attribute of | | | that module. | +---------------------+-------------------------------------------------------+ -| ``autosetup`` | DEPRECATED. Specify whether that entity will contain | -| | automatic setup triggers. | -| | That is if this entity will be | -| | automatically setup (along with all other entities | -| | which were already declared) if any of the following | -| | condition happen: some of its attributes are accessed | -| | ('c', 'table', 'mapper' or 'query'), instanciated | -| | (called) or the create_all method of this entity's | -| | metadata is called. Defaults to ``False``. | -+---------------------+-------------------------------------------------------+ | ``allowcoloverride``| Specify whether it is allowed to override columns. | | | By default, Elixir forbids you to add a column to an | | | entity's table which already exist in that table. If | @@ -225,7 +215,6 @@ MIGRATION_TO_07_AID = False # options_defaults = dict( abstract=False, - autosetup=False, inheritance='single', polymorphic=True, identity=None, diff --git a/libs/elixir/py23compat.py b/libs/elixir/py23compat.py deleted file mode 100644 index 0d6b1b88..00000000 --- a/libs/elixir/py23compat.py +++ /dev/null @@ -1,73 +0,0 @@ -# Some helper functions to get by without Python 2.4 - -# set -try: - set = set -except NameError: - from sets import Set as set - -orig_cmp = cmp -# [].sort -def sort_list(l, cmp=None, key=None, reverse=False): - try: - l.sort(cmp, key, reverse) - except TypeError, e: - if not str(e).startswith('sort expected at most 1 arguments'): - raise - if cmp is None: - cmp = orig_cmp - if key is not None: - # the cmp=cmp parameter is required to get the original comparator - # into the lambda namespace - cmp = lambda self, other, cmp=cmp: cmp(key(self), key(other)) - if reverse: - cmp = lambda self, other, cmp=cmp: -cmp(self,other) - l.sort(cmp) - -# sorted -try: - sorted = sorted -except NameError: - # global name 'sorted' doesn't exist in Python2.3 - # this provides a poor-man's emulation of the sorted built-in method - def sorted(l, cmp=None, key=None, reverse=False): - sorted_list = list(l) - sort_list(sorted_list, cmp, key, reverse) - return sorted_list - -# rsplit -try: - ''.rsplit - def rsplit(s, delim, maxsplit): - return s.rsplit(delim, maxsplit) - -except AttributeError: - def rsplit(s, delim, maxsplit): - """Return a list of the words of the string s, scanning s - from the end. To all intents and purposes, the resulting - list of words is the same as returned by split(), except - when the optional third argument maxsplit is explicitly - specified and nonzero. When maxsplit is nonzero, at most - maxsplit number of splits - the rightmost ones - occur, - and the remainder of the string is returned as the first - element of the list (thus, the list will have at most - maxsplit+1 elements). New in version 2.4. - >>> rsplit('foo.bar.baz', '.', 0) - ['foo.bar.baz'] - >>> rsplit('foo.bar.baz', '.', 1) - ['foo.bar', 'baz'] - >>> rsplit('foo.bar.baz', '.', 2) - ['foo', 'bar', 'baz'] - >>> rsplit('foo.bar.baz', '.', 99) - ['foo', 'bar', 'baz'] - """ - assert maxsplit >= 0 - - if maxsplit == 0: return [s] - - # the following lines perform the function, but inefficiently. - # This may be adequate for compatibility purposes - items = s.split(delim) - if maxsplit < len(items): - items[:-maxsplit] = [delim.join(items[:-maxsplit])] - return items diff --git a/libs/elixir/relationships.py b/libs/elixir/relationships.py index f825120b..6c14dbb6 100644 --- a/libs/elixir/relationships.py +++ b/libs/elixir/relationships.py @@ -278,8 +278,9 @@ relationships accept the following optional (keyword) arguments: | | reference the "local"/current entity's table. | +--------------------+--------------------------------------------------------+ | ``table`` | Use a manually created table. If this argument is | -| | used, Elixir won't generate a table for this | -| | relationship, and use the one given instead. | +| | used, Elixir will not generate a table for this | +| | relationship, and use the one given instead. This | +| | argument only accepts SQLAlchemy's Table objects. | +--------------------+--------------------------------------------------------+ | ``order_by`` | Specify which field(s) should be used to sort the | | | results given by accessing the relation field. | @@ -303,13 +304,6 @@ relationships accept the following optional (keyword) arguments: | ``table_kwargs`` | A dictionary holding any other keyword argument you | | | might want to pass to the underlying Table object. | +--------------------+--------------------------------------------------------+ -| ``column_format`` | DEPRECATED. Specify an alternate format string for | -| | naming the | -| | columns in the mapping table. The default value is | -| | defined in ``elixir.options.M2MCOL_NAMEFORMAT``. You | -| | will be passed ``tablename``, ``key``, and ``entity`` | -| | as arguments to the format string. | -+--------------------+--------------------------------------------------------+ ================ @@ -493,6 +487,7 @@ class Relationship(Property): self.property = relation(self.target, **kwargs) self.add_mapper_property(self.name, self.property) + @property def target(self): if not self._target: if isinstance(self.of_kind, basestring): @@ -501,8 +496,8 @@ class Relationship(Property): else: self._target = self.of_kind return self._target - target = property(target) + @property def inverse(self): if not hasattr(self, '_inverse'): if self.inverse_name: @@ -531,7 +526,6 @@ class Relationship(Property): inverse._inverse = self return self._inverse - inverse = property(inverse) def match_type_of(self, other): return False @@ -612,12 +606,12 @@ class ManyToOne(Relationship): def match_type_of(self, other): return isinstance(other, (OneToMany, OneToOne)) + @property def target_table(self): if isinstance(self.target, EntityMeta): return self.target._descriptor.table else: return class_mapper(self.target).local_table - target_table = property(target_table) def create_keys(self, pk): ''' @@ -634,7 +628,10 @@ class ManyToOne(Relationship): source_desc = self.entity._descriptor if isinstance(self.target, EntityMeta): # make sure the target has all its pk set up + #FIXME: this is not enough when specifying target_column manually, + # on unique, non-pk col, see tests/test_m2o.py:test_non_pk_forward self.target._descriptor.create_pk_cols() + #XXX: another option, instead of the FakeTable, would be to create an # EntityDescriptor for the SA class. target_table = self.target_table @@ -655,6 +652,12 @@ class ManyToOne(Relationship): "Couldn't find a foreign key constraint in table " "'%s' using the following columns: %s." % (self.entity.table.name, colnames)) + else: + # in this case we let SA handle everything. + # XXX: we might want to try to build join clauses anyway so + # that we know whether there is an ambiguity or not, and + # suggest using colname if there is one + pass if self.field: raise NotImplementedError( "'field' argument not allowed on autoloaded table " @@ -808,8 +811,8 @@ class OneToOne(Relationship): # useless because the remote_side is already setup in the other way # (ManyToOne). if self.entity.table is self.target.table: - #FIXME: IF this code is of any use, it will probably break for - # autoloaded tables + # When using a manual/autoloaded table, it will be assigned + # an empty list, which doesn't seem to upset SQLAlchemy kwargs['remote_side'] = self.inverse.foreign_key # Contrary to ManyToMany relationships, we need to specify the join @@ -839,7 +842,6 @@ class ManyToMany(Relationship): local_colname=None, remote_colname=None, ondelete=None, onupdate=None, table=None, schema=None, - column_format=None, filter=None, table_kwargs=None, *args, **kwargs): @@ -858,14 +860,9 @@ class ManyToMany(Relationship): self.table = table self.schema = schema - if column_format: - warnings.warn("The 'column_format' argument on ManyToMany " - "relationships is deprecated. Please use the 'local_colname' " - "and/or 'remote_colname' arguments if you want custom " - "column names for this table only, or modify " - "options.M2MCOL_NAMEFORMAT if you want a custom format for " - "all ManyToMany tables", DeprecationWarning, stacklevel=3) - self.column_format = column_format or options.M2MCOL_NAMEFORMAT + #TODO: this can probably be simplified/moved elsewhere since the + #argument disappeared + self.column_format = options.M2MCOL_NAMEFORMAT if not hasattr(self.column_format, '__call__'): # we need to store the format in a variable so that the # closure of the lambda is correct @@ -891,13 +888,6 @@ class ManyToMany(Relationship): super(ManyToMany, self).__init__(of_kind, *args, **kwargs) - def get_table(self): - warnings.warn("The secondary_table attribute on ManyToMany objects is " - "deprecated. You should rather use the table attribute.", - DeprecationWarning, stacklevel=2) - return self.table - secondary_table = property(get_table) - def match_type_of(self, other): return isinstance(other, ManyToMany)