Is there a hook similar to Class#inherited that's triggered only after a Ruby class definition?
Asked Answered
E

9

26

#inherited is called right after the class Foo statement. I want something that'll run only after the end statement that closes the class declaration.

Here's some code to exemplify what I need:

class Class
  def inherited m
    puts "In #inherited for #{m}"
  end
end

class Foo
  puts "In Foo"
end
puts "I really wanted to have #inherited tiggered here."


### Output:
# In #inherited for Foo
# In Foo
# I really wanted to have #inherited tiggered here.

Does anything like that exist? Can it be created? Am I totally out of luck?

Educational answered 26/4, 2009 at 10:47 Comment(2)
My first thought is that there's probably a better way to achieve the functionality you have in mind. Of course it's hard to say for sure without more information and I suspect at this point you've moved on.Gallegos
It's now possible to do this easily using TracePoint. See my answer below, or my answer to this similar question: #32234360Potentate
P
12

Use TracePoint to track when your class sends up an :end event.

This module will let you create a self.finalize callback in any class.

module Finalize
  def self.extended(obj)
    TracePoint.trace(:end) do |t|
      if obj == t.self
        obj.finalize
        t.disable
      end
    end
  end
end

Now you can extend your class and define self.finalize, which will run as soon as the class definition ends:

class Foo
  puts "Top of class"

  extend Finalize

  def self.finalize
    puts "Finalizing #{self}"
  end

  puts "Bottom of class"
end

puts "Outside class"

# output:
#   Top of class
#   Bottom of class
#   Finalizing Foo
#   Outside class
Potentate answered 1/1, 2016 at 19:26 Comment(2)
TracePoint have no method trace for jrubyDewie
@МалъСкрылевъ Call tp = TracePoint.new(:end) do |t| ... and then tp.enable directly afterward. TracePoint.trace is just a convenience method for these two actions together.Potentate
E
10

You may be out of luck. But that's only a warning, not a definitive answer.

Ruby hooks the beginning of the class definition, rather than the end, for Class#inherited b/c ruby class definitions don't have a real end. They can be reopened any time.

There was some talk a couple years ago about adding a const_added trigger, but it hasn't gone through yet. From Matz:

I'm not going to implement every possible hook. So when somebody comes with more concrete usage, I will consider this again. It would be const_added, not class_added.

So this might handle your case - but I'm not sure (it may trigger on the start too, when it's eventually implemented).

What are you trying to do with this trigger? There may be another way to do it.

Evelineevelinn answered 26/4, 2009 at 13:59 Comment(2)
I'm trying to add behavior to activerecord models, but I need all the model customizations to go through before I mess with it. Right now I'm just including the extension module manually at a point in the model where everything I need has been set.Educational
I believe const_added would also be triggered by the time the const is created. The parts of ruby that have different standard syntax and meta-programming interface always let me down. Like def/define_method. If class Foo; end was just syntactic sugar for Class.new(superclass, &block), wherein the block would then be passed to class_instance#instance_eval one could just wrap around the Class#initialize method. Oh well, another monday, another day wherein I consider the jump to Lisp.Educational
C
10

I am late, but I think I have an answer (to anyone who visit here).

You can trace until you find the end of the class definition. I did it in a method which I called after_inherited:

class Class
  def after_inherited child = nil, &blk
    line_class = nil
    set_trace_func(lambda do |event, file, line, id, binding, classname|
      unless line_class
        # save the line of the inherited class entry
        line_class = line if event == 'class'
      else
        # check the end of inherited class
        if line == line_class && event == 'end'
          # if so, turn off the trace and call the block
          set_trace_func nil
          blk.call child
        end
      end
    end)
  end
end

# testing...

class A
  def self.inherited(child)
    after_inherited do
      puts "XXX"
    end
  end
end

class B < A
  puts "YYY"
  # .... code here can include class << self, etc.
end

Output:

YYY
XXX
Clientele answered 18/8, 2011 at 4:43 Comment(3)
That just gave me chills. :-) Nice work man, very weird and totally scary, but for the right problem... hawt.Marcum
this doesn't work anymore.. not all that well.. It was an amazing lead to TracePoint... check out my question here. #28754570Subterranean
set_trace_func no longer works, see solution using TracePointPotentate
B
5

Take a look at defined gem. You can do like this:

require "defined"
Defined.enable!

class A
  def self.after_inherited(child)
    puts "A was inherited by #{child}"
  end

  def self.defined(*args)
    superclass.after_inherited(self) if superclass.respond_to?(:after_inherited)
  end
end

class B < A
  puts "B was defined"
end

Output:

B was defined
A was inherited by B

However self.defined will be fired after each class definition. So if you add the following code

class B < A
  puts "B was redefined"
end

You will see

