Welcome, guest | Sign In | My Account | Store | Cart

As with version 1 of this recipe, it was sparked by a discussion on python-ideas about adding a special syntax to function signatures for reevaluating the arguments to a function at runtime. The below is a decorator and annotation based solution to this problem which stores the code to be evaluated as a string in the annotations, rather than reevaluating the entire function every time it is called.

Python, 113 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#! /usr/bin/env python3

"""
runtime.py

Written by Geremy Condra

Licensed under GPLv3

Released 14 May 2009

This module provides a simple decorator used
to reevaluate function arguments at runtime
based on their annotations.
"""

from inspect import getfile, getfullargspec
from functools import wraps

def runtime(f):
	"""Evaluates a function's annotations at runtime.

	Usage:
		>>> @runtime
		... def f(x, y:'[]'):
		... 	y.append(x)
		... 	return y
		...
		>>> f(1)
		[1]
		>>> f(2)
		[2]

	Arguments evaluated at runtime must be treated as
	though they were keyword-only arguments for the
	purposes of assignment.

	Good:
		>>> f(4, y=[1, 2, 3])
		[1, 2, 3, 4]

	Bad:
		>>> f(4, [1, 2, 3])
		TypeError: f() got multiple values for keyword argument 'y'

	For this reason you should always make the arguments
	you want evaluated at runtime the last non-keyword
	arguments to your function.

	If you need a varargs argument, just place your
	runtime-evaluated arguments afterwards.

	Good:
		>>> @runtime
		... def f(*args, z:'[]'):
		... 	z.extend(args)
		... 	return z
		...
		>>> f(1, 2, 3, 4)
		[1, 2, 3, 4]
		>>> f(4, 5, 6, 7, z=[1, 2, 3])
		[1, 2, 3, 4, 5, 6, 7]

	Bad:
		>>> @runtime
		... def f(z:'[]', *args):
		... 	z.extend(args)
		... 	return z
		...
		>>> f(1, 2, 3, 4)
		TypeError: f() got multiple values for keyword argument 'z'
	"""

	# get the functions' file of origin
	filename = getfile(f)

	# build the evaluatable annotations table
	comp = lambda stmt: compile(stmt, filename, 'eval')
	defaults = {k: comp(v) for k, v in getfullargspec(f)[-1].items()}

	# build the wrapping function
	@wraps(f)
	def wrapped(*args, **kwargs):
		# update kwargs with the unfilled defaults, evaluated at runtime
		for k, v in defaults.items():
			if k not in kwargs:
				kwargs[k] = eval(v)
		return f(*args, **kwargs)

	# and return it
	return wrapped

@runtime
def example1(x, y:'[]'):
	y.append(x)
	return y

@runtime
def example2(*, x:'a**2+2*b+c'):
	return x

if __name__ == "__main__":
	print("Testing example1")
	print(example1(1))
	print(example1(2))
	print(example1(3))
	print()
	print("Testing example2 with values 0, 1, 2")
	a, b, c = 0, 1, 2
	print(example2())
	print("Changing a to 5")
	a = 5
	print(example2())
Created by geremy condra on Thu, 14 May 2009 (MIT)
Python recipes (4591)
geremy condra's recipes (8)

Required Modules

  • (none specified)

Other Information and Tasks