Project

General

Profile

Actions

Feature #18951

closed

Object#with to set and restore attributes around a block

Added by byroot (Jean Boussier) over 2 years ago. Updated over 1 year ago.

Status:
Rejected
Assignee:
-
Target version:
-
[ruby-core:109403]

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

Actions

Also available in: Atom PDF

Like3
Like1Like0Like1Like1Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0