From: "austin (Austin Ziegler)" Date: 2022-08-31T21:12:41+00:00 Subject: [ruby-core:109803] [Ruby master Feature#18951] Object#with to set and restore attributes around a block Issue #18951 has been updated by austin (Austin Ziegler). Dan0042 (Daniel DeLorme) wrote in #note-10: > Also really like the idea, also ambivalent about such a short and generic name. > > I think it would be good if the name emphasized a bit more that the original values will be restored after the block. Something in the vein of `temporarily_with` - `#override_with` - `#mut` - `#mutate` - `#borrow_with` - `#with_restore` - `#restore_do` - `#tap_restore` - `#tap_reset` - `#override_reset` - `#overlay_with` - `#overlay` - `#overlay_reset` This could do something with `Object#extend` or maybe a temporary refinement (I have yet to *use* refinements, so I���m going to avoid trying to make an example that way). ```ruby module RestorableOverlay def overlay(values) (@__restoreable_overlay__ ||= []) << {} values.each_pair { |k, v| @__restorable_overlay__[-1][k] = send(:"#{k}") send(:"#{k}=", v) } end def overlay_commit @__restorable_overlay__.pop end def overlay_rollback overlay_commit.each_pair { |k, v| send(:"#{k}=", v) } # magic function that doesn���t exist if @__restorable_overlay__.empty? unextend RestorableOverlay instance_variable_del(:@__restorable_overlay__) end end end class Object def overlay_with(values) extend(RestorableOverlay) unless extended_with?(RestorableOverlay) if block_given? begin overlay(values) yield ensure overlay_rollback end else overlay(values) end end end ``` I had done something *similar* back in 2004 while working with PDF::Writer (Ruby 1.8 mostly), originally released as https://github.com/halostatue/transaction-simple. An absolute memory hog, broke parent / child ownership cycles, and slow as can be, but it *worked* for the most part. Maybe worth revisiting? ---------------------------------------- Feature #18951: Object#with to set and restore attributes around a block https://bugs.ruby-lang.org/issues/18951#change-99044 * Author: byroot (Jean Boussier) * Status: Open * Priority: Normal ---------------------------------------- ### Use case A very common pattern in Ruby, especially in testing is to save the value of an attribute, set a new value, and then restore the old value in an `ensure` clause. e.g. in unit tests ```ruby def test_something_when_enabled enabled_was, SomeLibrary.enabled = SomeLibrary.enabled, true # test things ensure SomeLibrary.enabled = enabled_was end ``` Or sometime in actual APIs: ```ruby def with_something_enabled enabled_was = @enabled @enabled = true yield ensure @enabled = enabled_was end ``` There is no inherent problem with this pattern, but it can be easy to make a mistake, for instance the unit test example: ```ruby def test_something_when_enabled some_call_that_may_raise enabled_was, SomeLibrary.enabled = SomeLibrary.enabled, true # test things ensure SomeLibrary.enabled = enabled_was end ``` In the above if `some_call_that_may_raise` actually raises, `SomeLibrary.enabled` is set back to `nil` rather than its original value. I've seen this mistake quite frequently. ### Proposal I think it would be very useful to have a method on Object to implement this pattern in a correct and easy to use way. The naive Ruby implementation would be: ```ruby class Object def with(**attributes) old_values = {} attributes.each_key do |key| old_values[key] = public_send(key) end begin attributes.each do |key, value| public_send("#{key}=", value) end yield ensure old_values.each do |key, old_value| public_send("#{key}=", old_value) end end end end ``` NB: `public_send` is used because I don't think such method should be usable if the accessors are private. With usage: ```ruby def test_something_when_enabled SomeLibrary.with(enabled: true) do # test things end end ``` ```ruby GC.with(measure_total_time: true, auto_compact: false) do # do something end ``` ### Alternate names and signatures If `#with` isn't good, I can also think of: - `Object#set` - `Object#apply` But the `with_` prefix is by far the most used one when implementing methods that follow this pattern. Also if accepting a Hash is dimmed too much, alternative signatures could be: - `Object#set(attr_name, value)` - `Object#set(attr1, value1, [attr2, value2], ...)` -- https://bugs.ruby-lang.org/ Unsubscribe: