Pry - Une alternative à IRB

Publié le 11 septembre 2012 par Nicolas Cavigneaux | outils

Cet article est publié sous licence CC BY-NC-SA

Pry ?

Pry est une alternative à la console interactive IRB.

IRB fait très bien son travail de console interactive (REPL) mais j’ai toujours eu l’impression qu’il manquait quelques petits plus pour qu’IRB soit parfait.

Pry vient combler ces manques en nous offrant :

  • la coloration syntaxique
  • la navigation dans les sources
  • la navigation dans la documentation
  • l’ouverture du code des méthodes dans un éditeur
  • l’intégration du shell pour lancer des commandes (éditeur, rake, git, …)
  • la création de gist
  • l’extension des fonctionnalités via des plugins

Installation

Pour commencer à utiliser Pry, je vous conseille de l’installer de manière globale :

$ gem install pry pry-doc

Premiers pas

Commençons par lancer Pry et testons ses possibilités :

$ pry

Je passe ici sur toutes les fonctionnalités basiques de Pry qui se comportent exactement comme IRB.

Définition de méthodes

Pry nous permet donc de lister la définition d’une méthode, qu’elle ait été écrite en Ruby ou en C :

pry(main)> show-method Array#first

From: array.c (C Method):
Number of lines: 11
Owner: Array
Visibility: public

static VALUE
rb_ary_first(int argc, VALUE *argv, VALUE ary)
{
    if (argc == 0) {
    if (RARRAY_LEN(ary) == 0) return Qnil;
    return RARRAY_PTR(ary)[0];
    }
    else {
    return ary_take_first_or_last(argc, argv, ary, ARY_TAKE_FIRST);
    }
}


pry(main)> show-method FileUtils.rm_rf

From: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/1.9.1/fileutils.rb @ line 644:
Number of lines: 6
Owner: #Class:FileUtils
Visibility: public

def rm_rf(list, options = {})
  fu_check_options options, OPT_TABLE['rm_rf']
  options = options.dup
  options[:force] = true
  rm_r list, options
end

Je trouve cette fonctionnalité extrêmement pratique au quotidien. Il m’arrive souvent de vouloir parcourir le code de Gems que j’utilise. Maintenant plus besoin d’ouvrir les sources du gem en se déplaçant dans le bon répertoire et en lançant son éditeur. Avec Pry, il suffit de demander directement la définition d’une méthode.

Documentation des méthodes

Pry nous permet également d’avoir un accès à la documentation d’une classe ou d’une méthode donnée :

pry(main)> show-doc Enumerable#any?

From: enum.c (C Method):
Number of lines: 11
Owner: Enumerable
Visibility: public
Signature: any?()

