1 puntos por GN⁺ 2024-08-23 | 1 comentarios | Compartir por WhatsApp
  • 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 import desde un archivo .pth de configuración de rutas, y agregar una función de búsqueda con codecs.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 solo SyntaxError: encoding problem: your_codec en 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++ mediante cppyy e 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-fstrings y future-annotations; magic_codec ayuda 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 :
  • 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]+
  • 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 paquete site se carga durante la inicialización
  • Los archivos .pth de configuración de rutas dentro de site-packages agregan 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 import hay un espacio o tabulación
    • Ej.: si en packagename.pth se pone import packagename.register_codec, ese módulo se importa durante la inicialización
  • El módulo importado puede llamar a codecs.register para registrar una función de búsqueda de codecs
    • Como el import se ejecuta solo una vez, la función de búsqueda también se registra una sola vez

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 decode puede hacer la decodificación UTF-8 real con codecs.utf_8_decode y 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_codec en 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
  • 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.BufferedIncrementalDecoder y procesa solo cuando final es verdadero en decode(self, data, final=False)
  • 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, --x sí son válidos sintácticamente, pero se convierten respectivamente en llamadas a x.__pos__().__pos__() y x.__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 en x++ - (-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 braces produce SyntaxError: 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 readline de io.StringIO
      • eliminar los tokens INDENT y DEDENT existentes
      • al encontrar {, aumentar el nivel de indentación y emitir :
      • al encontrar }, disminuir el nivel de indentación
      • después de NL, agregar un token INDENT correspondiente al nivel actual de indentación
    • 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
      • cppyy usa cling internamente
    • El resultado del preprocesamiento termina siendo, aproximadamente, el siguiente código Python
      • import cppyy
      • cppyy.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

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
    • tomllib para leer el archivo TOML
    • json para 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 arreglo scores no 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
  • Si no quieres tocar directamente site-packages ni escribir a mano archivos .pth y código repetitivo, puedes usar magic_codec
  • Las extensiones de magic_codec pueden crearse como paquetes Python con el prefijo magic_
    • Si el codec del archivo se establece como magic_foo, se carga el paquete magic_foo
    • Luego se verifica si ese paquete tiene una función preprocess
  • La firma esperada de preprocess es la siguiente
    • def preprocess(data: str) -> str:
  • Hay ejemplos de extensiones en example/

1 comentarios

 
GN⁺ 2024-08-23
Opiniones de Hacker News
  • Al ejecutar from __future__ import braces, el divertido mensaje de error SyntaxError: not a chance estaba hardcodeado en CPython desde 2001
    https://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.

    • Me recuerda a cuando break rust; provocaba un error interno del compilador en el compilador de Rust. Me pregunto cuántos easter eggs parecidos habrá en otros lenguajes.
    • No veo por qué eso sorprende. En 2001 no era que cualquiera pudiera meter algo en Python; en ese entonces era un proyecto de nicho, y quienes contribuían probablemente eran personas inteligentes y dedicadas, con altas chances de terminar teniendo carreras influyentes.
      Creo que es un malentendido pensar que el hacking lúdico e informal de hobby es un mundo aparte del desarrollo profesional real.
    • Eran tiempos inocentes. Hylton incluso podría sumarse al movimiento de voto de censura en defensa de Tim Peters.
      https://news.ycombinator.com/item?id=41314393
    • Siempre es genial ver este tipo de easter eggs. Da lástima que ya no sean tan comunes como antes.
  • 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 y sys.settrace para monkey-patchear todas las funciones con la función llamada justo antes y cambiar stdout y stderr cada 17 minutos.

    • Como buen lenguaje, también debería obligar a usar llaves.
  • 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.

    • Python tiene la filosofía de ser un lenguaje para “adultos que consienten” (consenting adults), por eso no tiene especificadores de visibilidad public/private y 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 :)
    • Si impides que la gente haga esto de una forma fácil y obvia, lo intentará de formas peores y más hacky.
  • 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ía exec y luego metía un exit(), pero con un preprocesador sería mucho más usable.
    Antes de que todos los dict garantizaran el orden, lo usaba sobre todo para convertir literales de lista en llamadas a ordered dict mediante reescritura de ast, 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 mmap para escribir un script que se modificaba a sí mismo. Ahora siento que debería escribir un intérprete de Lisp como producer.

    • “Modificar cadenas en el lugar”, pero las cadenas son inmutables. ¿Escribiste directamente en la ubicación de memoria con algo como 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: pyxl
    print 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.python2 adaptara código de Python 2 para que fuera código válido de Python 3, o que # coding: six.python3 transformara código de Python 3 para que corriera en Python 2.
    También parece que habría sido posible agregar o quitar prefijos b"..." o u"...".

    • Podría haber ayudado, pero la parte en la que ayudaría era la parte fácil. Lo difícil de pasar de Python 2 a 3 fueron los cambios de comportamiento en runtime. En Python 2, Unicode que contenía ASCII y las cadenas normales se comportaban como la misma cadena, así que si se usaban como claves de dict apuntaban al mismo elemento; en Python 3, bytes y str con el mismo contenido ASCII apuntan a elementos distintos dentro del mismo dict.
      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 de six u 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 str versus bytes.
  • ¿Las dependencias generadas por esta estrategia de coding hook las detectan pip freeze o uv?
    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.