1 pontos por GN⁺ 2024-08-23 | 1 comentários | Compartilhar no WhatsApp
  • O Python pode usar as duas primeiras linhas de comentários mágicos de codificação da fonte e um codec personalizado para alterar o conteúdo de um arquivo antes da execução ou até substituí-lo por um código completamente diferente
  • Um codec personalizado pode ser registrado durante a inicialização do interpretador por meio da execução de import em um arquivo .pth de configuração de caminho, adicionando uma função de busca com codecs.register
  • A implementação do codec exige decode(data: bytes) -> tuple[str, int] e um decodificador incremental; se as exceções não forem tratadas, pode aparecer apenas SyntaxError: encoding problem: your_codec em vez da causa real
  • Operadores de incremento/decremento ++/--, Python baseado em chaves, execução de C/C++ via cppyy e até validação de TOML com JSON Schema podem ser implementados pelo mesmo ponto de entrada
  • Além de exemplos brincalhões, isso também pode ser usado em extensões e backports de Python como pythonql, future-typing, future-fstrings e future-annotations, enquanto magic_codec reduz trabalho repetitivo

Usando a codificação da fonte como ponto de entrada do pré-processamento

  • De acordo com a PEP-0263, é possível definir a codificação do código-fonte em uma das duas primeiras linhas de um arquivo Python
    • Exemplos: # coding=utf8, # -*- coding: utf8 -*-, # vim: set fileencoding=utf8 :
  • A linha mágica precisa corresponder à regex ^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)
    • O nome do codec precisa corresponder a [-_.a-zA-Z0-9]+
  • Um codec personalizado não precisa apenas decodificar o código-fonte: ele também pode modificar a string de origem antes de entregá-la ao interpretador Python

Registrando o codec com arquivo .pth

  • Quando o interpretador Python inicia sem a opção -S, o pacote site é carregado durante a inicialização
  • Os arquivos .pth de configuração de caminho dentro de site-packages adicionam seu conteúdo ao caminho de busca de módulos, exceto linhas em branco e linhas que começam com #
  • Segundo a documentação do Python, linhas em que import é seguido por espaço ou tabulação são executadas
    • Exemplo: colocar import packagename.register_codec em packagename.pth faz esse módulo ser importado durante a inicialização
  • O módulo importado pode chamar codecs.register para registrar uma função de busca de codec
    • Como o import é executado só uma vez, a função de busca também é registrada só uma vez

Implementando um codec personalizado

  • Um codec personalizado precisa de duas coisas
    • decode(data: bytes) -> tuple[str, int]
    • uma classe de decodificador incremental
  • A função decode pode usar codecs.utf_8_decode para fazer a decodificação UTF-8 real e então passar a string resultante para a função de pré-processamento
  • Se exceções internas do codec não forem capturadas, pode ser exibido apenas SyntaxError: encoding problem: your_codec em vez de um traceback normal
    • É melhor imprimir diretamente a exceção gerada na função de pré-processamento com traceback.print_exc() e depois relançá-la
  • O decodificador incremental pode acumular o arquivo inteiro em buffer e executar o pré-processamento só na chamada final de decode
    • A implementação de exemplo herda de codecs.BufferedIncrementalDecoder e processa apenas quando final é verdadeiro em decode(self, data, final=False)
  • O resultado do pré-processamento não precisa usar obrigatoriamente o conteúdo original do arquivo; ele pode retornar código Python totalmente arbitrário
    • Porém, a primeira linha é esperada como linha mágica e por isso é removida, e o resultado precisa ser Python válido

Exemplos de extensão da sintaxe do Python

  • Operadores de incremento e decremento ++ e --

    • O Python não tem operadores unários de incremento/decremento
    • x++ e x-- não são sintaticamente válidos
    • ++x e --x são sintaticamente válidos, mas se tornam chamadas a x.__pos__().__pos__() e x.__neg__().__neg__() respectivamente
    • O pré-processador pode modificar o fluxo de tokens para fazê-los funcionar 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]
    • Essa transformação usa o operador walrus do Python, isto é, a expressão de atribuição
    • Uma simples substituição de tokens pode falhar em expressões como x++ - -y; usar parênteses como em x++ - (-y) pode reduzir a ambiguidade
    • incdec.py faz a substituição com regex, mas pode continuar frágil mesmo tentando evitar substituições dentro de literais de string
    • Há uma implementação que modifica diretamente o fluxo de tokens em magic.incdec
  • Python baseado em chaves

    • from __future__ import braces gera SyntaxError: not a chance
    • O pré-processador pode modificar o fluxo de tokens para converter escopo baseado em chaves em Python baseado em indentação
    • O fluxo de implementação é o seguinte
      • gerar tokens com tokenize.generate_tokens
      • fornecer a string de entrada linha por linha com readline de io.StringIO
      • remover os tokens INDENT e DEDENT existentes
      • ao encontrar {, aumentar o nível de indentação e emitir :
      • ao encontrar }, diminuir o nível de indentação
      • depois de NL, adicionar um token INDENT correspondente ao nível atual de indentação
    • Para reduzir conflito com literais de dicionário do Python, é possível ajustar o nível de indentação só quando houver quebra de linha depois de {, e tratar como fechamento de escopo só quando houver quebra de linha antes de }
    • Dicionários multilinha podem funcionar usando barra invertida, pois assim não surgem tokens de newline dentro das chaves

