RedBaron
une approche bottom-up au refactoring en Python

Presenter Notes

Moi

  • Laurent Peuch
  • Bram

En beaucoup trop détaillé : http://worlddomination.be/about/about.html

Presenter Notes

Plan

  • Pourquoi ?
  • Solution au premier problème (baron)
  • Solution au deuxième problème (RedBaron)
  • Conclusion

Presenter Notes

Avant de commencer : revisions

Presenter Notes

Refactoring (eclipse)

refactoring.png

Presenter Notes

Abstract Syntax Tree (AST)

ast.png

Presenter Notes

Pourquoi ?

Presenter Notes

Refactoring custom

  • J'ai toujours voulu écrire du code pour modifier mon code
  • Très difficile : string énorme sans sens, analyse, déplacement, trop de possibilités, syntaxe
  • Frustrant, plein de cas où "ah, si seulement je pouvais scripter cette modification !"
  • Comme une blessure à la lèvre
  • Générer du code aussi
  • seulement une poignée de gens font ça
  • Hyper dur : je suis en (x,y) dans un fichier, y a quoi autour de moi ?

Presenter Notes

Ast.py

ast.py.png

Presenter Notes

Ast.py

Pas lossless !

ast_to_code(code_to_ast(code_source)) != source_code

(Commentaires, formatting)

