Personal Website

My Web: MindEchoes.com

Tuesday, August 16, 2011

Obtener Arbol Sintactico en Python

El fin de semana estuve de "paseo" en Buenos Aires, llegue el domingo como a las 7 de la mañana, pero recién tenia el check-in en el Hostel como a las 12... así que tenia que hacer tiempo!
Dije: "bueno... voy a pasear al shopping... debe ser lo único abierto y por ahí engancho una peli"... WRONG!!
Llegue al shopping como a las 8am... y recién abría a  las 10am... así que a buscar alguna confitería donde pudiera hacer un largooooooo desayuno... Y A PROGRAMAR NINJA-IDE!

Para mi sorpresa descubrí que Palermo (por donde estaba) es bastanteeeeeeeee caro!! (no, no tengo idea de nada de las zonas de BsAs)

Así que pedí mi desayuno:

Y a empezar a programar!
Lo que quise hacer fue agregar la parte de "Tree Symbols" de NINJA-IDE, básicamente la función es mostrarte un árbol con la información del archivo que se esta visualizando, es decir:

  • Variables del Módulo
  • Funciones del Módulo
  • Clases
  • Funciones de Clase
  • Atributos de Clase
Para la versión 1.1 de NINJA-IDE, habíamos hecho una implementación de esto pero obteniendo esos datos utilizando Rope, la cual es una muy buena librería, PERO genera bastante basura en el directorio del proyecto y la documentación es mala.... lo que si tiene son muchos tests, y básicamente podes aprender a usarla leyendo los tests, pero para la versión 2.0 estamos tratando de que la única dependencia que tenga el IDE sea con PyQt, de esta forma algunas libs que usamos (que no son muchas por el momento), las hemos incluido con el IDE mismo, y las hemos modificado para que se adapten mejor a ninja, funcionando más rápido para nuestras necesidades.

Entonces tenia que armar el Widget con el Árbol de Sintaxis, pero sin usar Rope que por el momento no la pensamos incluir... y que mejor que usar: AST (Abstract Syntax Trees)

Con AST se puede obtener mucha más información sobre el código, como los Imports, ImportsFrom, donde se están agregando For, Ifs, etc.
Y varios de los elementos (funciones, clases, for, etc), son nodos que pueden ser navegador internamente para seguir obteniendo información de las cosas que contienen.

En el caso de NINJA-IDE, solo me interesaba que obtuviera la info que se mencionó antes (Variables del Módulo, Funciones del Módulo, Clases, Funciones de Clase, Atributos de Clase), por lo que cree un método "obtain_symbols", que recibe un string con el código fuente (también podríamos modificar simplemente esta función para que recibiera un path de un archivo y trabajara con eso, pero en NINJA-IDE como se van a mostrar los símbolos sobre el archivo siendo visualizado, ya tengo el código cargado en un string, y me es más rápido que ir a leer a disco), y se encarga de parsear el cuerpo de ese código fuente, almacenando en un diccionario los atributos, funciones y clases que encuentra.
En el caso de las Clases, como sabemos que estas pueden llegar a contener Atributos o Métodos, al momento de parsear la clase, no solo guardamos en que linea del código esta declarada, sino que llamamos a un método para que ahora parsee el cuerpo de la clase (vamos un nodo más abajo): "_parse_class_body", la cual recorre las funciones y atributos de la clase almacenándolos en una nueva estructura de diccionario y sobre las funciones vuelve a realizarse una búsqueda interna sobre otros atributos de la clase que se encuentren definido dentro de las funciones llamando a: "_parse_cls_function_body".

El código de esto sería:

# -*- coding: utf-8 -*-
import ast


def _parse_cls_function_body(funcBody):
    attr = {}
    for at in funcBody:
        if type(at) is ast.Assign and \
        type(at.targets[0]) is ast.Attribute:
            attr[at.targets[0].attr] = at.targets[0].lineno
    return attr


def _parse_class_body(classBody):
    attr = {}
    func = {}
    for sym in classBody:
        if type(sym) is ast.Assign:
            attr[sym.targets[0].id] = sym.lineno
        elif type(sym) is ast.FunctionDef:
            func[sym.name] = sym.lineno
            moreAttr = _parse_cls_function_body(sym.body)
            for ma in moreAttr:
                attr[ma] = moreAttr[ma]
    return {'attributes': attr, 'functions': func}


def obtain_symbols(source):
    try:
        module = ast.parse(source)
    except:
        module = ast.parse(source[source.find('\n'):])
    symbols = {}
    globalAttributes = {}
    globalFunctions = {}
    classes = {}
    for sym1 in module.body:
        if type(sym1) is ast.Assign:
            if type(sym1.targets[0]) is ast.Attribute:
                globalAttributes[sym1.targets[0].attr] = sym1.lineno
            else:
                globalAttributes[sym1.targets[0].id] = sym1.lineno
        elif type(sym1) is ast.FunctionDef:
            globalFunctions[sym1.name] = sym1.lineno
        elif type(sym1) is ast.ClassDef:
            classes[sym1.name] = (sym1.lineno, _parse_class_body(sym1.body))
    if globalAttributes:
        symbols['attributes'] = globalAttributes
    if globalFunctions:
        symbols['functions'] = globalFunctions
    if classes:
        symbols['classes'] = classes

    return symbols



