]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Overhauled _generative and starargs decorators and flipped to 2.4 @syntax
authorJason Kirtland <jek@discorporate.us>
Wed, 16 Jul 2008 06:47:22 +0000 (06:47 +0000)
committerJason Kirtland <jek@discorporate.us>
Wed, 16 Jul 2008 06:47:22 +0000 (06:47 +0000)
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/util.py

index 5905c71115fc9cac9b6a16f96710a46d71f8d37d..45982ab2ed67afc4455e24c484e6c6620585513d 100644 (file)
@@ -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
index 392e9647d649aa3b94895288187efa7b610af377..f84a2d30e0b884334ac9a0c461afe18f3f31455d 100644 (file)
@@ -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:
index e351b53a14574a56292086a622c70359625c8a16..434ad4c7b16025dc2577064e21e5355ba12b2d33 100644 (file)
@@ -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]