ast _fields python abstract-syntax-tree

ast - _fields python



Cómo obtener lineno de "fin de declaración" en Python ast (5)

Estoy tratando de trabajar en un script que manipula otro script en Python, el script que se va a modificar tiene una estructura como:

class SomethingRecord(Record): description = ''This records something'' author = ''john smith''

Utilizo ast para localizar el número de línea de description , y uso algún código para cambiar el archivo original con una nueva cadena de descripción en el número de línea. Hasta aquí todo bien.

Ahora el único problema es la description ocasionalmente es una cadena de varias líneas, por ejemplo

description = (''line 1'' ''line 2'' ''line 3'')

o

description = ''line 1'' / ''line 2'' / ''line 3''

y solo tengo el número de línea de la primera línea, no las siguientes líneas. Entonces mi sustituto de una línea haría

description = ''new value'' ''line 2'' / ''line 3''

y el código está roto. Pensé que si conocía el lineno de inicio y final / número de líneas de asignación de description , podría reparar mi código para manejar esa situación. ¿Cómo obtengo dicha información con la biblioteca estándar de Python?


Como solución, puede cambiar:

description = ''line 1'' / ''line 2'' / ''line 3''

a:

description = ''new value''; tmp = ''line 1'' / ''line 2'' / ''line 3''

etc.

Es un cambio simple pero, de hecho, un código feo producido.


De hecho, la información que necesita no se almacena en el ast . No sé los detalles de lo que necesita, pero parece que podría usar el módulo tokenize de la biblioteca estándar. La idea es que cada declaración lógica de Python NEWLINE un token NEWLINE (también podría ser un punto y coma, pero como entiendo, no es su caso). Probé este enfoque con dicho archivo:

# first comment class SomethingRecord: description = (''line 1'' ''line 2'' ''line 3'') class SomethingRecord2: description = (''line 1'', ''line 2'', # comment in the middle ''line 3'') class SomethingRecord3: description = ''line 1'' / ''line 2'' / ''line 3'' whatever = ''line'' class SomethingRecord3: description = ''line 1'', / ''line 2'', / ''line 3'' # last comment

Y esto es lo que propongo hacer:

import tokenize from io import BytesIO from collections import defaultdict with tokenize.open(''testmod.py'') as f: code = f.read() enc = f.encoding rl = BytesIO(code.encode(enc)).readline tokens = list(tokenize.tokenize(rl)) token_table = defaultdict(list) # mapping line numbers to token numbers for i, tok in enumerate(tokens): token_table[tok.start[0]].append(i) def find_end(start): i = token_table[start][-1] # last token number on the start line while tokens[i].exact_type != tokenize.NEWLINE: i += 1 return tokens[i].start[0] print(find_end(3)) print(find_end(8)) print(find_end(15)) print(find_end(21))

Esto se imprime:

5 12 17 23

Esto parece ser correcto, puede ajustar este enfoque dependiendo de lo que necesite exactamente. tokenize es más detallado que ast pero también más flexible. Por supuesto, el mejor enfoque es usarlos tanto para diferentes partes de su tarea.

EDITAR: Intenté esto en Python 3.4, pero creo que también debería funcionar en otras versiones.


Hay una nueva biblioteca de asttokens que aborda este problema: https://github.com/gristlabs/asttokens

import ast, asttokens code = '''''' class SomethingRecord(object): desc1 = ''This records something'' desc2 = (''line 1'' ''line 2'' ''line 3'') desc3 = ''line 1'' / ''line 2'' / ''line 3'' author = ''john smith'' '''''' atok = asttokens.ASTTokens(code, parse=True) assign_values = [n.value for n in ast.walk(atok.tree) if isinstance(n, ast.Assign)] replacements = [atok.get_text_range(n) + ("''new value''",) for n in assign_values] print(asttokens.util.replace(atok.text, replacements))

produce

class SomethingRecord(object): desc1 = ''new value'' desc2 = (''new value'') desc3 = ''new value'' author = ''new value''


Mi solución toma un camino diferente: cuando tuve que cambiar el código en otro archivo, abrí el archivo, encontré la línea y obtuve todas las líneas siguientes que tenían una sangría más profunda que la primera y devuelvo el número de línea para la primera línea que no es t más profundo. Devuelvo Ninguno, Ninguno si no pude encontrar el texto que estaba buscando. Esto es, por supuesto, incompleto, pero creo que es suficiente para ayudarte :)

def get_all_indented(text_lines, text_in_first_line): first_line = None indent = None for line_num in range(len(text_lines)): if indent is not None and first_line is not None: if not text_lines[line_num].startswith(indent): return first_line, line_num # First and last lines if text_in_first_line in text_lines[line_num]: first_line = line_num indent = text_lines[line_num][:text_lines[line_num].index(text_in_first_line)] + '' '' # At least 1 more space. return None, None


Miré las otras respuestas; parece que las personas hacen saltos para evitar los problemas de calcular los números de línea, cuando su problema real es modificar el código. Eso sugiere que la maquinaria de referencia no te está ayudando de la manera que realmente necesitas.

Si usa un sistema de transformación de programa (PTS) , puede evitar muchas de estas tonterías.

Un buen PTS analizará su código fuente en un AST, y luego le permitirá aplicar reglas de reescritura a nivel de fuente para modificar el AST, y finalmente convertirá el AST modificado nuevamente en texto de origen. Genéricamente, los PTS aceptan reglas de transformación esencialmente de esta forma:

if you see *this*, replace it by *that*

[Un analizador sintáctico que construye un AST NO es un PTS. No permiten reglas como esta; Puedes escribir un código ad hoc para hackear el árbol, pero eso suele ser bastante incómodo. No hacen el AST para generar regeneración de texto.]

(Mi PTS, ver biografía, llamado) DMS es un PTS que podría lograr esto. El ejemplo específico de OP se lograría fácilmente mediante el uso de la siguiente regla de reescritura:

source domain Python; -- tell DMS the syntax of pattern left hand sides target domain Python; -- tell DMS the syntax of pattern right hand sides rule replace_description(e: expression): statement -> statement = " description = /e " -> " description = (''line 1'' ''line 2'' ''line 3'')";

A la regla de una transformación se le asigna un nombre replace_description para distinguirla de todas las demás reglas que podamos definir. Los parámetros de la regla (e: expresión) indican que el patrón permitirá una expresión arbitraria según lo definido por el idioma de origen. declaración-> declaración significa que la regla mapea una declaración en el idioma de origen, a una declaración en el idioma de destino; podríamos usar cualquier otra categoría de sintaxis de la gramática de Python proporcionada a DMS. El " usado aquí es un metaquote , usado para distinguir la sintaxis del lenguaje de reglas de la sintaxis del lenguaje del tema. El segundo -> separa el patrón fuente de este del patrón objetivo.

Notará que no es necesario mencionar los números de línea. El PTS convierte la sintaxis de la superficie de la regla en los AST correspondientes al analizar los patrones con el mismo analizador utilizado para analizar el archivo de origen. Los AST producidos para los patrones se usan para efectuar la coincidencia / reemplazo del patrón. Como esto se deriva de los AST, la disposición real del código original (espaciado, saltos de línea, comentarios) no afecta la capacidad del DMS de coincidir o reemplazar. Los comentarios no son un problema para la coincidencia porque están unidos a nodos de árbol en lugar de ser nodos de árbol; se conservan en el programa transformado. DMS captura información de líneas y columnas precisas para todos los elementos del árbol; simplemente no es necesario para implementar transformaciones. El diseño del código también se preserva en el resultado por DMS, usando esa información de línea / columna.

Otros PTS ofrecen generalmente capacidades similares.