Al principio de la función "obtain_symbols" estoy haciendo un try-except porque los archivos que definían un encoding fallaban al tratar de ejecutarse el "parse", y obviando la linea del encoding podían ser parseados tranquilamente... obviamente voy a tener que hacer más pruebas para asegurarme que no tenga otros efectos secundarios :P
Esta fue la primera implementación y hace EXACTAMENTE lo que quiero, quizás después deba fijarme si sería conveniente usar algo como NodeVisitor o no.

Si llamaramos a la función "obtain_symbols" pasandole como código este mismo módulo, obtendríamos como resultado:

{'functions': {'_parse_class_body': 14, 'obtain_symbols': 28, '_parse_cls_function_body': 5}}

Lo cual no es una información muy loca que estemos obteniendo, pero cuando llamamos a esta función con el código del módulo "editor.py" de NINJA-IDE, los datos que obtenemos son los siguientes:

{'functions': {'_parse_class_body': 14, 'obtain_symbols': 28, '_parse_cls_function_body': 5}}
editor^[{'functions': {'create_editor': 733}, 'classes': {'Editor': (36, {'attributes': {'syncDocErrorsSignal': 72, 'errors': 67, '_mtime': 110, 'highlighter': 71, 'newDocument': 70, 'extraSelections': 599, 'textModified': 69, 'pep8': 66, 'ask_if_externally_modified': 82, '_sidebarWidget': 61, '__actionFindOccurrences': 92, '_patIsWord': 77, 'braces': 79}, 'functions': {'jump_to_line': 181, '_find_occurrences': 194, 'mousePressEvent': 524, 'get_text': 174, 'go_to_definition': 537, 'keyPressEvent': 374, '_sync_tab_icon_notification_signal': 130, 'dropEvent': 530, 'has_write_permission': 145, 'show_pep8_errors': 116, 'show_static_errors': 123, '__init__': 55, 'indent_more': 231, 'zoom_in': 204, 'get_parent_project': 220, '_text_under_cursor': 432, 'set_flags': 97, 'set_cursor_position': 226, 'keyReleaseEvent': 369, 'contextMenuEvent': 462, 'zoom_out': 212, 'check_external_modification': 138, 'find_match': 325, 'register_syntax': 168, 'indent_less': 272, 'mouseMoveEvent': 493, 'wheelEvent': 450, 'go_to_line': 198, 'get_cursor_position': 223, 'set_font': 177, '_match_braces': 563, 'get_selection': 551, '_file_saved': 161, 'highlight_current_line': 598, 'restyle': 150, 'replace_match': 339, 'paintEvent': 437, 'focusInEvent': 362, 'set_id': 108}})}}

Los cuales una vez parseado por el Widget del Árbol de Símbolos de NINJA-IDE, nos muestra la información de esta forma:

Y nos permite navegar hacia cualquiera de esos símbolos solo haciendo click en el.

Y ahora para el espacio cultural del Post, dejo algunas fotos que saque en Buenos Aires:




Y después al mediodia del Lunes cuando almorce en Aeroparque... descubrí que todos los lugares de comida eran sucursales del Restaurant "Uy, nos rompieron el orto" de Capussotto.


Ejemplo 1:
sandwich de jamon y queso + gaseosa = 52


Vale aclarar que se aceptan mejoras sobre el código las cuales seguramente terminarán incluidas en NINJA-IDE! :P

4 comments:

luismarianoguerra said...

grande gatuso, siempre quise jugar con el modulo ast pero nunca encontre una excusa

no sabia lo de las sucursales de capussoto ;)

aca busca pizzeria:

http://techcrunch.com/2011/05/14/teardown-top-facebook-brand-page-growth/

pero basta de referencias el comentario es para decirte lo siguiente:

Santa aberracion del PEP8, usas camelCase! :P

saludos

Diego Sarmentero said...

jejej heyyyyyyyy!!!
Pep8 dice que:
mixedCase (differs from CapitalizedWords by initial lowercase character!)

es una de las convenciones...
yo sigo lo siguiente:
* Para variables: camelCase
* Para funciones: con_underscore

de esa forma los diferencio re fácil a simple vista :D

Además ninja me corrige Pep8 y no se queja jeje

Jeffry O'Nassto said...

podemos hacer lo de los achievements ahora!

Diego Sarmentero said...

Nassto:
Y podrías meterlo como plugin, y de paso nos ayudas a testear la API de plugins!! :D