Executando outras linguagens com Python

  • C e C++

    • Linguagens que usam # para comentários, como scripts shell, scripts CMake, PHP e Ruby, permitem inserir facilmente uma linha mágica de codificação junto com shebang
    • Em C e C++, os comentários são /* ... */ ou // ..., mas diretivas de pré-processador começam com #, então podem corresponder à regex de codificação
    • A linha mágica de exemplo é válida também em código C/C++ e ao mesmo tempo corresponde ao padrão de codificação do Python
      • #define CODEC "coding:magic.cpp"
    • Com cppyy, o Python pode interpretar código C/C++ e gerar bindings
      • Internamente, cppyy usa cling
    • O resultado do pré-processamento fica aproximadamente como o código Python abaixo
      • import cppyy
      • cppyy.cppdef("<input source file content>")
      • from cppyy.gbl import main
      • chamar main() de C/C++ quando __name__ == "__main__"
    • A implementação de exemplo está em magic.cpp

Usando como ferramenta de validação de TOML

  • Como TOML inicia comentários com #, é possível inserir uma linha mágica de codificação como # coding: magic.toml
  • Se o resultado do pré-processamento for convertido em um script Python de validação, o interpretador Python pode ser usado como uma ferramenta de validação de TOML
  • O exemplo de validação usa os seguintes módulos
    • tomllib para ler o arquivo TOML
    • json para ler o arquivo JSON Schema
    • jsonschema para validar
  • Exemplo de execução:
    • python tests/toml/data_valid.toml -s tests/toml/schema.json
    • Se for válido, imprime Successfully validated.
  • Em um exemplo de TOML inválido, aparece um erro de validação informando que a string '20' dentro do array scores não é um número
  • A implementação de exemplo está em magic.toml

Uso real e magic_codec

  • Combinar codecs personalizados com arquivos .pth permite alterar bastante o comportamento do interpretador Python
  • A maioria dos exemplos é mais pela diversão, mas também existem casos de uso reais
  • Se você não quiser mexer diretamente em site-packages nem escrever manualmente arquivos .pth e código repetitivo, pode usar magic_codec
  • As extensões de magic_codec podem ser criadas como pacotes Python com prefixo magic_
    • Se o codec do arquivo for definido como magic_foo, o pacote magic_foo será carregado
    • Depois, é verificado se esse pacote tem uma função preprocess
  • A assinatura esperada de preprocess é a seguinte
    • def preprocess(data: str) -> str:
  • Há exemplos de extensão em example/

