Escribe todo

Hola a todos!


Ya tenemos un artículo sobre el desarrollo de la escritura en Ostrovok.ru . Explica por qué estamos cambiando de pyContracts a typeguard, por qué estamos cambiando a typeguard y con qué terminamos. Y hoy les contaré más sobre cómo ocurre esta transición.



Una declaración de función con pyContracts generalmente se ve así:


from contracts import new_contract import datetime @new_contract def User (x): from models import User return isinstance(x, User) @new_contract def dt_datetime (x): return isinstance(x, datetime.datetime) @contract def func(user_list, amount, dt=None): """ :type user_list: list(User) :type amount: int|float :type dt: dt_datetime|None :rtype: bool """ 

Este es un ejemplo abstracto, porque no encontré en nuestro proyecto una definición de una función que sea breve y significativa en términos de la cantidad de casos para la verificación de tipo. Normalmente, las definiciones de pyContracts se almacenan en archivos que no contienen ninguna otra lógica. Tenga en cuenta que aquí Usuario es una clase de usuario específica y no se importa directamente.


Y este es el resultado deseado con typeguard:


 from typechecked import typechecked from typing import List, Optional, Union from models import User import datetime @typechecked def func (user_list: List[User], amount: Union[int, float], dt: Optional[datetime.datetime]=None) -> bool: ... 

En general, hay tantas funciones y métodos con verificación de tipo en el proyecto que si los apilas en una pila, puedes llegar a la luna. Entonces, traducirlos manualmente de pyContracts a typeguard simplemente no es posible (¡lo intenté!). Entonces decidí escribir un guión.


El script se divide en dos partes: una almacena en caché las importaciones de nuevos contratos y la segunda trata con la refactorización de código.


Quiero señalar que ni uno ni el otro script dicen ser universales. No teníamos el objetivo de escribir una herramienta para resolver todos los casos requeridos. Por lo tanto, a menudo omití el procesamiento automático de algunos casos especiales, si rara vez se encuentran en el proyecto, es más rápido solucionarlo a mano. Por ejemplo, el script para generar contratos de mapeo e importaciones recolectó el 90% de los valores, el 10% restante es mapeo de elaboración artesanal.


La lógica del script para generar mapeo:


Paso 1. Revisa todos los archivos del proyecto, léelos. Para cada archivo:


  • si la subcadena "@new_contract" no está presente, omita este archivo,
  • si es así, divida el archivo por la línea "@new_contract". Para cada artículo:
    - analizar para su definición e importación,
    - si tiene éxito, escriba en el archivo de éxito,
    - Si no, escriba en el archivo de error.

Paso 2. Procesar errores manualmente


Ahora que tenemos los nombres de todos los tipos que utiliza pyContracts (se definieron con el decorador new_contract), y tenemos todas las importaciones necesarias, podemos escribir código para refactorizar. Mientras traducía de pyContracts a typeguard manualmente, me di cuenta de lo que necesitaba del script:


  1. Este es un comando que toma el nombre de un módulo como argumento (se pueden usar varios), en el que se debe reemplazar la sintaxis de las anotaciones de función.
  2. Revise todos los archivos del módulo, léalos. Para cada archivo:
    • si no hay una subcadena "@contract", omita este archivo;
    • si es así, convierta el código en ast (árbol de sintaxis abstracta);
    • encuentre todas las funciones que están bajo el decorador de contrato para cada una:
      • obtener la cadena de acoplamiento, analizar y luego eliminar,
      • cree un diccionario de la forma {arg_name: arg_type}, úselo para reemplazar la anotación de función,
      • recordar nuevas importaciones,
    • escribe el árbol modificado en un archivo a través de astunparse;
    • agregar nuevas importaciones a la parte superior del archivo;
    • reemplace las líneas "@contract" con "@typechecked" porque es más fácil que a través de ast.

Resuelva la pregunta "¿este nombre ya está importado en este archivo?" No tenía la intención desde el principio: con este problema nos enfrentaremos a una ejecución adicional de la biblioteca isort.


Pero después de ejecutar la primera versión del script, surgieron preguntas que aún tenían que resolverse. Resultó que 1) ast no es omnipotente, 2) astunparse es más omnipotente de lo que nos gustaría. Esto se manifestó en lo siguiente:


  • en el momento de la transición al árbol de sintaxis, todos los comentarios de una sola línea desaparecen del código;
  • las líneas vacías también desaparecen;
  • Para no distinguir entre funciones y métodos de la clase, tuvimos que agregar lógica;
  • por el contrario, al pasar de un árbol a un código, los comentarios de varias líneas en comillas triples se escriben en comillas simples y ocupan una línea, y los nuevos saltos de línea se reemplazan por \ n;
  • aparecen corchetes innecesarios, por ejemplo, si A y B y C o D se convierten en if ((A y B y C) o D).

