Allow with block (context manager) to return a value (e.g. to use as a builder)

it would be nice to be able to use the "with" block syntax to do things like implement a builder. To do this, the with block must be able to return a final value to the surrounding context ass part of its __exit__ behavior. obj = with builder() as b: … This would be a little like what can be done in Ruby by passing a {…} block argument to a function.

Here's a more fleshed-out example of the kind of usage I have in mind. Note that this does not require `with` to have any affect on variable scope. The object returned from the context manager's __enter__ method is all the context that is needed or wanted for this pattern. restaurant_choices = with build_choices('Restaurant') as cb: # Get of nonexistent attr auto-generates matching entry. cb.BURGER_KING cb.FIVE_GUYS # Returned value is a ChoiceItem that implements __mod__ to return a # new ChoiceItem instance w/ substituted label. cb.MCDONALDS %= "McDonald's" print(restaurant_choices) # RestaurantChoices( # ( # ('BURGER_KING', 'Burger King'), # ('FIVE_GUYS', 'Five Guys'), # ('MCDONALDS', "McDonald's"))) print(RestaurantChoices.BURGER_KING) # Prints 'BURGER_KING' print(RestaurantChoices.value_labels['FIVE_GUYS']) # Prints 'Five Guys'

Ah. Now I get the key point you were making about scope — that the `as <something>` variable is in the surrounding scope following the execution of the `with` block. You're right. That really does already do pretty close to what I am asking for. It's just a hair short in that it requires an additional line to get the result out of the builder, but that's not enough of a big deal to make significant changes to the capabilities of context managers.

On Sun, Oct 13, 2019 at 03:59:19PM +0200, Anders Hovmöller wrote:
That's not a problem. We could introduce a rule that when the with block is part of an assignment, the with block runs in a seperate scope. A bigger problem is that this would be the first statement which returns a value (as opposed to having an effect via side-effects, like the class and def statements), and it would only be usable in assignments: spam = with eggs() as cheese: ... but not: items = [x, y, with eggs() as cheese: ... , z] -- Steven

On Mon, Oct 14, 2019 at 9:39 AM Steven D'Aprano <steve@pearwood.info> wrote:
Scope... return value... this sounds familiar. What if this were actually a function? You'd have to implement build_choices appropriately for this to work, but you'd need to do that for the context manager anyway. @build_choices('Restaurant') def restaurant_choices(cb): # Get of nonexistent attr auto-generates matching entry. cb.BURGER_KING cb.FIVE_GUYS # Returned value is a ChoiceItem that implements __mod__ to return a # new ChoiceItem instance w/ substituted label. cb.MCDONALDS %= "McDonald's" print(restaurant_choices) # RestaurantChoices( # ( # ('BURGER_KING', 'Burger King'), # ('FIVE_GUYS', 'Five Guys'), # ('MCDONALDS', "McDonald's"))) print(RestaurantChoices.BURGER_KING) To make this work, your build_choices would need to have something like this: class build_choices: def __call__(self, builder): builder(self) return self Does that look clean enough? ChrisA

Here's a more fleshed-out example of the kind of usage I have in mind. Note that this does not require `with` to have any affect on variable scope. The object returned from the context manager's __enter__ method is all the context that is needed or wanted for this pattern. restaurant_choices = with build_choices('Restaurant') as cb: # Get of nonexistent attr auto-generates matching entry. cb.BURGER_KING cb.FIVE_GUYS # Returned value is a ChoiceItem that implements __mod__ to return a # new ChoiceItem instance w/ substituted label. cb.MCDONALDS %= "McDonald's" print(restaurant_choices) # RestaurantChoices( # ( # ('BURGER_KING', 'Burger King'), # ('FIVE_GUYS', 'Five Guys'), # ('MCDONALDS', "McDonald's"))) print(RestaurantChoices.BURGER_KING) # Prints 'BURGER_KING' print(RestaurantChoices.value_labels['FIVE_GUYS']) # Prints 'Five Guys'

Ah. Now I get the key point you were making about scope — that the `as <something>` variable is in the surrounding scope following the execution of the `with` block. You're right. That really does already do pretty close to what I am asking for. It's just a hair short in that it requires an additional line to get the result out of the builder, but that's not enough of a big deal to make significant changes to the capabilities of context managers.

On Sun, Oct 13, 2019 at 03:59:19PM +0200, Anders Hovmöller wrote:
That's not a problem. We could introduce a rule that when the with block is part of an assignment, the with block runs in a seperate scope. A bigger problem is that this would be the first statement which returns a value (as opposed to having an effect via side-effects, like the class and def statements), and it would only be usable in assignments: spam = with eggs() as cheese: ... but not: items = [x, y, with eggs() as cheese: ... , z] -- Steven

On Mon, Oct 14, 2019 at 9:39 AM Steven D'Aprano <steve@pearwood.info> wrote:
Scope... return value... this sounds familiar. What if this were actually a function? You'd have to implement build_choices appropriately for this to work, but you'd need to do that for the context manager anyway. @build_choices('Restaurant') def restaurant_choices(cb): # Get of nonexistent attr auto-generates matching entry. cb.BURGER_KING cb.FIVE_GUYS # Returned value is a ChoiceItem that implements __mod__ to return a # new ChoiceItem instance w/ substituted label. cb.MCDONALDS %= "McDonald's" print(restaurant_choices) # RestaurantChoices( # ( # ('BURGER_KING', 'Burger King'), # ('FIVE_GUYS', 'Five Guys'), # ('MCDONALDS', "McDonald's"))) print(RestaurantChoices.BURGER_KING) To make this work, your build_choices would need to have something like this: class build_choices: def __call__(self, builder): builder(self) return self Does that look clean enough? ChrisA
participants (5)
-
Anders Hovmöller
-
Chris Angelico
-
David Mertz
-
Steve Jorgensen
-
Steven D'Aprano