1 comentários

 
GN⁺ 2024-08-23
Comentários do Hacker News
  • A mensagem de erro engraçada SyntaxError: not a chance, exibida ao executar from __future__ import braces, está hardcoded no CPython desde 2001
    https://github.com/python/cpython/commit/ad3d3f2f3f19833f59f...
    O autor, Jeremy Hylton, hoje trabalha no Google como Principal Engineer responsável pela qualidade da busca com IA, e é bem impressionante ver como, em 24 anos, sua carreira foi de comemorar em tom de brincadeira uma sintaxe proibida até sistemas universais de consulta que não exigem sintaxe dedicada

    • Isso me lembrou de quando break rust; causava um erro interno do compilador no compilador Rust. Fico curioso para saber quantos easter eggs parecidos existem em outras linguagens
    • Não entendo por que isso seria surpreendente. Em 2001, não era como se qualquer um pudesse colocar algo no Python; na época era um projeto de nicho, e quem contribuía era inteligente e dedicado, então provavelmente já tinha boas chances de construir uma carreira influente
      Acho um engano imaginar que esse tipo de hacking brincalhão e informal de hobby seja um mundo separado do desenvolvimento profissional real
    • Eram tempos mais inocentes. Talvez Hylton até entrasse num movimento de voto de desconfiança em defesa da definição de Tim Peters
      https://news.ycombinator.com/item?id=41314393
    • É sempre legal ver esse tipo de easter egg. Uma pena que tenham ficado menos comuns do que antes
  • Eu achava que brincar com import hook era a forma mais criativa de ser demitido, mas isso era ingenuidade minha. É uma pena que, por causa da regex do codec, não dê para trollar direito com algo como μtf8; agora só resta usar import hook, pré-processador e sys.settrace para monkeypatchar todas as funções para a função chamada imediatamente antes e trocar stdout com stderr a cada 17 minutos

    • Como toda boa linguagem, também deveria forçar o uso de chaves
  • Há bons motivos para o Python não expor de propósito os hooks de pré-processador, e acho que isso é algo de que um adulto razoável deveria manter distância
    Mas, por outro lado, eu não quero me envolver com adultos razoáveis. Parece que daria para fazer muita coisa realmente divertida

    • O Python tem a filosofia de ser uma linguagem para “adultos consentidos (consenting adults)”, e por isso não tem especificadores de visibilidade public/private, e aparentemente também deixa aberta toda sorte de magia de metaprogramação. Se realmente estivesse preocupado com “adultos razoáveis”, algumas dessas decisões de design pareceriam meio estranhas :)
    • Se impedirem as pessoas de fazer isso de um jeito fácil e óbvio, elas vão tentar de um jeito pior e mais gambiarra
  • Isso parece conveniente e realmente útil. Quando eu fazia hacks absurdos de import, normalmente importava o módulo, reescrevia o código com o módulo ast, dava exec e então enfiava um exit(), mas com um pré-processador isso seria bem mais agradável de usar
    Antes de todos os dict garantirem ordem, eu usava mais para reescrever AST e trocar literais de lista por chamadas de ordered dict, e isso de fato era útil
    Gosto da flexibilidade do Python. A coisa mais amaldiçoada que já fiz foi modificar strings in-place, e acabei até abusando de mmap para escrever um script que modificava a si próprio. Agora parece que preciso escrever um interpretador Lisp como producer

    • “Modificar strings in-place”? Strings são imutáveis. Você escreveu diretamente na posição de memória com algo como ctypes?
  • O melhor caso de uso que encontrei até agora é o pyxl, inspirado em JSX: https://github.com/dropbox/pyxl
    Dá para escrever código assim
    # coding: pyxl
    print Hello World!

  • Fico pensando se isso poderia ter sido usado para lidar melhor com a transição do Python 2 para o 3. Por exemplo, # coding: six.python2 poderia ajustar código Python 2 para virar código Python 3 válido, ou # coding: six.python3 poderia transformar código Python 3 para rodar no Python 2
    Também talvez desse para adicionar ou remover prefixos como b"..." ou u"..."

    • Poderia ajudar, mas a parte em que ajuda é a parte fácil. A dificuldade da migração do Python 2 para o 3 estava nas mudanças de comportamento em runtime. No Python 2, Unicode contendo ASCII e strings comuns se comportavam como a mesma string, então, se usadas como chave de dict, apontavam para a mesma entrada; já no Python 3, bytes e str com o mesmo conteúdo ASCII apontam para entradas diferentes dentro do mesmo dict
      Houve mudanças ainda mais complicadas. Vários recursos embutidos, como .keys() e .values(), retornam listas no Python 2, mas iteradores no Python 3. Se você tenta traduzir o código de forma segura com utilitários do six ou outros contornos, o código fica muito verboso. Na maior parte das vezes esses valores são usados uma vez só, mas às vezes são usados duas vezes
      Se houvesse uma ferramenta para reescrever o código no momento do import, eu ainda acharia melhor simplesmente dar commit no código convertido e ir limpando aos poucos. A parte difícil são mudanças de comportamento como str versus bytes, que afetam até trechos de código distantes
  • Dependências criadas por essa estratégia de coding hook são detectadas por pip freeze ou uv?
    Se não, parece que vai ser divertido :). Se alguém colocou algo assim, é praticamente garantido que existam outras armadilhas, então provavelmente seria mais fácil reescrever a biblioteca do que lutar contra esse tipo de dragão

  • Seria bem engraçado criar um Python em pseudocódigo e deixar uma LLM decodificar isso. Obviamente seria horrível, mas talvez divertido