Awesome print is a nice gem that formats your output in irb
& pry
to make it more readable.
For example…
This is what displaying a hash with awesome_print
looks like:
But how does this work?
“Truth can only be found in one place: the code.” ― Robert C. Martin
Let’s take a look at the source code to find out!
Printing Awesomely
I like to start a code reading session with a quick overview of the project structure (files & folders), then I like to ask a question to focus my exploration going forward.
So the first question I came up with is:
How does awesome_print
change the output of pry?
Now I put my detective hat on & make a hypothesis:
“This gem could be replacing $stdout
so that it can capture the output of pry & then make it pretty.”
But this implementation wouldn’t allow you to customize your output, like pretty_print
(from Ruby’s Standard Library) does.
Also, we would have to parse, or even eval code, not a great idea!
Next:
I looked into how this gem is loaded.
This will give us an entry point into the code.
Loading Awesome Print
To load awesome_print
on pry you have to do this:
require 'awesome_print' AwesomePrint.pry!
Now we want to find where pry!
is defined.
I used the “search in directory” feature in Atom to find it.
Here’s the code:
def pry! Pry.print = proc { |output, value| output.puts value.ai } if defined?(Pry) end
It seems like Pry allows you to modify its output by setting the value of print
.
So this answers our “where to get started” question 🙂
Want to learn more about this “proc” thing? Read “The Ultimate Guide to Blocks, Procs & Lambdas”. This is a sample chapter from my Ruby Deep Dive book.
This proc takes two arguments:
- output
- value
And it calls two methods on those objects.
Next question:
What is this ai
method?
It’s a method defined on the Kernel
module:
def ai(options = {}) ap = AwesomePrint::Inspector.new(options) awesome = ap.awesome(self) if options[:html] awesome = "</pre>#{awesome}</pre>" awesome = awesome.html_safe if defined? ActiveSupport end awesome end alias :awesome_inspect :ai
Because all objects include Kernel
by default, they will have this ai
method available on them.
Now let’s dig a little deeper & see how this Inspector
class works.
The Inspector Class
Right away after opening inspector.rb
we find a huge options hash inside the initialize
method.
Here’s part of it:
@options = { indent: 4, # Number of spaces for indenting. index: true, # Display array indices. html: false, # Use ANSI color codes rather than HTML. multiline: true, # Display in multiple lines. # ... }
After that we can find this code:
@formatter = AwesomePrint::Formatter.new(self) @indentator = AwesomePrint::Indentator.new(@options[:indent].abs) Thread.current[AP] ||= []
So this sets up two more objects which seem to handle the formatting itself & indentation of the code.
But what’s with this Thread.current
thing?
Well, this gives you access to the current thread. Even if you are not using threads in your application you will have one, the “main” thread.
This AP
constant is just a constant which is defined at the top of inspector.rb
:
AP = :__awesome_print__
So what’s happening here?
Awesome print is using Thread.current
& the __awesome_print__
key to save some data that is only available on the current thread.
This is used to avoid problems with multi-threading.
Awesome Formatting
Let’s take a look at the output formatting code, which happens inside the AwesomePrint::Formatter
class.
The way this works is that the inspector (the object created by the ai
method) will call the format
method.
def unnested(object) @formatter.format(object, printable(object)) end
Then this format
method on the Formatter
class will find the best way to deal with this type of object & call another method using a bit of metaprogramming.
Here’s the method:
def format(object, type = nil) core_class = cast(object, type) awesome = if core_class != :self send(:"awesome_#{core_class}", object) # Core formatters. else awesome_self(object, type) # Catch all that falls back to object.inspect. end awesome end
To understand this Formatter
class we also need to take a look at the cast
method:
def cast(object, type) CORE.grep(type)[0] || :self end
The CORE
constant is an array of symbols representing core Ruby classes & :self
is a symbol used to mean “not found” (just in this example, not in Ruby in general).
CORE = [:array, :bigdecimal, :class, :dir, :file, :hash, :method, :rational, :set, :struct, :unboundmethod]
What happens is this:
If the object being formatted is in the “core class” list then it will get a specialized format.
Otherwise, it will get a generic one.
Specialized formatters are defined under the lib/awesome_print/formatters/
directory & include things like Array
, Hash
& Class
.
For example, here’s the formatter method for classes:
def format superclass = klass.superclass if superclass colorize("#{klass.inspect} < #{superclass}", :class) else colorize(klass.inspect, :class) end end
You can write your own formatters if you want.
Summary
You have learned about the Awesome Print gem, which lets you display objects like arrays & hashes in a nice way.
Hope you enjoyed this & learned something new!
Please share this post on your favorite social network now so more people can learn 🙂