(Et ast_to_code n'existe même pas de manière standard).

Presenter Notes

Ast.py

API Sax like

1 class KeyAttributesFinder(ast.NodeVisitor):
2     def visit_Assign(self, assign_node):
3         # ...
4 
5     def visit_FunctionDef(self, function_node):
6         # ...
7 
8     # visit_...

Super chiant, impossible à utiliser dans IPython efficacement.

Presenter Notes

pythonfmt

Auto formater du code python

Presenter Notes

Génération de code

Django (memopol et co) :

donnees.json -> models.py + import.py

Autre projet :

Générer du boiler plate en lisant des models de db

Presenter Notes

Refactoring : top to bottom

refactoring1.png

Presenter Notes

Refactoring en python

  • BycleRepairMan
  • Rope (ast.py + regexs)
  • PyCharm

Presenter Notes

Conclusion : 2 problèmes

  • Il manque la bonne abstraction
  • Il manque la bonne interface

Presenter Notes

Solution 1 : l'abstraction -> Baron

Presenter Notes

Baron

  • ast lossless !
  • source == ast_to_code(code_to_ast(source))
  • transforme un problème d'analyse de code en parcours/modification d'un graphe
  • output du json pour compatibilité maximum (+ structure de donnée simple)

Presenter Notes

Exemple

 1 from baron.helpers import show
 2 
 3 print show("1 + 2")
 4 
 5 [
 6     {
 7         "first_formatting": [
 8             {
 9                 "type": "space",
10                 "value": " "
11             }
12         ],
13         "value": "+",
14         "second_formatting": [
15             {
16                 "type": "space",
17                 "value": " "
18             }
19         ],
20         "second": {
21             "section": "number",
22             "type": "int",
23             "value": "2"
24         },
25         "type": "binary_operator",
26         "first": {
27             "section": "number",
28             "type": "int",
29             "value": "1"
30         }
31     }
32 ]

Presenter Notes

refactoring2.png

Presenter Notes

État du projet

1 an de boulot (j'ai dû apprendre)

  • +1000 tests (TDD)
  • marche sur le top 100 de pypi
  • utilities : position_to_path, position_to_node, bounding_box, walker etc...
  • entièrement documenté

Presenter Notes

Solution 2 : l'interface -> RedBaron

Presenter Notes

Plan

  • principe
  • exploration (query)
  • modification
  • abstraction des listes

Presenter Notes

RedBaron

  • Api au dessus de Baron
  • Comme BeautifulSoup/Jquery : mapping structure de donnée -> objects
  • Pour l'humain, user friendly autant que possible
  • l'interface qui fait tous les trucs chiants pour vous
  • Pensé, entre autre, pour être utilisé dans IPython (ou bpython)

Presenter Notes

RedBaron

API super simple :

1 from redbaron import RedBaron
2 
3 red = RedBaron("string représentant du code source")
4 # ...
5 red.dumps()  # code source

Presenter Notes

Intuitif (autant que possible)

Surcharge de __repr__ :

BeautifulSoup :

soup.png

Presenter Notes

Intuitif (autant que possible)

Surcharge de __repr__ :

BeautifulSoup :

soup.png

RedBaron :

repr.png

Presenter Notes

Auto descriptif

RedBaron :

at_0.png

Presenter Notes

Auto descriptif

RedBaron :

at_0.png

".help()"

help.png

Presenter Notes

Exploration

Comme BeautifulSoup :

 1 red = RedBaron("a = 42\ndef test_chocolat(): pass")
 2 red.find("name")
 3 red.find("int", value=42)
 4 red.find("def", name="g:test_*")
 5 red.find("def", name="re:test_*")
 6 red.find("assignment", lambda x: x.target.dumps() == "INSTALLED_APPS")
 7 
 8 red.find_all("name")
 9 red.find_all(("name", "int"))
10 red.find_all("def", arguments=lambda x: len(x) == 3)
11 red.find_all("def", recursive=False)

Presenter Notes

Exploration

Comme BeautifulSoup :

 1 red = RedBaron("a = 42\ndef test_chocolat(): pass")
 2 red.find("name")
 3 red.find("int", value=42)
 4 red.find("def", name="g:test_*")
 5 red.find("def", name="re:test_*")
 6 red.find("assignment", lambda x: x.target.dumps() == "INSTALLED_APPS")
 7 
 8 red.find_all("name")
 9 red.find_all(("name", "int"))
10 red.find_all("def", arguments=lambda x: len(x) == 3)
11 red.find_all("def", recursive=False)

Raccourcis (comme BeautifulSoup) :

1 red = RedBaron("a = 42\ndef test_chocolat(): pass")
2 red.name
3 red.int
4 red.else_
5 
6 red("name")
7 red(("name", "int"))
8 red("def", arguments=lambda x: len(x) == 3)

Presenter Notes

Modification

Comment modifier une node ?

1 from redbaron import RedBaron, BinaryOperatorNode
2 
3 red = RedBaron("a = 'plop'")
4 red[O].value  # 'plop'
5 red[0].value = BinaryOperatorNode({'first_formatting':[{'type': 'space',
6 'value': ' '}], 'value': '+', 'second_formatting': [{'type': 'space',
7 'value': ' '}], 'second': {'section': 'number', 'type': 'int', 'value':
8 '1'}, 'type': 'binary_operator', 'first': {'section': 'number', 'type':
9 'int', 'value': '1'}})

Pas hyper pratique ...

Presenter Notes

Magie de __setattr__

1 from redbaron import RedBaron, BinaryOperatorNode
2 
3 red = RedBaron("a = 'plop'")
4 red[0].value = "1 + 1"
5 
6 # marche aussi avec : nodes redbaron et ast

Marche pour toutes les nodes.

Presenter Notes

Modifications avancées :

Autre problème : quel est le corps/body de la fonction "bar" ?

1 class Foo():
2     def bar(self):
3         pass
4 
5     def baz(self):
6         pass

Presenter Notes

Modifications avancées :

Autre problème : quel est le corps/body de la fonction "bar" ?

1 class Foo():
2     def bar(self):
3         pass
4 
5     def baz(self):
6         pass

Expected :

expected.png

Presenter Notes

Modifications avancés :

Autre problème : quel est le corps/body de la fonction "bar" ?

1 class Foo():
2     def bar(self):
3         pass
4 
5     def baz(self):
6         pass

Expected :

expected.png

Reality :

reality.png

Presenter Notes

Solution : magie !

magic.gif

1 red.find("def", name="bar").value = "pass"
2 red.find("def", name="bar").value = "pass\n"
3 red.find("def", name="bar").value = "    pass\n"
4 red.find("def", name="bar").value = "    pass\n    "
5 red.find("def", name="bar").value = "        pass\n        "
6 red.find("def", name="bar").value = "\n    pass\n    "
7 # etc ..

Pareil pour les : else, exceptions, finally, elif etc ...

Presenter Notes

Listes

Problème : combien d'éléments dans le corps de cette liste ? ['a', 'b', 'c']

Presenter Notes

Listes

Problème : combien d'éléments dans le corps de cette liste ?

Expected :

list_expected.png

Presenter Notes

Listes

Problème : combien d'éléments dans le corps de cette liste ?

Expected :

list_expected.png

Reality :

list_reality.png

Presenter Notes

Listes : solutions

Solution : des "proxy" de listes qui donnent la même API que les listes python et gèrent le formatting pour vous.

Reality again :

list_expected.png

Marche pour les :

  • "," (avec et sans indentation)
  • les ".", par exemple : a.b.c().pouet[stuff]
  • les lignes séparées par des retours à la ligne (corps des fonctions, "bloc python")

Presenter Notes

Helpers

  • .map
  • .apply
  • .filter
  • .next, .previous, .parent
  • .replace

Presenter Notes

Quelques exemples

1 # renommer un 'name' (attention : renommera pas tout)
2 for i in red('name', value='pouet'): i.value = 'plop'

Presenter Notes

Quelques exemples

1 # installer une django app
2 red.find("assign", target=lambda x: x.dumps() == 'INSTALLED_APPS').\
3     value.append("'debug_toolbar.apps.DebugToolbarConfig'")

Presenter Notes

Quelques exemples

1 # lines_profiler
2 red('def', recursive=False).\
3     map(lambda x: x.decorators.insert(0, '@profile'))

Presenter Notes

Quelques exemples

1 # lines_profiler
2 red('def', recursive=False).\
3     map(lambda x: x.decorators.insert(0, '@profile'))
4 
5 # les retirer
6 red("decorator", lambda x: x.dumps() == "@decorator").\
7     map(lambda x: x.parent.parent.decorators.remove(x))

Presenter Notes

Quelques exemples

1 # print a -> logger.debug(a)
2 red('print', value=lambda x: len(x) == 1).\
3     map(lambda x: x.replace('logger.debug(%s)' % x.value.dumps())
4 
5 # print a, b, c -> logger.debug("%s %s %s" % (a, b, c))
6 red('print', value=lambda x: len(x) == 1).\
7     map(lambda x: x.replace('logger.debug("%s" % (%s))' %
8                                 (" ".join('%s' * len(x.value)))

Presenter Notes

refactoring3.png

Presenter Notes

État

  • +1200 tests
  • entièrement documenté (plein d'exemples)
  • tutoriel
  • librairie de référence pour écrire du code qui modifie du code
  • encore un peu rugueux (alpha ?)
  • devrait remplir 80% des cas
  • pas d'analyse statique (plus tard avec rope/astroid ?)

Presenter Notes

Documentation

Exemples executés à la compilation :

documentation.png

Presenter Notes

Projets voisins: PyFmt

Usage:

1 from pyfmt import format_code
2 
3 format_code(source_code)

Presenter Notes

Projets voisins: RedFlyingBaron

1 red *.py  # dans un shell bash/zsh/autre

Qui lance un shell:

 1 red
 2 red[0]
 3 red["./test_redflyingbaron.py"]
 4 red["test_redflyingbaron.py"]
 5 red["test_redflyingbaron"]
 6 red[1:]
 7 
 8 red.display()
 9 
10 red[0].save()
11 red.save()
12 red[0].reload()
13 red.reload()
14 
15 red["f:redflyingbaron"]
16 red[re.compile(r'[^_]+')]
17 red["re:[^_]+"]
18 red[lambda key, value: "red" in key]
19 
20 red.find("stuff")
21 red.find_all("stuff")
22 
23 red.add("/path/to/file", "/path/to/another/file", "again.py")

Presenter Notes

Conclusion

Presenter Notes

kent.png

Presenter Notes

« Mec, t'es en train de coder le nouvel 'ed' du 21 ème siècle avec 4 niveaux d'abstractions en plus »
un pote, fin bourré

Presenter Notes

Infos

RedBaron :

Baron :

Contacts :

  • Moi : cortex@worlddomination.be
  • Irc : irc.freenode.net#baron

Presenter Notes