It's typical to require for some task multiple objects which have resources to be explicitly released - say, two files; this is easily done when the task is local to a function using nested with blocks, or - even better - a single with block with multiple with_item clauses:
with open('in.txt', 'r') as i, open('out.txt', 'w') as o:
    # do stuff
OTOH, I still struggle to understand how this is supposed to work when such objects aren't just local to a function scope, but owned by a class instance - in other words, how context managers compose.
Ideally I'd like to do something like:
class Foo:
    def __init__(self, in_file_name, out_file_name):
        self.i = WITH(open(in_file_name, 'r'))
        self.o = WITH(open(out_file_name, 'w'))
and have Foo itself turn into a context manager that handles i and o, such that when I do
with Foo('in.txt', 'out.txt') as f:
    # do stuff
self.i and self.o are taken care of automatically as you would expect.
I tinkered about writing stuff such as:
class Foo:
    def __init__(self, in_file_name, out_file_name):
        self.i = open(in_file_name, 'r').__enter__()
        self.o = open(out_file_name, 'w').__enter__()
    def __enter__(self):
        return self
    def __exit__(self, *exc):
        self.i.__exit__(*exc)
        self.o.__exit__(*exc)
but it's both verbose and unsafe against exceptions occurring in the constructor. After searching for a while, I found this 2015 blog post, which uses contextlib.ExitStack to obtain something very similar to what I'm after:
class Foo(contextlib.ExitStack):
    def __init__(self, in_file_name, out_file_name):
        super().__init__()
        self.in_file_name = in_file_name
        self.out_file_name = out_file_name
    def __enter__(self):
        super().__enter__()
        self.i = self.enter_context(open(self.in_file_name, 'r')
        self.o = self.enter_context(open(self.out_file_name, 'w')
        return self
This is pretty satisfying, but I'm perplexed by the fact that:
- I find nothing about this usage in the documentation, so it doesn't seem to be the "official" way to tackle this problem;
- in general, I find it extremely difficult to find information about this issue, which makes me think I'm trying to apply an unpythonic solution to the problem.
Some extra context: I work mostly in C++, where there is no distinction between the block-scope case and the object-scope case for this issue, as this kind of cleanup is implemented inside the destructor (think __del__, but invoked deterministically), and the destructor (even if not explicitly defined) automatically invokes the destructors of the subobjects. So both:
{
    std::ifstream i("in.txt");
    std::ofstream o("out.txt");
    // do stuff
}
and
struct Foo {
    std::ifstream i;
    std::ofstream o;
    Foo(const char *in_file_name, const char *out_file_name) 
        : i(in_file_name), o(out_file_name) {}
}
{
    Foo f("in.txt", "out.txt");
}
do all the cleanup automatically as you generally want.
I'm looking for a similar behavior in Python, but again, I'm afraid I'm just trying to apply a pattern coming from C++, and that the underlying problem has a radically different solution that I can't think of.
So, to sum it up: what is the Pythonic solution to the problem of having an object who owns objects that require cleanup become a context-manager itself, calling correctly the __enter__/__exit__ of its children?
 
     
     
     
     
    