diff options
-rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 12 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/query.py | 184 | ||||
-rw-r--r-- | lib/sqlalchemy/util.py | 108 |
3 files changed, 177 insertions, 127 deletions
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 5905c7111..45982ab2e 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -716,6 +716,7 @@ def extension(ext): """ return ExtensionOption(ext) +@sa_util.accepts_a_list_as_starargs(list_deprecation='pending') def eagerload(*keys): """Return a ``MapperOption`` that will convert the property of the given name into an eager load. @@ -724,8 +725,8 @@ def eagerload(*keys): """ return strategies.EagerLazyOption(keys, lazy=False) -eagerload = sa_util.array_as_starargs_fn_decorator(eagerload) +@sa_util.accepts_a_list_as_starargs(list_deprecation='pending') def eagerload_all(*keys): """Return a ``MapperOption`` that will convert all properties along the given dot-separated path into an eager load. @@ -741,8 +742,8 @@ def eagerload_all(*keys): """ return strategies.EagerLazyOption(keys, lazy=False, chained=True) -eagerload_all = sa_util.array_as_starargs_fn_decorator(eagerload_all) +@sa_util.accepts_a_list_as_starargs(list_deprecation='pending') def lazyload(*keys): """Return a ``MapperOption`` that will convert the property of the given name into a lazy load. @@ -751,7 +752,6 @@ def lazyload(*keys): """ return strategies.EagerLazyOption(keys, lazy=True) -lazyload = sa_util.array_as_starargs_fn_decorator(lazyload) def noload(*keys): """Return a ``MapperOption`` that will convert the property of the @@ -772,6 +772,7 @@ def contains_alias(alias): """ return AliasOption(alias) +@sa_util.accepts_a_list_as_starargs(list_deprecation='pending') def contains_eager(*keys, **kwargs): """Return a ``MapperOption`` that will indicate to the query that the given attribute will be eagerly loaded. @@ -790,8 +791,8 @@ def contains_eager(*keys, **kwargs): raise exceptions.ArgumentError("Invalid kwargs for contains_eager: %r" % kwargs.keys()) return (strategies.EagerLazyOption(keys, lazy=False), strategies.LoadEagerFromAliasOption(keys, alias=alias)) -contains_eager = sa_util.array_as_starargs_fn_decorator(contains_eager) +@sa_util.accepts_a_list_as_starargs(list_deprecation='pending') def defer(*keys): """Return a ``MapperOption`` that will convert the column property of the given name into a deferred load. @@ -799,8 +800,8 @@ def defer(*keys): Used with ``query.options()`` """ return strategies.DeferredOption(keys, defer=True) -defer = sa_util.array_as_starargs_fn_decorator(defer) +@sa_util.accepts_a_list_as_starargs(list_deprecation='pending') def undefer(*keys): """Return a ``MapperOption`` that will convert the column property of the given name into a non-deferred (regular column) load. @@ -809,7 +810,6 @@ def undefer(*keys): """ return strategies.DeferredOption(keys, defer=False) -undefer = sa_util.array_as_starargs_fn_decorator(undefer) def undefer_group(name): """Return a ``MapperOption`` that will convert the given group of deferred diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 392e9647d..f84a2d30e 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -42,24 +42,15 @@ __all__ = ['Query', 'QueryContext', 'aliased'] aliased = AliasedClass def _generative(*assertions): - """mark a method as generative.""" - - def decorate(fn): - argspec = util.format_argspec_plus(fn) - run_assertions = assertions - code = "\n".join([ - "def %s%s:", - " %r", - " self = self._clone()", - " for a in run_assertions:", - " a(self, %r)", - " fn%s", - " return self" - ]) % (fn.__name__, argspec['args'], fn.__doc__, fn.__name__, argspec['apply_pos']) - env = locals().copy() - exec code in env - return env[fn.__name__] - return decorate + """Mark a method as generative.""" + def generate(fn, *args, **kw): + self = args[0]._clone() + fn_name = fn.func_name + for assertion in assertions: + assertion(self, fn_name) + fn(self, *args[1:], **kw) + return self + return util.decorator(generate) class Query(object): """Encapsulates the object-fetching operations provided by Mappers.""" @@ -194,10 +185,10 @@ class Query(object): return e return replace + @_generative() def _adapt_all_clauses(self): self._disable_orm_filtering = True - _adapt_all_clauses = _generative()(_adapt_all_clauses) - + def _adapt_clause(self, clause, as_filter, orm_only): adapters = [] if as_filter and self._filter_aliases: @@ -298,9 +289,9 @@ class Query(object): "To modify the row-limited results of a Query, call from_self() first. Otherwise, call %s() before limit() or offset() are applied." % (meth, meth) ) + @_generative(__no_criterion_condition) def __no_criterion(self): """generate a Query with no criterion, warn if criterion was present""" - __no_criterion = _generative(__no_criterion_condition)(__no_criterion) def __get_options(self, populate_existing=None, version_check=None, only_load_props=None, refresh_state=None): if populate_existing: @@ -329,27 +320,27 @@ class Query(object): return self.statement.alias() + @_generative() def with_labels(self): """Apply column labels to the return value of Query.statement. - - Indicates that this Query's `statement` accessor should return a SELECT statement - that applies labels to all columns in the form <tablename>_<columnname>; this - is commonly used to disambiguate columns from multiple tables which have the - same name. - + + Indicates that this Query's `statement` accessor should return a + SELECT statement that applies labels to all columns in the form + <tablename>_<columnname>; this is commonly used to disambiguate + columns from multiple tables which have the same name. + When the `Query` actually issues SQL to load rows, it always uses column labeling. - + """ self._with_labels = True - with_labels = _generative()(with_labels) - - + def whereclause(self): """return the WHERE criterion for this Query.""" return self._criterion whereclause = property(whereclause) + @_generative() def _with_current_path(self, path): """indicate that this query applies to objects loaded within a certain path. @@ -359,8 +350,8 @@ class Query(object): """ self._current_path = path - _with_current_path = _generative()(_with_current_path) + @_generative(__no_from_condition, __no_criterion_condition) def with_polymorphic(self, cls_or_mappers, selectable=None): """Load columns for descendant mappers of this Query's mapper. @@ -388,8 +379,8 @@ class Query(object): """ entity = self._generate_mapper_zero() entity.set_with_polymorphic(self, cls_or_mappers, selectable=selectable) - with_polymorphic = _generative(__no_from_condition, __no_criterion_condition)(with_polymorphic) + @_generative() def yield_per(self, count): """Yield only ``count`` rows at a time. @@ -404,7 +395,6 @@ class Query(object): """ self._yield_per = count - yield_per = _generative()(yield_per) def get(self, ident): """Return an instance of the object based on the given identifier, or None if not found. @@ -450,10 +440,11 @@ class Query(object): return Query(target, **kwargs).filter(criterion) query_from_parent = classmethod(util.deprecated(None, False)(query_from_parent)) + @_generative() def correlate(self, *args): self._correlate = self._correlate.union(_orm_selectable(s) for s in args) - correlate = _generative()(correlate) + @_generative() def autoflush(self, setting): """Return a Query with a specific 'autoflush' setting. @@ -464,8 +455,8 @@ class Query(object): """ self._autoflush = setting - autoflush = _generative()(autoflush) + @_generative() def populate_existing(self): """Return a Query that will refresh all instances loaded. @@ -480,21 +471,23 @@ class Query(object): """ self._populate_existing = True - populate_existing = _generative()(populate_existing) def with_parent(self, instance, property=None): - """add a join criterion corresponding to a relationship to the given parent instance. + """Add a join criterion corresponding to a relationship to the given + parent instance. - instance - a persistent or detached instance which is related to class represented - by this query. + instance + a persistent or detached instance which is related to class + represented by this query. - property - string name of the property which relates this query's class to the - instance. if None, the method will attempt to find a suitable property. + property + string name of the property which relates this query's class to the + instance. if None, the method will attempt to find a suitable + property. - currently, this method only works with immediate parent relationships, but in the - future may be enhanced to work across a chain of parent mappers. + Currently, this method only works with immediate parent relationships, + but in the future may be enhanced to work across a chain of parent + mappers. """ from sqlalchemy.orm import properties @@ -509,6 +502,7 @@ class Query(object): prop = mapper.get_property(property, resolve_synonyms=True) return self.filter(prop.compare(operators.eq, instance, value_is_parent=True)) + @_generative() def add_entity(self, entity, alias=None): """add a mapped entity to the list of result columns to be returned.""" @@ -518,8 +512,8 @@ class Query(object): self._entities = list(self._entities) m = _MapperEntity(self, entity) self.__setup_aliasizers([m]) - add_entity = _generative()(add_entity) + @_generative() def from_self(self, *entities): """return a Query that selects from this Query's SELECT statement. @@ -538,7 +532,6 @@ class Query(object): _QueryEntity(self, ent) self.__setup_aliasizers(self._entities) - from_self = _generative()(from_self) _from_self = from_self def values(self, *columns): @@ -556,13 +549,13 @@ class Query(object): return iter(q) _values = values + @_generative() def add_column(self, column): """Add a SQL ColumnElement to the list of result columns to be returned.""" self._entities = list(self._entities) c = _ColumnEntity(self, column) self.__setup_aliasizers([c]) - add_column = _generative()(add_column) def options(self, *args): """Return a new Query object, applying the given list of @@ -574,6 +567,7 @@ class Query(object): def _conditional_options(self, *args): return self.__options(True, *args) + @_generative() def __options(self, conditional, *args): # most MapperOptions write to the '_attributes' dictionary, # so copy that as well @@ -586,14 +580,14 @@ class Query(object): else: for opt in opts: opt.process_query(self) - __options = _generative()(__options) + @_generative() def with_lockmode(self, mode): """Return a new Query object with the specified locking mode.""" self._lockmode = mode - with_lockmode = _generative()(with_lockmode) + @_generative() def params(self, *args, **kwargs): """add values for bind parameters which may have been specified in filter(). @@ -609,8 +603,8 @@ class Query(object): raise sa_exc.ArgumentError("params() takes zero or one positional argument, which is a dictionary.") self._params = self._params.copy() self._params.update(kwargs) - params = _generative()(params) + @_generative(__no_statement_condition, __no_limit_offset) def filter(self, criterion): """apply the given filtering criterion to the query and return the newly resulting ``Query`` @@ -629,7 +623,6 @@ class Query(object): self._criterion = self._criterion & criterion else: self._criterion = criterion - filter = _generative(__no_statement_condition, __no_limit_offset)(filter) def filter_by(self, **kwargs): """apply the given filtering criterion to the query and return the newly resulting ``Query``.""" @@ -640,6 +633,8 @@ class Query(object): return self.filter(sql.and_(*clauses)) + @_generative(__no_statement_condition, __no_limit_offset) + @util.accepts_a_list_as_starargs(list_deprecation='pending') def order_by(self, *criterion): """apply one or more ORDER BY criterion to the query and return the newly resulting ``Query``""" @@ -649,9 +644,9 @@ class Query(object): self._order_by = criterion else: self._order_by = self._order_by + criterion - order_by = util.array_as_starargs_decorator(order_by) - order_by = _generative(__no_statement_condition, __no_limit_offset)(order_by) + @_generative(__no_statement_condition, __no_limit_offset) + @util.accepts_a_list_as_starargs(list_deprecation='pending') def group_by(self, *criterion): """apply one or more GROUP BY criterion to the query and return the newly resulting ``Query``""" @@ -661,9 +656,8 @@ class Query(object): self._group_by = criterion else: self._group_by = self._group_by + criterion - group_by = util.array_as_starargs_decorator(group_by) - group_by = _generative(__no_statement_condition, __no_limit_offset)(group_by) + @_generative(__no_statement_condition, __no_limit_offset) def having(self, criterion): """apply a HAVING criterion to the query and return the newly resulting ``Query``.""" @@ -679,26 +673,25 @@ class Query(object): self._having = self._having & criterion else: self._having = criterion - having = _generative(__no_statement_condition, __no_limit_offset)(having) + @util.accepts_a_list_as_starargs(list_deprecation='pending') def join(self, *props, **kwargs): """Create a join against this ``Query`` object's criterion and apply generatively, returning the newly resulting ``Query``. - each element in \*props may be: - - * a string property name, i.e. "rooms". This will join along - the relation of the same name from this Query's "primary" - mapper, if one is present. - + Each element in \*props may be: + + * a string property name, i.e. "rooms". This will join along the + relation of the same name from this Query's "primary" mapper, if + one is present. + * a class-mapped attribute, i.e. Houses.rooms. This will create a join from "Houses" table to that of the "rooms" relation. - - * a 2-tuple containing a target class or selectable, and - an "ON" clause. The ON clause can be the property name/ - attribute like above, or a SQL expression. - - + + * a 2-tuple containing a target class or selectable, and an "ON" + clause. The ON clause can be the property name/ attribute like + above, or a SQL expression. + e.g.:: # join along string attribute names @@ -714,17 +707,17 @@ class Query(object): # "Colonials" subclass of Houses, then join to the # "closets" relation on Room session.query(Houses).join(Colonials.rooms, Room.closets) - + # join from Company entities to the "employees" collection, # using "people JOIN engineers" as the target. Then join # to the "computers" collection on the Engineer entity. session.query(Company).join((people.join(engineers), 'employees'), Engineer.computers) - + # join from Articles to Keywords, using the "keywords" attribute. # assume this is a many-to-many relation. session.query(Article).join(Article.keywords) - - # same thing, but spelled out entirely explicitly + + # same thing, but spelled out entirely explicitly # including the association table. session.query(Article).join( (article_keywords, Articles.id==article_keywords.c.article_id), @@ -747,8 +740,8 @@ class Query(object): if kwargs: raise TypeError("unknown arguments: %s" % ','.join(kwargs.keys())) return self.__join(props, outerjoin=False, create_aliases=aliased, from_joinpoint=from_joinpoint) - join = util.array_as_starargs_decorator(join) + @util.accepts_a_list_as_starargs(list_deprecation='pending') def outerjoin(self, *props, **kwargs): """Create a left outer join against this ``Query`` object's criterion and apply generatively, retunring the newly resulting ``Query``. @@ -760,8 +753,8 @@ class Query(object): if kwargs: raise TypeError("unknown arguments: %s" % ','.join(kwargs.keys())) return self.__join(props, outerjoin=True, create_aliases=aliased, from_joinpoint=from_joinpoint) - outerjoin = util.array_as_starargs_decorator(outerjoin) + @_generative(__no_statement_condition, __no_limit_offset) def __join(self, keys, outerjoin, create_aliases, from_joinpoint): self.__currenttables = set(self.__currenttables) self._polymorphic_adapters = self._polymorphic_adapters.copy() @@ -889,8 +882,7 @@ class Query(object): self._from_obj = clause self._joinpoint = right_entity - __join = _generative(__no_statement_condition, __no_limit_offset)(__join) - + @_generative(__no_statement_condition) def reset_joinpoint(self): """return a new Query reset the 'joinpoint' of this Query reset back to the starting mapper. Subsequent generative calls will @@ -901,8 +893,8 @@ class Query(object): """ self.__reset_joinpoint() - reset_joinpoint = _generative(__no_statement_condition)(reset_joinpoint) + @_generative(__no_from_condition, __no_criterion_condition) def select_from(self, from_obj): """Set the `from_obj` parameter of the query and return the newly resulting ``Query``. This replaces the table which this Query selects @@ -915,9 +907,7 @@ class Query(object): if isinstance(from_obj, (tuple, list)): util.warn_deprecated("select_from() now accepts a single Selectable as its argument, which replaces any existing FROM criterion.") from_obj = from_obj[-1] - self.__set_select_from(from_obj) - select_from = _generative(__no_from_condition, __no_criterion_condition)(select_from) def __getitem__(self, item): if isinstance(item, slice): @@ -934,10 +924,10 @@ class Query(object): return list(res) else: return list(self[item:item+1])[0] - + + @_generative(__no_statement_condition) def slice(self, start, stop): """apply LIMIT/OFFSET to the ``Query`` based on a range and return the newly resulting ``Query``.""" - if start is not None and stop is not None: self._offset = (self._offset or 0) + start self._limit = stop - start @@ -945,34 +935,31 @@ class Query(object): self._limit = stop elif start is not None and stop is None: self._offset = (self._offset or 0) + start - slice = _generative(__no_statement_condition)(slice) - + + @_generative(__no_statement_condition) def limit(self, limit): """Apply a ``LIMIT`` to the query and return the newly resulting ``Query``. """ - self._limit = limit - limit = _generative(__no_statement_condition)(limit) - + + @_generative(__no_statement_condition) def offset(self, offset): """Apply an ``OFFSET`` to the query and return the newly resulting ``Query``. """ - self._offset = offset - offset = _generative(__no_statement_condition)(offset) - + + @_generative(__no_statement_condition) def distinct(self): """Apply a ``DISTINCT`` to the query and return the newly resulting ``Query``. """ self._distinct = True - distinct = _generative(__no_statement_condition)(distinct) def all(self): """Return the results represented by this ``Query`` as a list. @@ -982,6 +969,7 @@ class Query(object): """ return list(self) + @_generative(__no_criterion_condition) def from_statement(self, statement): """Execute the given SELECT statement and return results. @@ -998,7 +986,6 @@ class Query(object): if isinstance(statement, basestring): statement = sql.text(statement) self._statement = statement - from_statement = _generative(__no_criterion_condition)(from_statement) def first(self): """Return the first result of this ``Query`` or None if the result doesn't contain any row. @@ -1445,11 +1432,12 @@ class Query(object): def _adjust_for_single_inheritance(self, context): """Apply single-table-inheritance filtering. - - For all distinct single-table-inheritance mappers represented in the columns - clause of this query, add criterion to the WHERE clause of the given QueryContext - such that only the appropriate subtypes are selected from the total results. - + + For all distinct single-table-inheritance mappers represented in the + columns clause of this query, add criterion to the WHERE clause of the + given QueryContext such that only the appropriate subtypes are + selected from the total results. + """ for entity, (mapper, adapter, s, i, w) in self._mapper_adapter_map.iteritems(): if mapper.single and mapper.inherits and mapper.polymorphic_on and mapper.polymorphic_identity is not None: diff --git a/lib/sqlalchemy/util.py b/lib/sqlalchemy/util.py index e351b53a1..434ad4c7b 100644 --- a/lib/sqlalchemy/util.py +++ b/lib/sqlalchemy/util.py @@ -103,31 +103,93 @@ def to_list(x, default=None): else: return x -def array_as_starargs_decorator(fn): - """Interpret a single positional array argument as - *args for the decorated method. +try: + from functools import update_wrapper +except ImportError: + def update_wrapper(wrapper, wrapped, + assigned=('__doc__', '__module__', '__name__'), + updated=('__dict__',)): + for attr in assigned: + setattr(wrapper, attr, getattr(wrapped, attr)) + for attr in updated: + getattr(wrapper, attr).update(getattr(wrapped, attr, ())) + return wrapper + +def accepts_a_list_as_starargs(list_deprecation=None): + def decorate(fn): - """ - def starargs_as_list(self, *args, **kwargs): - if isinstance(args, basestring) or (len(args) == 1 and not isinstance(args[0], tuple)): - return fn(self, *to_list(args[0], []), **kwargs) + spec = inspect.getargspec(fn) + assert spec[1], 'Decorated function does not accept *args' + + meta = format_argspec_plus(spec) + meta['name'] = fn.func_name + meta['varg'] = spec[1] + scratch = list(spec) + scratch[1] = '(%s[0])' % scratch[1] + meta['unpacked_pos'] = format_argspec_plus(scratch)['apply_pos'] + + def _deprecate(): + if list_deprecation: + if list_deprecation == 'pending': + warning_type = exc.SAPendingDeprecationWarning + else: + warning_type = exc.SADeprecationWarning + msg = ( + "%s%s now accepts multiple %s arguments as a " + "variable argument list. Supplying %s as a single " + "list is deprecated and support will be removed " + "in a future release." % ( + fn.func_name, + inspect.formatargspec(*spec), + spec[1], spec[1])) + warnings.warn(msg, warning_type, stacklevel=3) + + code = "\n".join(( + "def %(name)s%(args)s:", + " if len(%(varg)s) == 1 and isinstance(%(varg)s[0], list):", + " _deprecate()", + " return fn%(unpacked_pos)s", + " else:", + " return fn%(apply_pos)s")) % meta + + env = locals().copy() + exec code in env + decorated = env[fn.func_name] + update_wrapper(decorated, fn) + decorated.generated_src = code + return decorated + return decorate + +def unique_symbols(used, *bases): + used = set(used) + for base in bases: + pool = itertools.chain((base,), + itertools.imap(lambda i: base + str(i), + xrange(1000))) + for sym in pool: + if sym not in used: + used.add(sym) + yield sym + break else: - return fn(self, *args, **kwargs) - starargs_as_list.__doc__ = fn.__doc__ - return function_named(starargs_as_list, fn.__name__) + raise NameError("exhausted namespace for symbol base %s" % base) -def array_as_starargs_fn_decorator(fn): - """Interpret a single positional array argument as - *args for the decorated function. +def decorator(target): + """A signature-matching decorator factory.""" - """ - def starargs_as_list(*args, **kwargs): - if isinstance(args, basestring) or (len(args) == 1 and not isinstance(args[0], tuple)): - return fn(*to_list(args[0], []), **kwargs) - else: - return fn(*args, **kwargs) - starargs_as_list.__doc__ = fn.__doc__ - return function_named(starargs_as_list, fn.__name__) + def decorate(fn): + spec = inspect.getargspec(fn) + names = tuple(spec[0]) + spec[1:3] + (fn.func_name,) + targ_name, fn_name = unique_symbols(names, 'target', 'fn') + + metadata = dict(target=targ_name, fn=fn_name) + metadata.update(format_argspec_plus(spec, grouped=False)) + + code = 'lambda %(args)s: %(target)s(%(fn)s, %(apply_kw)s)' % ( + metadata) + decorated = eval(code, {targ_name:target, fn_name:fn}) + return update_wrapper(decorated, fn) + return update_wrapper(decorate, target) def to_set(x): if x is None: @@ -233,7 +295,7 @@ def format_argspec_plus(fn, grouped=True): A enhanced variant of inspect.formatargspec to support code generation. fn - An inspectable callable + An inspectable callable or tuple of inspect getargspec() results. grouped Defaults to True; include (parens, around, argument) lists @@ -259,7 +321,7 @@ def format_argspec_plus(fn, grouped=True): 'apply_pos': '(self, a, b, c, **d)'} """ - spec = inspect.getargspec(fn) + spec = callable(fn) and inspect.getargspec(fn) or fn args = inspect.formatargspec(*spec) if spec[0]: self_arg = spec[0][0] |