__enter__ and __exit__ on class level in python 3
Asked Answered
H

3

8

I am unsuccessfully trying to get the magic with-statement methods __enter__ and __exit__ running on class-level:

class Spam():

    @classmethod
    def __enter__(cls):
        return cls

    @classmethod
    def __exit__(cls, typ, value, tb):
        cls.cleanup_stuff()


with Spam:
    pass

However, this will result in an AttributeError:

Traceback (most recent call last):
  File "./test.py", line 15, in <module>
    with Spam:
AttributeError: __exit__

Is it possible to use the __enter__ and __exit__ methods on class-level anyway?

Holly answered 4/2, 2015 at 20:5 Comment(2)
Why would you even try to do this? My advice is to use the with statement as intended.Gunny
I am intending to extend the peewee.Model class with a class-level cleanup capability.Holly
P
16

__enter__ and __exit__ are special methods, and as such only work correctly when defined on a object's type, not in it's instance dictionary.

Now Spam is a instance of type, and type(Spam).__enter__ and type(Spam).__exit__ do not exist. Therefore you get an attribute error.

To make this work, the methods would need to be declared on the metaclass of the class you want to use. Example:

class Spam(type):

    def __enter__(cls):
        print('enter')
        return cls

    def __exit__(cls, typ, value, tb):
        print('exit')

class Eggs(metaclass=Spam):
    pass

with Eggs:
    pass

Now Eggs is an instance of Spam (type(Eggs) == Spam, and therefore type(Eggs).__enter__ and type(Eggs).__exit__ do exist).

However defining a metaclass just to use an instance of it as a context manager seems a little over the top. The more straight forward solution starting from your example would be to just use

with Spam():
    pass

Or if you want to reuse the same instance later:

spam = Spam()
with spam:
    pass
Pituri answered 4/2, 2015 at 20:59 Comment(5)
I think then that the @classmethod should not be needed anymore.Jassy
There is a difference in which class will be passed as cls with and without classmethod, with it will be Spam, without it will be Eggs, so while it's technically not needed, it's not the same.Pituri
+1 but please remove the @classmethod decorators. The first argument to a metaclass method is already a class. Decorating with @classmethod means you get the metaclass instead of the class, which is rarely what you want.Linseylinseywoolsey
@Linseylinseywoolsey Ok, removed it, you and @Jassy are correct, it makes little sense to have it on the metaclass. This way the cleanup_stuff method could also be declared on Eggs and not only on Spam...Pituri
Nonetheless I need to use the class in any case, since stuff from class-methods needs to be cleaned up on the class's level. I don't use instances in my case.Holly
S
0

It seems that CPython doesn't call a bound method like instance.__exit__, it seeks over instance type, doing something like type(instance).__dict__['__exit__'] than calls it. And since type(Spam) is a special type object (not a Spam itself), it doesn't contain __exit__ method.

I tried to workaround that using metaclasses, but wasn't successful. __getattr__ doesn't work either.

See here: https://github.com/python/cpython/blob/2545fdbd4b4a6a77b132fccf816578f59b609be5/Objects/typeobject.c#L1362

  • Py_TYPE is similiar to type(self)
  • _PyType_LookupId walks over type(self).__dict__ (no __getattr__ call here)

Python 2 implementation is different, but main idea about getting type(self) applies to it too

Solve answered 4/2, 2015 at 20:56 Comment(0)
J
0

You can combine @classmethod and @contextlib.contextmanager to make a class method that acts as a context manager:

from __future__ import annotations

from contextlib import contextmanager
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from typing import Iterator, Self

class Spam:
    @classmethod
    @contextmanager
    def context(cls) -> Iterator[type[Self]]:
        try:
            # Setup here
            yield cls
        finally:
            cls.cleanup_stuff()

with Spam.context():
    pass

Note that the order of the decorators matters.

Reference:

Jobe answered 6/5 at 22:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.