Let's pry into Ruby Objects
pry
bar?You are probably already familiar with irb
, an interactive Ruby shell. It is pretty powerful and can help you poke around Ruby. In Rails, you might have had access to byebug
and used it for debugging purposes. This is great and is standard with Rails projects. I do, however, recommend looking at pry
, which is just a bit more powerful in what it can do. We’re just going to scratch the surface here.
gem install pry
The pry
wiki is quite detailed with a lot of accompanying resources.
pry
to Open ObjectsWe’ll use a shortened example that I recently encountered. I was pretty deep in Rails and was dealing with ActiveSupport::Callbacks::CallbackChain
(lets not ask why ;P). I wanted to verify if we have any around_perform
callbacks set on a particular class. With my trusty pry
, I can inspect what I’m working with in more detail.
pry(main)> RandomClass_perform_callbacks
=> #<ActiveSupport::Callbacks::CallbackChain:0x007fdcdddb5b20 @callbacks=nil, @chain=[], @config={:scope=>[:kind]}, @mutex=#<Thread::Mutex:0x007fdcdddb53c8>, @name=:perform>
At this point we have ActiveSupport::Callbacks
included in our RandomClass
. We also have an empty callback chain.
I eventually included in a module MagicCallbacks
which defines our around_perform
upon being included. If we were to re-inspect the class, we would see that we have a callback present.
pry(main)> RandomClass.include(MagicCallbacks)
=> RandomClass
pry(main)> RandomClass._perform_callbacks
=> #<ActiveSupport::Callbacks::CallbackChain:0x007fdce2a22b70
@callbacks=nil,
@chain=
[#<ActiveSupport::Callbacks::Callback:0x007fdce2a22cd8
@chain_config={:scope=>[:kind]},
@filter=#<Proc:0x007fdce2a22eb8@/Users/jalbert/Projects/example-rails/app/models/concerns/magic_callbacks.rb:7>,
@if=[],
@key=70293335906140,
@kind=:around,
@name=:perform,
@unless=[]>],
@config={:scope=>[:kind]},
@mutex=#<Thread::Mutex:0x007fdce2a22b20>,
@name=:perform>
We can now see that we have a callback within the @chain
array! Back to the problem at hand, I was interested in programmatically determining if the class had any callbacks defined.
As I was working with an unfamiliar object, I reached for my trusty pry
. I can use ls
on any object and see a listing of methods and where they come from.
pry(main)> ls RandomClass._perform_callbacks
Enumerable#methods:
all? chunk collect_concat detect each_cons each_with_index exclude? find_index grep include? lazy max min minmax_by partition reverse_each slice_before sort_by take_while to_json to_set
any? chunk_while count drop each_entry each_with_object find first grep_v index_by many? max_by min_by none? reduce select slice_when sum to_a to_json_with_active_support_encoder zip
as_json collect cycle drop_while each_slice entries find_all flat_map group_by inject map member? minmax one? reject slice_after sort take to_h to_json_without_active_support_encoder
ActiveSupport::Callbacks::CallbackChain#methods: append clear compile config delete each empty? index insert name prepend
instance variables: @callbacks @chain @config @mutex @name
There is a lot of information here, but the key points to take away are:
#ancestors
. #respond_to?
itYou can also modify the ls
command with modifiers which you can learn more with ls -h
.
So we can see here that we have an #empty?
defined under ActiveSupport::Callbacks::CallbackChain#methods
. This sounds great, and my first thoughts is I can use #empty?
. My only concern is what it’s actually checking. Again, pry
to the rescue with show-source
.
pry(main)> show-source ActiveSupport::Callbacks::CallbackChain#empty?
From: /Users/jalbert/.rvm/gems/ruby-2.3.0/gems/activesupport-4.2.7.1/lib/active_support/callbacks.rb @ line 529:
Owner: ActiveSupport::Callbacks::CallbackChain
Visibility: public
Number of lines: 1
def empty?; @chain.empty?; end
So we can see the implementation of #empty?
is a one-liner, where it’s just calling @chain.empty?
. Sounds legit, but let’s go further to verify this.
pry(main)> show-source ActiveSupport::Callbacks::CallbackChain
From: /Users/jalbert/.rvm/gems/ruby-2.3.0/gems/activesupport-4.2.7.1/lib/active_support/callbacks.rb @ line 512:
Class name: ActiveSupport::Callbacks::CallbackChain
Number of lines: 80
class CallbackChain #:nodoc:#
include Enumerable
attr_reader :name, :config
def initialize(name, config)
@name = name
@config = {
:scope => [ :kind ]
}.merge!(config)
@chain = []
@callbacks = nil
@mutex = Mutex.new
end
...
def append(*callbacks)
callbacks.each { |c| append_one(c) }
end
...
def append_one(callback)
@callbacks = nil
remove_duplicates(callback)
@chain.push(callback)
end
end
Yep! Just what I wanted to see. @chain
is just an array to which all the callbacks are appended. So now we can do our check for any callbacks on our class by using !RandomClass._perform_callbacks.empty?
.
pry(main)> RandomClass._perform_callbacks.empty?
=> false
A colleague of mine suggested the use of #present?
instead a negative conditional with #empty?
. This is a fair point – I personally like to avoid negatives in my conditionals. Again, I want to verify it all works as expected with this change.
pry(main)> show-source ActiveSupport::Callbacks::CallbackChain#present?
From: /Users/jalbert/.rvm/gems/ruby-2.3.0/gems/activesupport-4.2.7.1/lib/active_support/core_ext/object/blank.rb @ line 23:
Owner: Object
Visibility: public
Number of lines: 3
def present?
!blank?
end
I can see that #present?
calls !blank?
. Now let’s now follow #blank?
.
pry(main)> show-source ActiveSupport::Callbacks::CallbackChain#blank?
From: /Users/jalbert/.rvm/gems/ruby-2.3.0/gems/activesupport-4.2.7.1/lib/active_support/core_ext/object/blank.rb @ line 16:
Owner: Object
Visibility: public
Number of lines: 3
def blank?
respond_to?(:empty?) ? !!empty? : !self
end
Yep! That works like I suspected it would – that method chain winds up calling ActiveSupport::Callbacks::CallbackChain#empty?
in the ending.
So if you are not using pry
, I highly recommend it. I barely scratched the surface on what it can do for you. It is a powerful tool that can help in debugging and further digging around your codebase.