Reputation: 12013
I am trying to redefine every new method defined in a class and add functionality to it:
class Test
def self.method_added(name)
old_method = instance_method(name)
define_method(name) do |*args, &block|
@@tests += 1
old_method.call(*args, &block)
end
end
end
This results in:
stack level too deep (SystemStackError)
on line:
def self.method_added(name)
How do I do it properly?
Upvotes: 1
Views: 904
Reputation: 26497
I needed something similar for an abstract test case class that defines setup (in minitest without hooks). I actually used your idea.
def self.method_added(method_name)
# only override 'setup'
return unless method_name == :setup
# prevents recursion
if @_rewriting
@_rewriting = false
return
end
@_rewriting = true
setup_method = instance_method(method_name)
define_method(method_name) do |*args, &block|
# do something
setup_method.bind(self).call(*args, &block)
end
end
There where two missing things in you example:
@_rewriting
)Upvotes: 0
Reputation: 121
You may want to append the method about to be defined to a list (Array) of methods so if you decide you want to memorize the methods you can clear the memoization(s) so
if !@memized_methods.include?name.to_sym then
@memoized_methods<<name.to_sym
define_method(name) { ...
.
.
.
}
end
Upvotes: 0
Reputation: 12013
Thanks for comments, the error was caused by infinite recursion of self.method_added
.
Every time define_method
was called inside self.method_added
, it triggered a new call to self.method_added
because a new method was being defined.
To avoid this I had to set an instance variable @new_method:
class Test
@new_method = true
def self.method_added(name)
if @new_method
@new_method = false
old_method = instance_method(name)
define_method(name) do |*args, &block|
@@tests += 1
old_method.call(*args, &block)
end
@new_method = true
end
end
end
I couldn't find any better solution. Is there anything more elegant?
Upvotes: 0
Reputation: 176412
The error is caused by an infinite recursion. Once you've defined the hook, whenever you call define_method
, it also triggers the method_added
that in turns recalls method_added
and so on.
Regardless any clean solution you may end up to, the approach itself is wrong. You said
I want to count each method call.
But using method_added
will not cover several cases. In fact, method_added
will not get triggered for all the methods that existed before the callback was created. Moreover, it will not work for all the methods you inherit from the parent class, including methods inherited from Object
such as to_s
.
If you want to count the method calls, you have other approaches.
The simplest one, without knowing too much of the Ruby internals, is to use a proxy object that delegates every method call to your object, but keeping track of the method calls. This is known as the Proxy Design Pattern.
class MethodCounter < BasicObject
def initialize(instance)
@counter = 0
@instance = instance
end
def method_missing(*args)
@counter += 1
@instance.send(*args, &block)
end
end
Then instead of using your instance directly
t = Test.new
t.method
wrap it inside the proxy
t = MethodCounter.new(Test.new)
t.method
Another approach is to use Ruby tracking methods such as set_trace_funct
to add a callback whenever a method is called.
Here's a practical example: Logging all method calls in a Rails app
Upvotes: 8