[Python-ideas] Proposal for new-style decorators

Mathias Panzenböck grosser.meister.morti at gmx.net
Fri Apr 29 18:37:46 CEST 2011


I thought about this problem for I wile and I came up with this @decorator:


Usage:
------
 >>> @decorator
 >>> def my_deco(func,func_args,func_kwargs,deco_args...):
 >>> 	pass
 >>>
 >>> @my_deco(*deco_args,**deco_kwargs)
 >>> def func(*func_args,**func_kwargs):
 >>> 	pass

Or a more specific example:
 >>> @decorator
 >>> def deco(func,func_args,func_kwargs,a,b=12,**kwargs):
 >>> 	return (func(*func_args,**func_kwargs),func_args,func_kwargs,a,b,kwargs)
 >>>
 >>> @deco(1,f=12)
 >>> def foo(x,y,z=2,*args):
 >>> 	return (x,y,z,args)
 >>>
 >>> foo(5,6)
((5, 6, 2, ()), (5, 6, 2), {}, 1, 12, {'f': 12})

This fully supports *args and **kwargs besides regular arguments for the decorator and the decorated 
function. By that I mean func_args already contains the filled in default values of func as well as 
any regular arguments that where passed as keyword argument and argument passing errors are handled 
(e.g. passing an argument as positional and keyword argument).


Error handling example:
 >>> @deco(1,2,3)
 >>> def bar(x,y,z=2,*args):
 >>> 	return (x,y,z,args)
 >>>

Traceback (most recent call last):
   File "<pyshell#114>", line 1, in <module>
     @deco(1,2,3)
   File "<pyshell#96>", line 22, in _deco_deco
     deco_args, deco_kwargs = apply_deco_args(*deco_args, **deco_kwargs)
TypeError: deco() takes at most 2 arguments (3 given)

Or:
 >>> foo(5,6,y=33)

Traceback (most recent call last):
   File "<pyshell#112>", line 1, in <module>
     foo(5,6,y=33)
   File "<pyshell#96>", line 27, in _f
     func_args, func_kwargs = apply_func_args(*func_args, **func_kwargs)
TypeError: foo() got multiple values for keyword argument 'y'


Of course that always needs function call parenthesis on the decorator, even if the decorator does 
not take any arguments. Maybe it could be extended that in this case a more simple decorator 
mechanism is used. A decorator-decorator for decorators without arguments would be very simple (see 
end of mail).


Implementation:
---------------
from types import FunctionType, ClassType
from functools import wraps

def inspect_callable(func):
	"""-> (arg_names, co_flags, func_defaults, func_name)"""
	return _inspect_callable(func,set())

def _inspect_callable(func,visited):
	if func in visited:
		raise TypeError("'%s' object is not callable" % type(func).__name__)
	visited.add(func)
	if isinstance(func, FunctionType):
		co = func.func_code
		func_name = func.__name__
		arg_names = list(co.co_varnames[0:co.co_argcount])
		defaults = func.func_defaults
		flags = co.co_flags
	elif isinstance(func, ClassType):
		func_name = func.__name__
		arg_names, flags, defaults, member_name = _inspect_callable(func.__init__,visited)
		if arg_names:
			del arg_names[0]
	elif hasattr(func, '__call__'):
		func_name = '<%s object at 0x%x>' % (type(func).__name__, id(func))
		arg_names, flags, defaults, member_name = _inspect_callable(func.__call__,visited)
	else:
		raise TypeError("'%s' object is not callable" % type(func).__name__)
	return arg_names, flags, defaults, func_name

FUNC_ARGS   = 0x04
FUNC_KWARGS = 0x08
FUNC_GEN    = 0x20

# this function should probably be reimplemented in C:
def args_applyer(arg_names,flags=0,defaults=None,func_name=None):
	"""-> f(args..., [*varargs], [**kwargs]) -> ((args...)+varargs, kwargs)"""
	all_args = list(arg_names)
	if arg_names:
		body = ['(',','.join(arg_names),')']
	else:
		body = []
	if flags & FUNC_ARGS:
		args_name = '_args'
		i = 0
		while args_name in arg_names:
			args_name = '_args'+i
			i += 1
		all_args.append('*'+args_name)
		if arg_names:
			body.append('+')
		body.append(args_name)
	elif not arg_names:
		body.append('()')
	body.append(',')
	if flags & FUNC_KWARGS:
		kwargs_name = '_kwargs'
		i = 0
		while kwargs_name in arg_names:
			kwargs_name = '_kwargs'+i
			i += 1
		all_args.append('**'+kwargs_name)
		body.append(kwargs_name)
	else:
		body.append('{}')
	if func_name:
		apply_args = named_lambda(func_name,all_args,''.join(body))
	else:
		apply_args = eval('lambda %s: (%s)' % (','.join(all_args), ''.join(body)))
	if defaults:
		apply_args.func_defaults = defaults
	return apply_args

def named_lambda(name,args,body):
	code = 'def _named_lambda():\n\tdef %s(%s):\n\t\treturn %s\n\treturn %s' % (
		name, ','.join(args), body, name)
	del name, args, body
	exec(code)
	return _named_lambda()

# begin helper functions (not used by this module but might be handy for decorator developers)
def args_applyer_for(func):
	return args_applyer(*inspect_callable(func))

def apply_args(args,kwargs,arg_names,flags=0,defaults=None,func_name=None):
	return args_applyer(arg_names,flags,defaults,func_name)(*args,**kwargs)

def apply_args_for(func,args,kwargs):
	return args_applyer(*inspect_callable(func))(*args,**kwargs)
# end helper functions

def decorator(deco):
	"""deco(func,func_args,func_kwargs,deco_args...)
	
	@decorator
	def my_deco(func,func_args,func_kwargs,deco_args...):
		pass
	
	@my_deco(*deco_args,**deco_kwargs)
	def func(*func_args,**func_kwargs):
		pass
	"""
	arg_names, flags, defaults, deco_name = inspect_callable(deco)
	if flags & FUNC_ARGS == 0:
		if len(arg_names) < 3:
			raise TypeError('decorator functions need at least 3 ' +
				'arguments (func, func_args, func_kwargs)')
		del arg_names[0:3]
	apply_deco_args = args_applyer(arg_names,flags,defaults,deco_name)
	del flags, defaults
	@wraps(deco)
	def _deco_deco(*deco_args,**deco_kwargs):
		deco_args, deco_kwargs = apply_deco_args(*deco_args, **deco_kwargs)
		def _deco(func):
			apply_func_args = args_applyer(*inspect_callable(func))
			@wraps(func)
			def _f(*func_args,**func_kwargs):
				func_args, func_kwargs = apply_func_args(*func_args, **func_kwargs)
				return deco(func,func_args,func_kwargs,*deco_args,**deco_kwargs)
			return _f
		return _deco
	return _deco_deco

def simple_decorator(deco):
	"""deco(func,func_args,func_kwargs)
	
	@simple_decorator
	def my_deco(func,func_args,func_kwargs):
		pass
	
	@my_deco
	def func(*func_args,**func_kwargs):
		pass
	"""
	@wraps(deco)
	def _deco(func):
		apply_func_args = args_applyer(*inspect_callable(func))
		@wraps(func)
		def _f(*args,**kwargs):
			return deco(func,*apply_func_args(*args,**kwargs))
		return _f
	return _deco




More information about the Python-ideas mailing list