ruby-core@ruby-lang.org archive (unofficial mirror)
 help / color / mirror / Atom feed
* [ruby-core:109403] [Ruby master Feature#18951] Object#with to set and restore attributes around a block
@ 2022-08-01 17:52 byroot (Jean Boussier)
  2022-08-27 14:37 ` [ruby-core:109737] " nobu (Nobuyoshi Nakada)
                   ` (21 more replies)
  0 siblings, 22 replies; 23+ messages in thread
From: byroot (Jean Boussier) @ 2022-08-01 17:52 UTC (permalink / raw)
  To: ruby-core

Issue #18951 has been reported by byroot (Jean Boussier).

----------------------------------------
Feature #18951: Object#with to set and restore attributes around a block
https://bugs.ruby-lang.org/issues/18951

* 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.dup
    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/

^ permalink raw reply	[flat|nested] 23+ messages in thread

end of thread, other threads:[~2023-03-17  1:15 UTC | newest]

Thread overview: 23+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-08-01 17:52 [ruby-core:109403] [Ruby master Feature#18951] Object#with to set and restore attributes around a block byroot (Jean Boussier)
2022-08-27 14:37 ` [ruby-core:109737] " nobu (Nobuyoshi Nakada)
2022-08-27 14:58 ` [ruby-core:109738] " Eregon (Benoit Daloze)
2022-08-27 15:06 ` [ruby-core:109739] " byroot (Jean Boussier)
2022-08-27 15:14 ` [ruby-core:109740] " retro
2022-08-27 15:22 ` [ruby-core:109741] " byroot (Jean Boussier)
2022-08-29 12:08 ` [ruby-core:109766] " ufuk (Ufuk Kayserilioglu)
2022-08-29 12:31 ` [ruby-core:109767] " byroot (Jean Boussier)
2022-08-29 14:11 ` [ruby-core:109771] " ngan (Ngan Pham)
2022-08-31 20:05 ` [ruby-core:109801] " Dan0042 (Daniel DeLorme)
2022-08-31 21:12 ` [ruby-core:109803] " austin (Austin Ziegler)
2022-09-01 10:37 ` [ruby-core:109809] " p8 (Petrik de Heus)
2022-09-11  2:34 ` [ruby-core:109881] " retro
2022-09-11  2:49 ` [ruby-core:109882] " baweaver (Brandon Weaver)
2022-09-21  5:58 ` [ruby-core:109969] " ko1 (Koichi Sasada)
2022-09-21  6:03 ` [ruby-core:109970] " byroot (Jean Boussier)
2022-09-23 15:40 ` [ruby-core:110049] " Dan0042 (Daniel DeLorme)
2022-11-15  9:18 ` [ruby-core:110760] " byroot (Jean Boussier)
2022-12-01  7:36 ` [ruby-core:111111] " matz (Yukihiro Matsumoto)
2022-12-01  7:38 ` [ruby-core:111112] " byroot (Jean Boussier)
2022-12-10 17:10 ` [ruby-core:111252] " retro
2023-03-06  8:18 ` [ruby-core:112698] " byroot (Jean Boussier) via ruby-core
2023-03-17  1:15 ` [ruby-core:112920] " dsisnero (Dominic Sisneros) via ruby-core

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).