this post was submitted on 16 Jun 2023
3 points (80.0% liked)

Python

3228 readers
1 users here now

News and discussions about the programming language Python


founded 5 years ago
MODERATORS
 

Let's say I have a context manager that provides a resource that then mutates on exit:

from contextlib import contextmanager

@contextmanager
def context():
    x = ['hi']
    yield x
    x[0] = 'there'

I found that if I want to make another context class that uses this, such that the context (before mutation) is valid, I have to pass it in:

class Example1:
    def __init__(self, obj):
        self.obj = obj
    def use_obj(self):
        print(self.obj)
    def __enter__(self):
        print("start")
        return self
    def __exit__(self, *exc):
        print("end")

with context() as x:
    with Example1(x) as y:
        y.use_obj()

prints:

start
['hi']
end

However, what I don't like is, let's say that obj is an internal detail of my class. I don't want the user to have to define it beforehand and pass it in.

The only way I can figure how to do this is by calling the context manager's __enter__() explicitly:

class Example2:
    def use_obj(self):
        print(self.obj)
    def __enter__(self):
        print("start")
        self.ctx = context()
        self.obj = self.ctx.__enter__()
        return self
    def __exit__(self, *exc):
        print("end")
        self.ctx.__exit__(None, None, None)

with Example2() as y:
    y.use_obj()

which also prints,

start
['hi']
end

For comparison, just as some other random attempt, the following doesn't work because the context ends when self.obj is created:

class Example3:
    def use_obj(self):
        print(self.obj)
    def __enter__(self):
        print("start")
        with context() as x:
            self.obj = x
        return self
    def __exit__(self, *exc):
        print("end")

with Example3() as y:
    y.use_obj()

which prints,

start
['there']
end

Okay, so my point is that Example2 is the right solution here. But, it's really ugly. So my question is, is there a better way to write Example2?

top 2 comments
sorted by: hot top controversial new old
[โ€“] [email protected] 2 points 1 year ago (1 children)

Have the thing that uses obj take it as a normal constructor argument, and have a convenience wrapper that supplies it:

from contextlib import contextmanager


@contextmanager
def context():
    x = ['hi']
    yield x
    x[0] = 'there'


class ObjUser:
    def __init__(self, obj):
        self.obj = obj

    def use_obj(self):
        print(self.obj)


@contextmanager
def MakeObjUser():
    with context() as obj:
        yield ObjUser(obj)


with MakeObjUser() as y:
    y.use_obj()
[โ€“] [email protected] 1 points 1 year ago

Huh, so a sort of factory pattern to encapsulate construction. I feel it's slightly awkward as construction has to happen outside the object, but maybe usable. At least nicer than calling __enter__() explicitly. Thanks, definitely an option I'll consider.