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:
...
an
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.