I want something similar to a function wrapped with contextlib.contextmanger
but can optionally return the a non-context object if used outside of a context call (e.g., with
or __enter__()
)
A silly example is this mutability switch.
def foobar(wrapped: tuple) -> AbstractContextManager | tuple:
if ... : # WHAT GOES HERE?
# Handles context called (e.g., `with` is used)
mutable_wrapped = list(wrapped)
try:
yield mutable_wrapped
finally:
return tuple(mutable_wrapped)
else:
return tuple(wrapped)
a = (1,2,3)
# Behavior using context
with foobar(a) as b:
b.append(4)
assert b is not a
assert baz == (1,2,3,4)
# Behavior not using context
c = foobar(a)
assert c is not a
assert c == a
My real use case is to design a immutable nested data object that can only be mutated during a copy (in context).
The code example above is very wrong. Python does not support returning on __exit__()
and you can only change the yielded object by changing its internal state (nb: adding this could be a PEP).
Regardless, I hope the example at least present my intentions.
I think you might just need a class that defines __enter__
and __call__
. I don’t think you can do this with one function.
But a “working” example (i.e. one that would work and make sense, if this thing worked) might help clarify your intent.
Here’s a working example that involves storing a lot of private state (undesired) and have a few undesirables:
class MyTuple():
def __init__(self, *args, _copied=False):
self._data = list(args)
self._copied = _copied
self._in_context = False
def append(self, value):
if self._copied and self._in_context:
self._data.append(value)
else:
raise TypeError("MyTuple cannot append outside of a 'with self.copy() as copied:")
def __enter__(self):
self._in_context = True
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._in_context = False
self._mutable_in_context = False
def __eq__(self, other):
return self._data == other._data
def copy(self):
return MyTuple(*self._data, _copied=True)
a = MyTuple(1,2,3)
# Intended behavior
with a.copy() as b:
b.append(4)
assert b is not a
assert b == MyTuple(1,2,3,4)
# Should raise TypeError
with a as c:
c.append(4)
Undesirables:
MyTuple
has to define __enter__
and __exit__
which I feel it doesn’t belong
- Need to maintain private state variables
- Awkward constructor argument of
_copied
To me it’s seems a lot of work just to allow this special behavior when copy()
is called.
This is a pretty different example, in the sense that you actually modify the data in place rather than returning a new copy. In your original example b
would be a list, not a new tuple.
But have you considered subclassing tuple
itself:
from contextlib import contextmanager
class MyTuple(tuple):
@contextmanager
def copy(self):
yield list(self)
a = MyTuple((1,2,3)) # just like tuple(), this takes one arg
# Intended behavior
with a.copy() as b:
b.append(4)
assert b is not a
assert b == MyTuple((1,2,3,4)) # this fails
assert b == [1,2,3,4] # this works
assert tuple(b) == MyTuple((1,2,3,4)) # or this
# raises TypeError
with a as c:
c.append(4)
So I was wrong, you can do this with one function, if “this” is what you wrote in the OP, not the follow-up 
The examples being different doesn’t change intent I introduced in the OP—to return a non-context object if outside the context call.
Thank you for taking the time to make an example. Your implementation will not allow the following:
b = a.copy()
assert b is not a
assert b==a
I don’t think there’s a way for the method to know it is being used in a with
statement, so you can’t have one method that does both of these things. You can use a different method to copy, like a[:]
, for those cases.
1 Like
This StackOverflow question introduces more reason that I agree with. It seems there is no way around needing more than 1 call syntax with the exception of
with factory.create() as product:
product.a = 1
with factory.create(a=2) as product:
pass
Other solutions requires multiple call syntax:
# Defining a dedicated context method
with factory.create_with_context() as product:
product.a = 1
product = factory.create(a=2)
I especially love the last answer which gives you:
# Take advantage of @property
with factory.create as product:
product.a = 1
product = factory.create(a=2)