El preprocesador de Python
(pydong.org)- Python puede usar las dos primeras líneas de comentarios mágicos de codificación de origen y un codec personalizado para cambiar el contenido de un archivo antes de ejecutarlo o incluso reemplazarlo por código completamente distinto
- Un codec personalizado puede registrarse durante la inicialización del intérprete mediante la ejecución de
importdesde un archivo.pthde configuración de rutas, y agregar una función de búsqueda concodecs.register - La implementación del codec requiere
decode(data: bytes) -> tuple[str, int]y un decodificador incremental; si no se manejan las excepciones, puede aparecer soloSyntaxError: encoding problem: your_codecen vez de la causa real - Con el mismo punto de entrada se pueden implementar operadores de incremento/decremento
++/--, Python basado en llaves, ejecución de C/C++ mediantecppyye incluso validación de TOML con JSON Schema - Más allá de ejemplos juguetones, también puede usarse en extensiones y backports de Python como
pythonql,future-typing,future-fstringsyfuture-annotations;magic_codecayuda a reducir trabajo repetitivo
Usar la codificación de origen como punto de entrada del preprocesamiento
- Según PEP-0263, se puede indicar la codificación del código fuente en una de las dos primeras líneas de un archivo Python
- Ej.:
# coding=utf8,# -*- coding: utf8 -*-,# vim: set fileencoding=utf8 :
- Ej.:
- La línea mágica debe coincidir con la expresión regular
^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)- El nombre del codec debe coincidir con
[-_.a-zA-Z0-9]+
- El nombre del codec debe coincidir con
- Un codec personalizado no solo puede decodificar el código fuente, sino también modificar la cadena resultante antes de entregarla al intérprete de Python
Registrar un codec con un archivo .pth
- Si el intérprete de Python se inicia sin la opción
-S, el paquetesitese carga durante la inicialización - Los archivos
.pthde configuración de rutas dentro desite-packagesagregan su contenido a la ruta de búsqueda de módulos, excepto líneas vacías y líneas que empiezan con# - Según la documentación de Python, se ejecutan las líneas donde después de
importhay un espacio o tabulación- Ej.: si en
packagename.pthse poneimport packagename.register_codec, ese módulo se importa durante la inicialización
- Ej.: si en
- El módulo importado puede llamar a
codecs.registerpara registrar una función de búsqueda de codecs- Como el
importse ejecuta solo una vez, la función de búsqueda también se registra una sola vez
- Como el
Implementación de un codec personalizado
- Un codec personalizado necesita dos cosas
decode(data: bytes) -> tuple[str, int]- una clase de decodificador incremental
- La función
decodepuede hacer la decodificación UTF-8 real concodecs.utf_8_decodey luego pasar la cadena resultante a una función de preprocesamiento - Si una excepción interna del codec no se captura, puede mostrarse solo
SyntaxError: encoding problem: your_codecen lugar del traceback normal- Conviene imprimir directamente la excepción ocurrida en la función de preprocesamiento con
traceback.print_exc()y luego volver a lanzarla
- Conviene imprimir directamente la excepción ocurrida en la función de preprocesamiento con
- El decodificador incremental puede acumular todo el archivo en un buffer y preprocesarlo una sola vez en la llamada final a
decode- La implementación de ejemplo hereda de
codecs.BufferedIncrementalDecodery procesa solo cuandofinales verdadero endecode(self, data, final=False)
- La implementación de ejemplo hereda de
- El resultado del preprocesamiento no tiene por qué usar el contenido original del archivo; incluso puede devolver código Python completamente arbitrario
- Eso sí, la primera línea se elimina porque se espera que sea la línea mágica, y el resultado debe ser Python válido
Ejemplos de extensiones de sintaxis en Python
-
Operadores de incremento y decremento
++y--- Python no tiene operadores unarios de incremento/decremento
x++,x--no son sintácticamente válidos++x,--xsí son válidos sintácticamente, pero se convierten respectivamente en llamadas ax.__pos__().__pos__()yx.__neg__().__neg__()- Un preprocesador puede modificar el flujo de tokens para que se comporten como operadores de incremento/decremento
x++→(x, x := x + 1)[0]x--→(x, x := x - 1)[0]++x→(x, x := x + 1)[1]--x→(x, x := x - 1)[1]
- Esta transformación usa la expresión de asignación de Python, el operador walrus
- Un reemplazo simple de tokens puede fallar en expresiones como
x++ - -y; usar paréntesis como enx++ - (-y)puede reducir la ambigüedad - incdec.py hace el reemplazo con expresiones regulares, pero puede seguir siendo frágil incluso si intenta evitar sustituciones dentro de literales de cadena
- Hay una implementación que modifica directamente el flujo de tokens en magic.incdec
-
Python basado en llaves
from __future__ import bracesproduceSyntaxError: not a chance- Un preprocesador puede modificar el flujo de tokens para convertir bloques con llaves en Python basado en indentación
- El flujo de implementación es el siguiente
- generar tokens con
tokenize.generate_tokens - proporcionar la cadena de entrada línea por línea con
readlinedeio.StringIO - eliminar los tokens
INDENTyDEDENTexistentes - al encontrar
{, aumentar el nivel de indentación y emitir: - al encontrar
}, disminuir el nivel de indentación - después de
NL, agregar un tokenINDENTcorrespondiente al nivel actual de indentación
- generar tokens con
- Para reducir conflictos con los literales de diccionario de Python, se puede ajustar el nivel de indentación solo cuando después de
{haya un salto de línea, y tratar}como cierre de bloque solo cuando antes haya un salto de línea - Los diccionarios multilínea pueden usarse con barra invertida para que no se generen tokens de salto de línea dentro de las llaves
Ejecutar otros lenguajes como Python
-
C y C++
- En lenguajes que usan
#para comentarios, como shell scripts, scripts de CMake, PHP o Ruby, es fácil incluir una línea mágica de codificación junto con el shebang - C y C++ usan comentarios
/* ... */o// ..., pero como las directivas del preprocesador empiezan con#, pueden ajustarse a la expresión regular de codificación - La línea mágica de ejemplo es válida en código C/C++ y también coincide con el patrón de codificación de Python
#define CODEC "coding:magic.cpp"
- Con cppyy, Python puede interpretar código C/C++ y generar bindings
cppyyusaclinginternamente
- El resultado del preprocesamiento termina siendo, aproximadamente, el siguiente código Python
import cppyycppyy.cppdef("<input source file content>")from cppyy.gbl import main- llamar a
main()de C/C++ cuando__name__ == "__main__"
- La implementación de ejemplo está en magic.cpp
- En lenguajes que usan
Usarlo como herramienta de validación de TOML
- TOML inicia los comentarios con
#, así que se puede incluir una línea mágica de codificación como# coding: magic.toml - Si el resultado del preprocesamiento se convierte en un script de validación en Python, el intérprete de Python puede usarse como una herramienta de validación de TOML
- El ejemplo de validación usa los siguientes módulos
tomllibpara leer el archivo TOMLjsonpara leer el archivo JSON Schema- jsonschema para validar
- Ejecución:
python tests/toml/data_valid.toml -s tests/toml/schema.json- Si es válido, imprime
Successfully validated.
- En el ejemplo de TOML inválido, se muestra un error de validación porque la cadena
'20'dentro del arregloscoresno es un número - La implementación de ejemplo está en magic.toml
Uso real y magic_codec
- Al combinar codecs personalizados con archivos
.pth, se puede cambiar de forma importante el comportamiento del intérprete de Python - La mayoría de los ejemplos son por diversión, pero también hay casos de uso reales
- pythonql: extensión de lenguaje de consultas para Python
- future-typing: backport de generic type hints y sintaxis de unión con
|para Python 3.6+ - future-fstrings
- future-annotations
- Si no quieres tocar directamente
site-packagesni escribir a mano archivos.pthy código repetitivo, puedes usar magic_codec - Las extensiones de
magic_codecpueden crearse como paquetes Python con el prefijomagic_- Si el codec del archivo se establece como
magic_foo, se carga el paquetemagic_foo - Luego se verifica si ese paquete tiene una función
preprocess
- Si el codec del archivo se establece como
- La firma esperada de
preprocesses la siguientedef preprocess(data: str) -> str:
- Hay ejemplos de extensiones en example/
1 comentarios
Opiniones de Hacker News
Al ejecutar
from __future__ import braces, el divertido mensaje de errorSyntaxError: not a chanceestaba hardcodeado en CPython desde 2001https://github.com/python/cpython/commit/ad3d3f2f3f19833f59f...
Jeremy Hylton, su autor, ahora trabaja en Google como Principal Engineer a cargo de la calidad de búsqueda con IA, y es bastante impresionante cómo en 24 años su carrera pasó de conmemorar en broma una sintaxis prohibida a trabajar en un sistema universal de consultas que no necesita sintaxis dedicada.
break rust;provocaba un error interno del compilador en el compilador de Rust. Me pregunto cuántos easter eggs parecidos habrá en otros lenguajes.Creo que es un malentendido pensar que el hacking lúdico e informal de hobby es un mundo aparte del desarrollo profesional real.
https://news.ycombinator.com/item?id=41314393
Pensaba que jugar con import hooks era la forma más creativa de que te despidieran, pero era una idea ingenua. Es una lástima que por la regex de codecs probablemente no se pueda trolear bien con cosas como
μtf8; ahora no quedará más que usar import hooks, un preprocesador ysys.settracepara monkey-patchear todas las funciones con la función llamada justo antes y cambiar stdout y stderr cada 17 minutos.Creo que hay buenas razones por las que Python no expone deliberadamente hooks de preprocesador, y es algo de lo que cualquier adulto razonable debería mantenerse lejos.
Pero, por otro lado, no quiero relacionarme con adultos razonables. Parece que se podrían hacer muchísimas cosas realmente divertidas.
public/privatey probablemente deja abierta toda clase de magia de metaprogramación. Si les preocuparan los “adultos razonables”, esas decisiones de diseño serían un poco raras :)Esto parece cómodo y realmente útil. Cuando hago hacks absurdos de import, normalmente importaba el módulo, reescribía el código con el módulo
ast, hacíaexecy luego metía unexit(), pero con un preprocesador sería mucho más usable.Antes de que todos los
dictgarantizaran el orden, lo usaba sobre todo para convertir literales de lista en llamadas a ordered dict mediante reescritura deast, y de hecho era útil.Me gusta la flexibilidad de Python. Lo más maldito que hice fue modificar cadenas en el lugar, y al final terminé abusando incluso de
mmappara escribir un script que se modificaba a sí mismo. Ahora siento que debería escribir un intérprete de Lisp como producer.ctypes?El mejor uso que encontré hasta ahora es pyxl, inspirado en JSX: https://github.com/dropbox/pyxl
Permite escribir código como este:
# coding: pyxlprint Hello World!Me pregunto si esto podría haberse usado para manejar mejor la transición de Python 2 a 3. Por ejemplo, que
# coding: six.python2adaptara código de Python 2 para que fuera código válido de Python 3, o que# coding: six.python3transformara código de Python 3 para que corriera en Python 2.También parece que habría sido posible agregar o quitar prefijos
b"..."ou"...".dictapuntaban al mismo elemento; en Python 3,bytesystrcon el mismo contenido ASCII apuntan a elementos distintos dentro del mismodict.Hay cambios todavía más complicados. Varias funcionalidades integradas como
.keys()y.values()devuelven listas en Python 2, pero en Python 3 devuelven iteradores. Si intentas traducir el código de forma segura con utilidades desixu otros workarounds, el código se vuelve muy verboso, porque la mayoría se usa una sola vez, pero a veces se usa dos veces.Si existe una herramienta capaz de reescribir el código al momento de importarlo, creo que sería mejor commitear el código convertido e ir limpiándolo gradualmente. La parte difícil son los cambios de comportamiento que afectan código distante, como
strversusbytes.¿Las dependencias generadas por esta estrategia de coding hook las detectan
pip freezeouv?Si no, se vienen tiempos divertidos :). Si alguien metió algo así, está casi garantizado que habrá otras trampas, así que probablemente sea más fácil reescribir la librería que luchar contra esos dragones.
Sería bastante gracioso crear un pseudocódigo Python y hacer que un LLM lo decodifique. Obviamente sería horrible, pero divertido seguro.