B was defined
A was inherited by B
B was redefined
A was inherited by B

There are ways to avoid that which I can explain to you if you want.

However, as said there are probably better ways to solve your problem.

Bartender answered 17/8, 2011 at 16:28 Comment(4)
BTW, Ruby is so cool that we don't need Matz approval to extend the language :-)Bartender
this calls a method after each line that is executed in your entire ruby code. slows down performance a lot i supposeFogy
@Dominik, what do you mean? "Inherited" is called after a class definition only once.Bartender
i mean the defined gem is starting to trace every ruby code line that is executed to find out when a class is beeing reopened and closed again. this trace function is executed on every ruby line in your code after the Defined.enable! line. take a look at the defined gem source codeFogy
P
1

If you are willing to assume your Ruby implements ObjectSpaces, you could could look up all model instances after the fact, and then modify them appropriately. Google suggests http://phrogz.net/ProgrammingRuby/ospace.html

Peck answered 26/4, 2009 at 15:5 Comment(1)
I suspect that wouldn't go so well with Rails's class reloading.Educational
B
1

Rails has a subclasses method, it might be worth looking at the implementation:

class Fruit; end

class Lemon < Fruit; end

Fruit.subclasses # => [Lemon]
Beeck answered 1/6, 2012 at 13:47 Comment(0)
L
1

No, there no such a hook to my knowledge, but the good thing is you can kind of do it yourself. Here is a possible implementation:

Is not super clean, but it works:

puts RUBY_VERSION # 2.4.1

class Father
  def self.engage_super_setup(sub)
    puts "self:#{self} sub:#{sub}"
    sub.class_eval do
      puts "toy:#{@toy}"
    end
  end

  def self.super_setup
    if self.superclass.singleton_methods.include?(:engage_super_setup)
      superclass.engage_super_setup(self)
    end
  end
end

Son = Class.new(Father) do
  @toy = 'ball'
end.tap { |new_class| new_class.super_setup } # this is needed to:
# 1. call the super_setup method in the new class.
# 2. we use tap to return the new Class, so this class is assigned to the Son constant.

puts Son.name # Son

Output:

self:Father sub:#<Class:0x0055d5ab44c038> #here the subclass is still anonymous since it was not yet assigned to the constant "Son"
toy:ball # here we can see we have acess to the @toy instance variable in Son but from the :engage_super_setup in the Father class
Son # the of the class has been assigned after the constant, since ruby does this automatically when a class is assigned to a constant 

So this is obviously not as clean as a hook, but I think at the end we have a pretty good result.

If we had tried to do the same with :inherited sadly is not possible, because :inherited is called even before the execution entoer in the body of the class:

puts RUBY_VERSION # 2.4.1

class Father
  def self.inherited(sub)
    puts "self:#{self} sub:#{sub}"
    sub.class_eval do
      puts "toy:#{@toy.inspect}"
    end
  end

end

class Son < Father
  puts "we are in the body of Son"
  @toy = 'ball'
end

puts Son.name # Son

Output:

self:Father sub:Son # as you can see here the hook is executed before the body of the declaration Son class runs
toy:nil # we dont have access yet to the instance variables
we are in the body of Son # the body of the class declaration begins to run after the :inherited hook.
Son
Linseylinseywoolsey answered 9/5, 2019 at 17:11 Comment(0)
C
1

You can store a block to be called after all code has been loaded.

For instance, if you're in Rails:

In an initializer:

module AfterInitialize
  extend self

  @@callbacks = []

  def add(&block)
    @@callbacks << block
  end

  def run
    @@callbacks.each(&:call)
  end
end

In application.rb:

config.after_initialize { AfterInitialize.run }

Then

class Parent
  def self.inherited(subclass)
    p "In inherited for #{subclass} before initialize"

    AfterInitialize.add do
      p "In inherited for #{subclass} after initialize"
    end
  end
end

And

class Child
  p "In Child"
end
Clupeoid answered 28/10, 2021 at 16:32 Comment(0)
S
0

I had the same question when trying to automatically add generic validations to all models. The problem was that if the model used #set_table_name then my code that added added the validations based on the DB data-types would blow up because it was guessing at the table name based on the name of the model (because #inherited was getting called BEFORE #set_table_name).

So just like you, I was really looking for a way to get #inherited to fire AFTER everything in the model had already been loaded. But I didn't really need to take it that far, all I needed was something that fired AFTER #set_table_name. So it turned out to be a simple as aliasing the method. You can see an example of what I did here: https://gist.github.com/1019294

In a comment above you stated "I'm trying to add behavior to activerecord models, but I need all the model customizations to go through before I mess with it". So my question to you is if there are specific model customizations you care about, if so then maybe you could use an aliasing approach to achieve your desired result.

Seventeen answered 10/6, 2011 at 17:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.