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
+1 -6
View File
@@ -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)
+1 -3
View File
@@ -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:
+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):
+3 -6
View File
@@ -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')
+2 -2
View File
@@ -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))
-251
View File
@@ -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)
-11
View File
@@ -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,
-73
View File
@@ -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
+20 -30
View File
@@ -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)