El código pasado a través de ast y astunparse sigue funcionando, pero se reduce su legibilidad.


El inconveniente más grave de lo anterior es la desaparición de los comentarios de una sola línea (en otros casos, no perdemos nada, solo ganamos, entre paréntesis, por ejemplo). La biblioteca horast basada en ast, astunparse y tokenize promete resolver esto. Promesas y cumple.


Ahora las líneas vacías. Había dos posibles soluciones:


  1. tokenize sabe cómo determinar la "parte del habla" de una pitón, y horast lo aprovecha cuando obtiene tokens de tipo comentario. Pero tokenize también tiene tokens como NewLine y NL. Por lo tanto, debe ver cómo horast restaura los comentarios y copiar, reemplazando el tipo de token.
    - sugirió Anya, experiencia en el desarrollo de 2 meses
  2. Como horast puede restaurar los comentarios, primero reemplazamos todas las líneas vacías con un comentario específico, luego saltamos a través de horast y reemplazamos nuestro comentario con una línea vacía.
    - Se le ocurrió Eugene, experiencia en el desarrollo de 8 años.

Diré un poco más abajo sobre las comillas triples en los comentarios, y fue bastante fácil soportar los corchetes adicionales, especialmente porque algunos de ellos se eliminan mediante el formateo automático.


En horast usamos dos funciones: parse y unparse, pero ambas no son ideales: el análisis contiene errores internos extraños, en casos excepcionales no puede analizar el código fuente y no puede escribir algo que tenga tipo de tipo (un tipo que Resulta que si escribes (any_other_type)).


Decidí no tratar con el análisis, porque la lógica del trabajo es bastante confusa, y las excepciones son raras: el principio de no universalidad funciona aquí.


Pero unparse funciona de manera muy clara y elegante. La función unparse crea una instancia de la clase Unparser, que en init procesa el árbol y luego lo escribe en un archivo. Horast.Unparser se hereda sucesivamente de muchos otros Unparsers, donde la clase más básica es astunparse.Unparser. Todas las clases descendientes simplemente extienden la funcionalidad de la clase base, pero la lógica del trabajo sigue siendo la misma, así que considere astunparse.Unparser. Tiene cinco métodos importantes:


  1. escribir: solo escribe algo en un archivo.
  2. fill: utiliza la escritura en función del número de sangrías (el número de sangrías se almacena como un campo de clase).
  3. enter: aumenta la sangría.
  4. licencia: reduce la sangría.
  5. despacho: determina el tipo de nodo del árbol (digamos T), llama al método correspondiente por el nombre del tipo de nodo, pero con guión bajo (es decir, _T). Este es un meta método.

Todos los demás métodos son métodos de la forma _T, por ejemplo, _Module o _Str. En cada uno de estos métodos, puede: 1) despachar recursivamente para nodos de subárbol, o 2) usar escribir para escribir el contenido del nodo o agregar caracteres y palabras clave para que el resultado sea una expresión válida en python.


Por ejemplo, encontramos un nodo de tipo arg, en el cual ast almacena el nombre del argumento y el nodo de anotación. Luego, dispatch llamará al método _arg, que primero escribirá el nombre del argumento, luego escribirá los dos puntos y ejecutará el despacho para el nodo de anotación, donde se analizará el subárbol de anotación, y se seguirá llamando a despacho y escritura para este subárbol.


Volvamos a nuestro problema de la imposibilidad de procesar el tipo de tipo. Ahora que comprende cómo funciona el análisis, es fácil crear su tipo. Vamos a crear algún tipo:


 class NewType(object): def __init__ (self, t): self.s = ts 

Almacena una cadena en sí misma, y ​​no solo así: necesitamos tipificar argumentos de función, y obtenemos los tipos de argumentos en forma de cadenas desde el acoplamiento. Por lo tanto, reemplacemos las anotaciones de argumentos no con los tipos que requerimos, sino con un objeto NewType que almacena solo el nombre del tipo deseado en su interior.


Para hacer esto, expanda horast.Unparser: escriba su UnparserWithType, herede de horast.Unparser y agregue el procesamiento de nuestro nuevo tipo.


 class UnparserWithType(horast.Unparser): def _NewType (self, t): self.write(ts) 

Esto se combina con el espíritu de la biblioteca. Los nombres de las variables están hechos al estilo de ast, y es por eso que consisten en una letra, y no porque no se me ocurran nombres. Creo que t es la abreviatura de tree y s para string. Por cierto, NewType no es una cadena. Si quisiéramos que se interpretara como un tipo de cadena, entonces tendríamos que escribir comillas antes y después de la llamada de escritura.


