From: "byroot (Jean Boussier)" Date: 2022-08-27T15:22:00+00:00 Subject: [ruby-core:109741] [Ruby master Feature#18951] Object#with to set and restore attributes around a block Issue #18951 has been updated by byroot (Jean Boussier). > But WITH is also SQL keyword and was recently added to ActiveRecord It was added to `ActiveRecord::Relation`, which doesn't have any setters in its public API, and even if it did it still wouldn't be a concern in my opinion. Since you pass attribute names that correspond to actual methods (accessors) on the object, it means the code needs to know the interface the object responds to, so presumably you also know `Object#with` wasn't overridden. So if some existing classes already define a `#with` method with a different meaning it's not a problem at all. ---------------------------------------- Feature #18951: Object#with to set and restore attributes around a block https://bugs.ruby-lang.org/issues/18951#change-98977 * 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: