How to check if an object is created with `with` statement?
Asked Answered
S

7

17

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?

Stercoricolous answered 14/2, 2015 at 15:41 Comment(5)
Why in heavens name would you ever want to do that? x = X(), with x as result_of_entering: (creating the CM and using it on two separate lines) is a valid use-case! What if I wanted to store CMs in a mapping to select one dynamically, or use an contextlib.ExitStack()? to combine multiple CMs? There are numerous use-cases where a CM is created outside of a with statement that you would block. Don't try and fix all possible errors at the expense of making it harder for those that know what they are doing.Wad
This is exactly the question I was just trying to figure out, and -- in all honesty, @MartijnPieters -- while I understand you're very passionate about your answer, I can't for the life of me figure out exactly what you're so adamant that I do instead.Hambletonian
@LaurentStanevich do you want to prevent x = X() from working? Can you tell me why you think that that is needed? Python doesn’t see assignment of objects as anything that needs to be prevented or special. The X() call expression in x = X() and with X() as x: is treated exactly the same by Python, both put the result on the stack so the next instruction (the assignment or with statement block setup) can work with that object. Preventing assignment doesn’t make sense here and would actively break important use-cases.Wad
@LaurentStanevich: anyway, that highlights, again, that both the OP and you have not told us why you think you need this. This has all the hallmarks of an XY problem, an attempt to solve a bigger problem. If you were to step back and go back to the original problem you thought the idea behind this question would be a solution for, then I could possibly help.Wad
@LaurentStanevich: for any scenario I can think of that would make a reasonable X for the Y represented by this question, the correct answer was written by Antti Haapala: by implementing a distinct object type to be returned from __enter__, so the thing that's assigned to target in the following three lines x = X() / with x as target: / # do stuff with target. Then x = X() is 'harmless', as that's not the same type of object as target.Wad
G
7

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)).lstrip(
        ).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.

Graecoroman answered 14/2, 2015 at 17:13 Comment(7)
And this is why we can't have nice things.Wad
Works only with with that is at module levelBordy
I know you don't like it, I don't like this solution too, but I still post it here cause that's what OP wants.Graecoroman
Awesome, wasn't familiar with linecache and friends. I would add .strip() before .startswith("with ") in case it's not top levelPoliclinic
Inspecting the line of code that instantiated the class... shivers... this should never be done in any production level code.Summertime
I'd like to file a bug report! Class behaves differently when I do while True: with X() as x:.Endogamy
Actually on further thought, sorry but I have to downvote this. Some answers are more harmful than their absence, because their absense might at least force a struggle that ends in epiphany - the fact that OP accepted this answer makes me think odds are high that this answer kept OP in the dark. Somewhere out there, people might be having their days ruined and hours wasted because you taught someone this trick. The real solution for any justified reason to do this is to put the functionality that needs context manager protection in another class that the context manager's __enter__ returns.Endogamy
D
22

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"
Davon answered 14/2, 2015 at 16:4 Comment(8)
You could use __getattribute__ to really nail it down for everythingLorelle
I wouldn't say that this isn't a solid solution. Anything more would be overkill/harmful, since being able to invoke enter/exit directly is quite important at times (e.g. contextlib.ExitStack).Cwm
Function decorator to ensure the state is what you want it to be seems very clean and pythonic. Nice.Tope
Why go through all the bother of having a state switch? A better approach would be to not return self from __enter__. Return a different object, and have that object implement do_something(). So MyContextManager() doesn't have a do_something() method at all. The object returned from __enter__ does.Wad
@MartijnPieters I guess the difference between the "state switch" approach and your "return a _Different object" approach is, the latter technically does NOT prevent the caller from initializing the _Different object directly. But I get your point. :-)Slippy
@RayLuo: Python is highly dynamic, you'll just have to trust the developer using the API in that case. The real difference is that a single MyContextManager() instance can be used for multiple contexts if __enter__ produces an independent object.Wad
@Slippy The former "technically does NOT prevent the caller from" setting .__is_context_manager = True directly either.Endogamy
While I do think Antti's answer returning a different context object is a cleaner approach overall, this seems reasonably clear and functional to me as well.Examinee
U
12

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.

Unpracticed answered 14/2, 2015 at 16:19 Comment(4)
Clever! This solution has a bit of a smell to it (as it silently returns a different object), but it's cleaner than what I've been able to come up with.Colima
Why is that a "smell"? The context manager protocol is specifically designed to return an arbitrary object from __enter__, so you can produce a new, special object to use in the context you just entered. That's what many database connections do, for example: return a transaction or cursor for a context.Wad
Unfortunately, the OP never articulated why they thought they needed to prevent assignment of a context manager instance, but if we treat the question as a XY problem with X being: people use the context manager wrong, they act on the object without entering the context, then the right answer to that question is to return a different object for the context, that's what returning something from __enter__ is for.Wad
@MartijnPieters though I am pretty sure user559633 won't be continuing this discussion any further :DBordy
G
7

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)).lstrip(
        ).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.

Graecoroman answered 14/2, 2015 at 17:13 Comment(7)
And this is why we can't have nice things.Wad
Works only with with that is at module levelBordy
I know you don't like it, I don't like this solution too, but I still post it here cause that's what OP wants.Graecoroman
Awesome, wasn't familiar with linecache and friends. I would add .strip() before .startswith("with ") in case it's not top levelPoliclinic
Inspecting the line of code that instantiated the class... shivers... this should never be done in any production level code.Summertime
I'd like to file a bug report! Class behaves differently when I do while True: with X() as x:.Endogamy
Actually on further thought, sorry but I have to downvote this. Some answers are more harmful than their absence, because their absense might at least force a struggle that ends in epiphany - the fact that OP accepted this answer makes me think odds are high that this answer kept OP in the dark. Somewhere out there, people might be having their days ruined and hours wasted because you taught someone this trick. The real solution for any justified reason to do this is to put the functionality that needs context manager protection in another class that the context manager's __enter__ returns.Endogamy
C
2

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()
Colima answered 14/2, 2015 at 16:28 Comment(1)
This answer is completely forgetting about the return value from __enter__.Wad
S
2

OP's question was believed to be an XY problem, and the current chosen answer was indeed (too?) hacky.

I don't really know the OP's original "X problem", but I'd assume the motivation was NOT literally about to "prevent x = X() ASSIGNMENT from working". Instead, it could be about to force the API user to always use x as a context manager, so that its __exit__(...) would always be triggered, which is the whole point of designing class X to be a context manager in the first place. At least, that was the reason brought me to this Q&A post.

class Holder(object):
    def __init__(self, **kwargs):
        self._data = allocate(...)  # Say, it allocates 1 GB of memory, or a long-lived connection, etc.
    def do_something(self):
        do_something_with(self._data)
    def tear_down(self):
        unallocate(self._data)

    def __enter__(self):
        return self
    def __exit__(self, *args):
        self.tear_down()

# This is desirable
with Holder(...) as holder:
    holder.do_something()

# This might not free the resource immediately, if at all
def foo():
    holder = Holder(...)
    holder.do_something()

That said, after learning all the conversations here, I ended up just leave my Holder class as-is, well, I just added one more docstring for my tear_down():

    def tear_down(self):
        """You are expect to call this eventually; or you can simply use this class as a context manager."""
        ...

After all, we are all consenting adults here...

Slippy answered 3/11, 2020 at 10:42 Comment(1)
@MartijnPieters Comments? :-)Slippy
S
-1

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.

Swordtail answered 5/6, 2015 at 19:42 Comment(1)
This is really the wrong approach. Just return a different object from __enter__ instead of self, and that object implements the methods you want to expose in the context.Wad
M
-1

There is a way. You can call dir() function to see all properties and methods. Context manager needs __enter__ and __exit__ implemented to work accordingly documentation: https://docs.python.org/3/library/contextlib.html enter image description here

Murat answered 18/5 at 23:55 Comment(1)
That checks that the class can be used as a context manager, not that it is being used as one.Examinee

© 2022 - 2024 — McMap. All rights reserved.