Passes each element of the collection to the given block. The method
returns true if the block ever returns a value other
than false or nil. If the block is not
given, Ruby adds an implicit block of {|obj| obj} (that
is any? will return true if at least one
of the collection members is not false or
nil.

   %w{ant bear cat}.any? {|word| word.length >= 3}   #=> true
   %w{ant bear cat}.any? {|word| word.length >= 4}   #=> true
   [ nil, true, 99 ].any?                            #=> true

Encore une fonctionnalité que je trouve très pratique en phase de développement ou de debug, d’autant plus que Pry prend en charge la coloration syntaxique, ce qui facilite beaucoup la lecture depuis la console.

Il est également possible d’appeler la documentation ou la définition d’une méthode directement sur une instance :

pry(main)> s = "lorem"
=> "lorem"

pry(main)> show-doc s.each_char

From: string.c (C Method):
Number of lines: 11
Owner: String
Visibility: public
Signature: each_char()

str.each_char {|cstr| block }    -> str
   str.each_char                    -> an_enumerator

Passes each character in str to the given block, or returns
an enumerator if no block is given.

   "hello".each_char {|c| print c, ' ' }

produces:

   h e l l o

Vous pouvez également avoir accès à la documentation RI depuis la console.

pry(main)> show-doc Fixnum#+

From: numeric.c (C Method):
Number of lines: 3
Owner: Fixnum
Visibility: public
Signature: +(arg1)

Performs addition: the class of the resulting object depends on
the class of numeric and on the magnitude of the
result.

Pour ma part je préfère show-doc que je trouve plus lisible et qui a pour avantage de gérer le format de documentation YARD.

Partage de code, documentatio, session Pry

Il est possible d’installer un paquet optionnel depuis Pry pour pouvoir créer des Gist directement depuis la console. Bien pratique quand vous travaillez en équipe et souhaitez un regard neuf sur votre code.

Commençons donc par installer la commande :

pry(main)> install-command gist

Nous pouvons, dès lors, utiliser la commande “gist” pour créer des Gist sans sortir de notre session Pry :

gist -m my_method       # crée un gist des sources de la méthode donnée
gist -d my_method       # crée un gist de la documentation de la méthode donnée
gist -i 1..10           # crée un gist pour les commandes entrée de 1 à 10
gist -c Array           # créer un gist pour la classe Array

Ceci n’est qu’un exemple des possibilités. Pour tout savoir sur cette commande, vous pouvez entrer :

help gist

Notez aussi que si vous voulez associer les gists créés à votre compte Github, il faut d’abord vous identifier via :

pry(main)> gist --login
Obtaining OAuth2 access_token from github.
Github username: Bounga
Github password:
Success! https://github.com/settings/applications

Navigation

L’un des grands atouts de Pry est qu’il permet de naviguer à travers les classes et les objets mais il permet également d’utiliser le shell sans avoir à sortir de la session Pry.

À travers les objets

Listons les méthodes de classe de Array :

pry(main)> ls Array -m
Array.methods: []  try_convert

puis les méthodes d’instance :

pry(main)> ls Array -M
Enumerable#methods: all?  any?  chunk  collect_concat  detect  each_cons  each_entry  each_slice  each_with_index  each_with_object  entries  find  find_all  flat_map  grep  group_by  inject  max  max_by  member?  min  min_by  minmax  minmax_by  none?  one?  partition  reduce  slice_before  sort_by
JSON::Ext::Generator::GeneratorMethods::Array#methods: to_json
Array#methods: &  *  +  -  <<  <=>  ==  []  []=  assoc  at  clear  collect  collect!  combination  compact  compact!  concat  count  cycle  delete  delete_at  delete_if  drop  drop_while  each  each_index  empty?  eql?  fetch  fill  find_index  first  flatten  flatten!  frozen?  hash  include?  index  insert  inspect  join  keep_if  last  length  map  map!  pack  permutation  place  pop  pretty_print  pretty_print_cycle  product  push  rassoc  reject  reject!  repeated_combination  repeated_permutation  replace  reverse  reverse!  reverse_each  rindex  rotate  rotate!  sample  select  select!  shelljoin  shift  shuffle  shuffle!  size  slice  slice!  sort  sort!  sort_by!  take  take_while  to_a  to_ary  to_s  transpose  uniq  uniq!  unshift  values_at  zip  |

Maintenant déplaçons nous dans des objets ActiveRecord :

pry(main)> Price
=> Price(id: integer, price: float, created_at: datetime, updated_at: datetime, product_item_id: integer, shop_id: integer, minimum_price: float)

pry(main)> cd Price

pry(Price):1> first
=> #<Price id: 112248585, price: 12.9, created_at: "2012-08-27 10:43:32", updated_at: "2012-08-27 10:43:32", product_item_id: 1010763938, shop_id: 459005629, minimum_price: nil>

pry(Price):1> cd first

pry(#<Price>):2> price
=> 12.9

pry(#<Price>):2> nesting
Nesting status:
--
0. main (Pry top level)
1. Price
2. #<Price>

pry(#<Price>):2> jump-to 0
pry(main)>

Utilisation du shell

Il est possible de se déplacer dans votre arborescence et d’exécuter n’importe quelle commande disponible en console :

pry(main)> gem-cd activesupport
/usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activesupport-3.2.6

pry(main)> .ls
CHANGELOG.md  MIT-LICENSE README.rdoc lib

pry(main)> dir = File.dirname(log_file)
=> "/var/log"

pry(main)> .cd #{dir}

pry(main)> .pwd
/var/log

pry(main)> .date
Jeu 30 aoû 2012 17:27:14 CEST

Ligne de commande et édition

Dans Pry, l’édition sur la ligne de commande n’est pas en reste. Il est par exemple possible modifier une ligne déjà validée précédemment, annuler la saisie courante, sauvez les commandes et résultats dans des fichiers, … :

pry(main)> def helo(name)
pry(main)*   puts "Hi #{name}"
pry(main)*   show-input
1: def helo(name)
2:   puts "Hi #{name}"
pry(main)*   amend-line 1 def hello(name)
1: def hello(name)
2:   puts "Hi #{name}"
[16] pry(main)*   end
=> nil

pry(main)> def test(foo, bar)
pry(main)*   puts [foo, bar].join
pry(main)*   !
Input buffer cleared!

pry(main)> save-file -m hello ./hello.rb
pry(main)> cat ./hello.rb
def hello(name)
  puts "Hi #{name}"
end

pry(main)> save-file -i 1..10 ./hello.rb --append
pry(main)> cat ./hello.rb
def hello(name)
  puts "Hi #{name}"
end
Price
first
price

pry(main)> save-file -k show-method ./my_command.rb
pry(main)> cat my_command.rb
c = block_command match, desc, options do |*args|
  run action, *args
end

Variables locales automatiques

Pry instancie et gère automatiquement un certain nombre de variables locales qui peuvent s’avérer utiles :

  • _ : résultat de la dernière expression
  • ex : résultat de la dernière exception
  • in : tableau des commandes entrées (hors commandes Pry)
  • out : tableau des résultats obtenus (hors commandes Pry)
  • file : dernier fichier ouvert

Personnalisation

Comme pour IRB, il est possible de définir des valeurs par défaut à utiliser au lancement de Pry pour le personnaliser.

Options

Les options à appliquer à Pry peuvent être définies globalement dans le fichier ~/.pryrc.

Voyons quelles sont les principales options disponibles :

Pry.config.color = true # active / désactive la coloration syntaxique
Pry.config.pager = false # active / désactive le pager pour la doc, les sources, …
Pry.config.auto_indent = false # active / désactive l'indentation automatique
Pry.config.correct_indent = false # active / désactive la correction automatique de l'indentation
Pry.config.command_prefix = "%" # caractère utilisé pour dénoter l'appel d'une commande shell
Pry.config.history.file = "~/.irb_history" # chemin du fichier d'historique
Pry.config.editor = proc { |file, line| "st -w #{file}:#{line}" } # éditeur utilisé
Pry.config.prompt = proc { |obj, nest_level, _| "#{obj}:#{nest_level}> " } # format du prompt
Pry.config.print = proc { |output, value| output.puts "=> #{value.inspect}" } # format d'affichage des valeurs de retour

Pry.config.exception_handler = proc do |output, exception, _|
  output.puts "#{exception.class}: #{exception.message}"
  output.puts "from #{exception.backtrace.first}"
end # Personnalisation de la réaction aux exceptions

Awesome print

Pour pouvoir utiliser awesome_print en coordination avec Pry, il nous faut modifier la configuration de print :

begin
  require 'awesome_print'
  Pry.config.print = proc { |output, value| output.puts value.ai }
rescue LoadError => err
  puts "no awesome_print"
end

Debug avec Pry

L’autre grand domaine de prédilection de Pry est le debug. En effet, il s’avère particulièrement bien pensé lorsqu’il s’agit d’inspecter une pile dans un état donné.

On peut soit lancer Pry en chargeant le code a explorer :

$ pry -r ./lib/ma_lib.rb

Ce qui permet ensuite de faire des tests, manipuler les objets, les examiner, etc. On peut également lancer Pry en mode “runtime invocation”, ce qui permet à la façon de n’importe quel debbuger de poser un “breakpoint” qui stoppera l’exécution de la pile et vous passera la main en ouvrant une session Pry dans votre terminal.

Pour ce faire il faut que votre projet utilise Pry (à ajouter en development dans votre Gemfile) et ensuite y faire appel dans votre code. Premièrement, il faut charger Pry avec un simple

require 'pry'

Il ne reste ensuite plus qu’à poser des breakpoint, là où bon vous semble :

binding.pry

À l’exécution, une fois arrivé sur cette ligne, l’interpréteur vous rend la main en vous proposant une session Pry.

Debugger en utilisant Pow

Si vous utilisez Pow pour vos développements Rails / Rack, vous n’avez pas de terminal dédié à l’exécution du serveur de votre appli. Il est donc impossible pour Pry de vous rendre la main en mode interactif.

Pour palier à ce problème, un plugin a été développé. Ce plugin permet d’avoir des sessions Pry distantes. On peut donc lancer une appli sans utiliser de shell interactif tout en concervant la possibilité de se connecter (à distance) à une session Pry lorsqu’un breakpoint est rencontré.

Il faut donc installer le gem pry-remote. Une fois installé, dans le code de votre app, les breakpoint se poseront à l’aide de binding.remote_pry.

Vous pouvez ensuite dans un terminal, lancer la commande pry-remote qui se chargera de se connecter à la session Pry distante (via DRB).

Analyser les exceptions

Dans une session Pry, comme pour IRB, lorsqu’une exception est levée, elle est attrapée par la console. Vous êtes notifié de l’exception qui a été levée, sans plus.

Si vous souhaitez obtenir un extrait de la trace, une commande est mise à votre disposition :

pry(main)> 1 / 0
ZeroDivisionError: divided by 0
from (pry):2:in `/'

pry(main)> wtf?
Exception: ZeroDivisionError: divided by 0
--
0: (pry):2:in `/'
1: (pry):2:in `__pry__'
2: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:275:in `eval'
3: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:275:in `re'
4: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:251:in `rep'
5: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:231:in `block (3 levels) in repl'
6: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:230:in `loop'
7: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:230:in `block (2 levels) in repl'
8: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:229:in `catch'
9: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:229:in `block in repl'

pry(main)> wtf???
Exception: ZeroDivisionError: divided by 0
--
 0: (pry):2:in `/'
 1: (pry):2:in `__pry__'
 2: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:275:in `eval'
 3: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:275:in `re'
 4: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:251:in `rep'
 5: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:231:in `block (3 levels) in repl'
 6: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:230:in `loop'
 7: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:230:in `block (2 levels) in repl'
 8: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:229:in `catch'
 9: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:229:in `block in repl'
10: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:228:in `catch'
11: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_instance.rb:228:in `repl'
12: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/pry_class.rb:154:in `start'
13: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/cli.rb:171:in `block in <top (required)>'
14: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/cli.rb:65:in `call'
15: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/cli.rb:65:in `block in parse_options'
16: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/cli.rb:65:in `each'
17: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/lib/pry/cli.rb:65:in `parse_options'
18: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/pry-0.9.10/bin/pry:16:in `<top (required)>'
19: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/bin/pry:23:in `load'
20: /usr/local/Cellar/rbenv/0.3.0/versions/1.9.3-p194/bin/pry:23:in `<main>'

Oui, vous avez bien lu, la méthode en question est wtf? et plus vous ajouterez de ? à la fin du nom de méthode, plus elle vous retournera de ligne de contexte.

Vous pouvez également faire un cat sur l’exception pour en avoir une description succinte vous rappelant le type d’exception levée ainsi que l’appel incriminé :

pry(main)> cat --ex
Exception: ZeroDivisionError: divided by 0
--
From: (pry) @ line 2 @ level: 0 of backtrace (of 20).

    1: Toto
 => 2: 1 / 0

Si le code incriminé n’a pas été tapé dans la console mais provient d’un fichier chargé, il est alors possible de l’éditer directement à l’endroit qui a levé l’exception :

pry(main)> edit --ex

Il est à noter que le fichier en question sera rechargé automatiquement dans Pry.

Il est également possible d’éditer le fichier de son choix en ouvrant l’éditeur sur ce dernier à une ligne donnée :

pry(main)> edit hello.rb:10

Historique

Pry propose de conserver un historique des commandes entrées, il est ensuite possible de le manipuler pour retrouver ou rejouer des commandes données :

pry(main)> hist
1: Order.where("messages IS NOT NULL")
2: Order.where("messages IS NOT NULL").count
3: Order.with_pending_messages

pry(main)> hist --grep Order
1: Order.where("messages IS NOT NULL")
2: Order.where("messages IS NOT NULL").count
3: Order.with_pending_messages
7: Order.with_pending_messages.all

pry(main)> hist --tail 5
# => retourne les cinq éléments les plus récents

pry(main)> hist --head 5
# => retourne les cinq éléments les plus anciens

pry(main> hist --replay 7000
# => rejoue la commande numéro 7000

pry(main)> hist --replay 7000..7010
# => rejoue les commandes 7000 à 7010

pry(main)> hist --exclude
# => ne contient que les expressions Ruby mais aucune commande Pry

pry(main)> hist --save 4..5 test.rb
Saving history in /Users/cavigneaux/articles/test.rb ...
... history saved.

pry(main)> hist --show 2..5
2: quit
3: Order.where("messages IS NOT NULL")
4: Order.where("messages IS NOT NULL").count
5: reload!

Utilisation dans rails

Charger Pry manuellement

Plusieurs possibilités s’offrent à nous, tout d’abord remplacer purement et simplement IRB par Pry. Il faudra modifier votre Gemfile pour y inclure Pry :

Gemfile: gem "pry", group: "development"

Puis ajouter ceci à votre fichier config/environments/development.rb:

silence_warnings do
  begin
    require 'pry'
    IRB = Pry
  rescue LoadError
  end
end

Désormais, en lançant rails console, c’est en fait un session Pry qui démarrera.

On peut également choisir de garder IRB et d’ajouter une tâche Rake pour lancer une console Pry. Ajoutons donc un fichier lib/tasks/pry.rake :

desc "Start a Pry console"
task :console do
   pry -r ./../config/environment
end

Nous sommes maintenant en mesure de lancer notre session Pry via rake console.

Charger Pry via un Gem

Pry-rails est un Gem qui permet de faire cette manipulation pour vous. Il ajoute en plus quelques méthode pour pouvoir facilement lister les routes, les models, …

Une simple ligne à ajouter à votre Gemfile et vous voilà sous Pry.

Remplacement complet d’IRB

Une fois qu’on a goûté à Pry, il est assez difficile de revenir en arrière et d’utiliser à nouveau IRB.

Il est possible de faire en sorte que Pry soit utilisé par défaut, chaque fois qu’IRB est appelé. Si vous souhaitez utiliser PRy en lieu et place d’IRB, voici les quelques lignes de code à ajouter à votre ~/.irbrc :

begin
  require "pry"
  Pry.start
  exit
rescue LoadError => e
  warn "=> Unable to load pry"
end

Conclusion

Pry est donc une alternative très intéressante à IRB et nous offre des fonctionnalités très pratiques pour développer et débugguer. Je vous conseille de jeter un œil au README du projet si vous voulez en apprendre d’avantage.

L’équipe Synbioz.

Libres d’être ensemble.