Feature #18951
closedObject#with to set and restore attributes around a block
Description
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
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:
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:
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:
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:
def test_something_when_enabled
SomeLibrary.with(enabled: true) do
# test things
end
end
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], ...)
Some real world code example that could be simplified with method¶
-
redis-client
with_timeout
https://github.com/redis-rb/redis-client/blob/23a5c1e2ff688518904f206df8d4a8734275292d/lib/redis_client/ruby_connection/buffered_io.rb#L35-L53 - Lots of tests in Rails's codebase:
- Changing
Thread.report_on_exception
: https://github.com/rails/rails/blob/2d2fdc941e7497ca77f99ce5ad404b6e58f043ef/activerecord/test/cases/connection_pool_test.rb#L583-L595 - Changing a class attribute: https://github.com/rails/rails/blob/2d2fdc941e7497ca77f99ce5ad404b6e58f043ef/activerecord/test/cases/associations/belongs_to_associations_test.rb#L136-L150
- Changing