Josh Bodah
Josh Bodah

Reputation: 1243

Wrap and unwrap method in Ruby

I'm working on a library that will spy on method calls (SinonJS-style). What I want to do is wrap and unwrap a method. To wrap the method, I can just wrap the original method in a block:

module Spy
  def on(receiver, msg)
    @original = receiver.method(msg)
    wrapped = Proc.new {|*args| @original}
    receiver.define_singleton_method(msg, wrapped)
  end

  extend self
end

instance = Object.new
Spy.on(instance, :to_s)

This works fine, but unwrapping the method is problematic:

module Spy
  # Add this to the above

  def restore(receiver, msg)
    receiver.define_singleton_method(msg, @original)
  end
end

instance = Object.new
original = instance.method(:to_s)
Spy.on(instance, :to_s)
Spy.restore(instance, :to_s)
restored = instance.method(:to_s)

original == restored
=> false
original.object_id
=> 70317288647120
restored.object_id
=> 70317302643500

In fact, it looks like Object#method always returns a new object_id. Is there a way for me to reattach the exact same method to the object? In JS I would just store the function and swap it back into place. Am I misunderstanding something about Ruby? Is there another approach that I can use? I'm really interested in the == comparator for testing

Thanks ahead of time!

EDIT:

A boiled down version of what the issue is:

irb(main):001:0> receiver = Object.new
=> #<Object:0x007fc4a1939320>
irb(main):002:0> original = receiver.method(:to_s)
=> #<Method: Object(Kernel)#to_s>
irb(main):003:0> original == receiver.method(:to_s)
=> true
irb(main):004:0> receiver.define_singleton_method(:to_s, original)
=> :to_s
irb(main):005:0> original == receiver.method(:to_s)
=> false
irb(main):006:0>

Is there another way to reattach a method such that the above would be true?

Upvotes: 1

Views: 1214

Answers (2)

Josh Bodah
Josh Bodah

Reputation: 1243

It looks like Mocha accomplishes this by defining a new method which wraps the original: https://github.com/freerange/mocha/blob/master/lib/mocha/class_method.rb#L75

I'm not 100% satisfied with this solution, but it should do the job in most cases. I'm still interested if I can unattach and reattach a method to an instance

EDIT:

It turns out that I can use Method#source_location instead for my tests. When this is spied, it will point to my Spy class, and when it is not spied, it points to the original implementation

EDIT 2:

Looked into Max's comment about where the methods were being defined and dug into Ruby's Method object. I eventually settled on original.owner.instance_eval { define_method msg, wrapped } to wrap and original.owner.instance_eval { define_method msg, original } for the restore. This worked with my original tests of using Method#== to test

Upvotes: 0

Max
Max

Reputation: 22325

The first problem is that the block you pass to define_singleton_method is evaluated in the context of receiver, so @original will be nil.

You can copy the method to a local variable so that the block will be a closure

@original = receiver.method(msg)
org = @original
wrapped = Proc.new { |*args| org }

Secondly, the object ID of the method is irrelevant and you should actually expect it to be different. Look at the method you actually get back:

instance.method(:to_s)
#<Method: Object(Kernel)#to_s>

Spy.on(instance, :to_s)
Spy.restore(instance, :to_s)

instance.method(:to_s)
#<Method: #<Object:0x00000001>.to_s>

The method changes from Kernel to the singleton class! Why would the ID be the same? But this doesn't matter because all you've done is attached the method from Kernel to the singleton class, so the same code gets run anyway.

Upvotes: 1

Related Questions