Y ahora la magia parche de mono: reemplace horast.Unparser con nuestro UnparserWithType.


Cómo funciona ahora: tenemos un árbol de sintaxis, tiene alguna función, las funciones tienen argumentos, los argumentos tienen anotaciones de tipo, una aguja está oculta en la anotación de tipo y la muerte de Koshcheev está oculta en ella. Anteriormente, no había ningún nodo de anotación, los creamos, y cualquier nodo es una instancia de NewType. Llamamos a la función no analizada para nuestro árbol, y para cada nodo llama despacho, que clasifica ese nodo y llama a su función correspondiente. Tan pronto como la función de despacho recibe el nodo del argumento, escribe el nombre del argumento, luego mira para ver si hay una anotación (solía ser None, pero colocamos NewType allí), si es así, escribe dos puntos y llama al despacho para la anotación, que llama a nuestro _NewType, que solo escribe la cadena que almacena; este es el nombre del tipo. Como resultado, obtenemos el argumento escrito: tipo.


En realidad, esto no es del todo legal. Desde el punto de vista del compilador, escribimos las anotaciones de los argumentos con algunas palabras que no están definidas en ningún lado, por lo que cuando unparse termina su trabajo, obtenemos el código incorrecto: necesitamos importaciones. Simplemente formo una línea con el formato correcto y la agrego al comienzo del archivo, y luego agrego el resultado para que se analice, aunque podría agregar importaciones como nodos al árbol de sintaxis, ya que ast admite los nodos Importar e Importar de.


Resolver el problema de las comillas triples no es más difícil que agregar un nuevo tipo. Crearemos la clase StrType y el método _StrType. El método no es diferente del método _NewType utilizado para anotar tipos, pero la definición de la clase ha cambiado: almacenaremos no solo la cadena en sí, sino también el nivel de tabulación en el que debería escribirse. El número de sangría se define de la siguiente manera: si esta línea se encuentra en una función, entonces una, si es en un método, luego dos, y no hay casos en que la función se defina en el cuerpo de otra función y se decore al mismo tiempo, en nuestro proyecto.


 class StrType(object): def __init__ (self, s, indent): self.s = s self.indent = indent def __repr__ (self): return '"""\n' + self.s + '\n' + ' ' * 4 * self.indent + '"""\n' 

En repr definimos cómo debería ser nuestra línea. Creo que esto está lejos de ser la única solución, pero funciona. Uno podría experimentar con astunparse.fill y astunparse.Unparser.indent, entonces sería más universal, pero esta idea ya se me ocurrió al momento de escribir este artículo.


Estas dificultades resueltas terminan. Después de ejecutar mi script, a veces surge el problema de las importaciones cíclicas, pero esto es una cuestión de arquitectura. No encontré una solución de terceros ya preparada, y manejar estos casos dentro del marco de mi script parece ser una complicación seria de la tarea. Probablemente, con la ayuda de ast es posible detectar y resolver las importaciones cíclicas, pero esta idea debe considerarse por separado. En general, la cantidad insignificante de tales incidentes en nuestro proyecto me permitió no procesarlos automáticamente.


Otra dificultad que encontré fue la falta de procesamiento de la expresión en ast desde la importación astronómica, ya que un lector cuidadoso ya sabe que el parche de mono es la cura para todas las enfermedades. Deje que esta sea su tarea para él, pero decidí hacer esto: solo agregue tales importaciones al archivo de mapeo, porque generalmente esta construcción se usa para omitir el conflicto de nombres, y tenemos pocos de ellos.


A pesar de las imperfecciones encontradas, el guión hace lo que estaba destinado a hacer. ¿Cuál es el resultado?


  1. El tiempo de lanzamiento del proyecto se ha reducido de 10 a 3 segundos;
  2. El número de archivos ha disminuido debido a la eliminación de las definiciones new_contract. Los archivos en sí se redujeron: no medí, pero en promedio el git totalizó n líneas agregadas y 2n borradas;
  3. Los IDE inteligentes comenzaron a dar pistas diferentes, porque ahora no son comentarios, sino importaciones honestas;
  4. La legibilidad ha mejorado;
  5. En algún lugar aparecieron los corchetes.

Gracias


Enlaces utiles:


  1. Ast
  2. Horast
  3. Todos los tipos de nodos ast y lo que se almacena en ellos.
  4. Muestra bellamente el árbol de sintaxis
  5. Isort

Source: https://habr.com/ru/post/480930/


All Articles