I would like to ensure that the class is only instantiated within a "with" statement.
i.e. this one is ok:
with X() as x:
...
and this is not:
x = X()
How can I ensure such functionality?
I would like to ensure that the class is only instantiated within a "with" statement.
i.e. this one is ok:
with X() as x:
...
and this is not:
x = X()
How can I ensure such functionality?
All answers so far do not provide what (I think) OP wants directly.
(I think) OP wants something like this:
>>> with X() as x:
... # ok
>>> x = X() # ERROR
Traceback (most recent call last):
File "run.py", line 18, in <module>
x = X()
File "run.py", line 9, in __init__
raise Exception("Should only be used with `with`")
Exception: Should only be used with `with`
This is what I come up with, it may not be very robust, but I think it's closest to OP's intention.
import inspect
import linecache
class X():
def __init__(self):
if not linecache.getline(__file__,
inspect.getlineno(inspect.currentframe().f_back)
).startswith("with "):
raise Exception("Should only be used with `with`")
def __enter__(self):
return self
def __exit__(self, *exc_info):
pass
This will give the exact same output as I showed above as long as with
is in the same line with X()
when using context manager.
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)
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.
Unfortunately, you can't very cleanly.
Context managers require having __enter__
and __exit__
methods, so you can use this to assign a member variable on the class to check in your code.
class Door(object):
def __init__(self, state='closed'):
self.state = state
self.called_with_open = False
# When being called as a non-context manger object,
# __enter__ and __exit__ are not called.
def __enter__(self):
self.called_with_open = True
self.state = 'opened'
def __exit__(self, type, value, traceback):
self.state = 'closed'
def was_context(self):
return self.called_with_open
if __name__ == '__main__':
d = Door()
if d.was_context():
print("We were born as a contextlib object.")
with Door() as d:
print('Knock knock.')
The stateful object approach has the nice added benefit of being able to tell if the __exit__
method was called later, or to cleanly handle method requirements in later calls:
def walk_through(self):
if self.state == 'closed':
self.__enter__
walk()
Here is a decorator that automates making sure methods aren't called outside of a context manager:
from functools import wraps
BLACKLIST = dir(object) + ['__enter__']
def context_manager_only(cls):
original_init = cls.__init__
def init(self, *args, **kwargs):
original_init(self, *args, **kwargs)
self._entered = False
cls.__init__ = init
original_enter = cls.__enter__
def enter(self):
self._entered = True
return original_enter(self)
cls.__enter__ = enter
attrs = {name: getattr(cls, name) for name in dir(cls) if name not in BLACKLIST}
methods = {name: method for name, method in attrs.items() if callable(method)}
for name, method in methods.items():
def make_wrapper(method=method):
@wraps(method)
def wrapper_method(self, *args, **kwargs):
if not self._entered:
raise Exception("Didn't get call to __enter__")
return method(self, *args, **kwargs)
return wrapper_method
setattr(cls, name, make_wrapper())
return cls
And here is an example of it in use:
@context_manager_only
class Foo(object):
def func1(self):
print "func1"
def func2(self):
print "func2"
def __enter__(self):
print "enter"
return self
def __exit__(self, *args):
print "exit"
try:
print "trying func1:"
Foo().func1()
except Exception as e:
print e
print "trying enter:"
with Foo() as foo:
print "trying func1:"
foo.func1()
print "trying func2:"
foo.func2()
print "trying exit:"
This was written as an answer to this duplicate question.