How to check if an object is created with `with` statement?
There is no straight forward way, as far as I know. But, you can have a boolean flag, to check if __enter__
was invoked, before the actual methods in the objects were called.
class MyContextManager(object):
def __init__(self):
self.__is_context_manager = False
def __enter__(self):
print "Entered"
self.__is_context_manager = True
return self
def __exit__(self, exc_type, exc_value, traceback):
print "Exited"
def do_something(self):
if not self.__is_context_manager:
raise Exception("MyContextManager should be used only with `with`")
print "I don't know what I am doing"
When you use it with with
,
with MyContextManager() as y:
y.do_something()
you will get
Entered
I don't know what I am doing
Exited
But, when you manually create an object, and invoke do_something
,
x = MyContextManager()
x.do_something()
you will get
Traceback (most recent call last):
File "/home/thefourtheye/Desktop/Test.py", line 22, in <module>
x.do_something()
File "/home/thefourtheye/Desktop/Test.py", line 16, in do_something
raise Exception("MyContextManager should be used only with `with`")
Exception: MyContextManager should be used only with `with`
Note: This is not a solid solution. Somebody can directly invoke __enter__
method alone, before calling any other methods and the __exit__
method may never be called in that case.
If you don't want to repeat that check in every function, you can make it a decorator, like this
class MyContextManager(object):
def __init__(self):
self.__is_context_manager = False
def __enter__(self):
print "Entered"
self.__is_context_manager = True
return self
def __exit__(self, exc_type, exc_value, traceback):
print "Exited"
def ensure_context_manager(func):
def inner_function(self, *args, **kwargs):
if not self.__is_context_manager:
raise Exception("This object should be used only with `with`")
return func(self, *args, **kwargs)
return inner_function
@ensure_context_manager
def do_something(self):
print "I don't know what I am doing"
There is no foolproof approach to ensure that an instance is constructed within a with
clause, but you can create an instance in the __enter__
method and return that instead of self
; this is the value that will be assigned into x
. Thus you can consider X
as a factory that creates the actual instance in its __enter__
method, something like:
class ActualInstanceClass(object):
def __init__(self, x):
self.x = x
def destroy(self):
print("destroyed")
class X(object):
instance = None
def __enter__(self):
# additionally one can here ensure that the
# __enter__ is not re-entered,
# if self.instance is not None:
# raise Exception("Cannot reenter context manager")
self.instance = ActualInstanceClass(self)
return self.instance
def __exit__(self, exc_type, exc_value, traceback):
self.instance.destroy()
return None
with X() as x:
# x is now an instance of the ActualInstanceClass
Of course this is still reusable, but every with
statement would create a new instance.
Naturally one can call the __enter__
manually, or get a reference to the ActualInstanceClass
but it would be more of abuse instead of use.
For an even smellier approach, the X()
when called does actually create a XFactory
instance, instead of an X
instance; and this in turn when used as a context manager, creates the ActualX
instance which is the subclass of X
, thus isinstance(x, X)
will return true.
class XFactory(object):
managed = None
def __enter__(self):
if self.managed:
raise Exception("Factory reuse not allowed")
self.managed = ActualX()
return self.managed
def __exit__(self, *exc_info):
self.managed.destroy()
return
class X(object):
def __new__(cls):
if cls == X:
return XFactory()
return super(X, cls).__new__(cls)
def do_foo(self):
print("foo")
def destroy(self):
print("destroyed")
class ActualX(X):
pass
with X() as x:
print(isinstance(x, X)) # yes it is an X instance
x.do_foo() # it can do foo
# x is destroyed
newx = X()
newx.do_foo() # but this can't,
# AttributeError: 'XFactory' object has no attribute 'do_foo'
You could take this further and have XFactory
create an actual X
instance with a special keyword argument to __new__
, but I consider it to be too black magic to be useful.