Reputation: 862
I have a Python 3 script similar to the following:
def transaction(write=False):
def _transaction_deco(fn):
@wraps(fn)
def _wrapper(*args, **kwargs):
env.timestamp = arrow.utcnow()
with TxnManager(write=write) as txn:
ret = fn(*args, **kwargs)
delattr(env, 'timestamp')
return ret
return _wrapper
return _transaction_deco
@transaction(True)
def fn1(x=False):
if x:
return fn2()
do_something()
@transaction(True)
def fn2():
do_something_else()
fn1
and fn2
can be called independently, and both are wrapped in the transaction
decorator.
Also, fn1
can call fn2
under certain circumstances. This is where I have a problem, because the decorator code is called twice, transactions are nested, and the timestamp is set twice and deleted twice (actually, the second time delattr(env, 'timestamp')
is called, an exception is raised because timestamp
is no longer there).
I want whatever is decorated with transaction
to only be decorated if not already so.
I tried to modify my code based on hints from a separate Q&A:
def transaction(write=False):
def _transaction_deco(fn):
# Avoid double wrap.
if getattr(fn, 'in_txn', False):
return fn(*args, **kwargs)
@wraps(fn)
def _wrapper(*args, **kwargs):
# Mark transaction begin timestamp.
env.timestamp = arrow.utcnow()
with TxnManager(write=write) as txn:
ret = fn(*args, **kwargs)
delattr(env, 'timestamp')
return ret
_wrapper.in_txn = True
return _wrapper
return _transaction_deco
I.e. if the _in_txn
attribute is set for the function, return the bare function because we are already in a transaction.
However, if I run fn1(True)
with a debugger, I see that the check for in_txn
is never true because the _wrapper
variable is set to f1
for the outer call, and to f2
for the inner call, so the functions are being both decorated separately.
I could set the timestamp variables as properties of the context manager, but I still end up with two bested context managers, and if both trnsactions are R/W, Bad Things will happen.
Can someone suggest a better solution?
Edit: Based on the replied received, I believe I omitted a key element: fn1
and fn2
are API metods and must always run within a transaction. The reason for using a decorator is to prevent the API implementer from having to make a decision about the transaction, or having to wrap the methods manually in a context manager or a decorator. However I would be in favor of a non-decorator approach as long as it keeps the implementation of fn1
and fn2
plain.
Upvotes: 1
Views: 698
Reputation: 531345
Rather than a decorator, I'd just make another context manager to use explicitly.
@contextlib.contextmanager
def MyTxnManager(write):
env.timestamp = arrow.utcnow()
with TxnManager(write=write) as txn:
yield txn
delattr(env, 'timestamp')
with MyTxnManager(True) as txn:
fn1()
Here, instead of hiding the transaction inside fn1
and fn2
, we wrap just the timestamp management inside a new transaction manager. This way, calls to fn1
and fn2
occur in a transaction only if we explicitly cause them to do so.
Upvotes: 4
Reputation: 114330
You can always just check if the environment has a timestamp already and not create it in that case. It is using the already-existing attribute as a flag instead of making a new one:
def transaction(write=False): def _transaction_deco(fn): @wraps(fn) def _wrapper(*args, **kwargs): if hasattr(env, 'timestamp'): ret = fn(*args, **kwargs) else: env.timestamp = arrow.utcnow() with TxnManager(write=write) as txn: ret = fn(*args, **kwargs) delattr(env, 'timestamp') return ret return _wrapper return _transaction_deco
This will let you nest an arbitrary number of transacted calls, and only use the timestamp and context manager of the outermost one.
Upvotes: 0
Reputation: 122061
There's no need to handle this in the decorator. It's much simpler to separate out the wrapped and unwrapped versions of fn2
, e.g.:
def _fn2():
do_something_else()
@transaction(True)
def fn1(x=False):
if x:
return _fn2()
do_something()
fn2 = transaction(True)(_fn2)
The decorator syntax is just syntactic sugar, so the result will be exactly the same.
Upvotes: 2