Christian Ledermann: Migrar de mypy para ty e pyrefly
Eu queria migrar um dos meus pacotes Python de mypy para ty e pyrefly. Entreguei essa tarefa a Claude e, no final, pedi-lhe que escrevesse algumas orientações sobre como executá-la com mais eficiência. Então, o que se segue é um “resíduo” gerado pela IA.
Este guia não é sobre o uso do fastkml. Ele documenta como o próprio fastkml foi migrado do mypy para o ty do Astral e o pyrefly do Meta, para que o mesmo manual possa ser reproduzido em outras bases de código com menos tentativa e erro. Mantenha-o aqui porque a próxima migração (conduzida por humanos ou agentes) deve começar a partir de descobertas, e não do zero.
Executar duas damas em vez de uma é deliberado, não acidental. ty e pyrefly discordam entre si e com mypy com frequência suficiente para que executar apenas um dê uma falsa sensação de completude. Faça um orçamento para ambos e espere que eles detectem diferentes subconjuntos dos mesmos bugs.
Antes de excluir [tool.mypy], leia o que cada sinalizador realmente comprou para você, porque essa é a barra de rigidez que ty/pyrefly precisa corresponder ou exceder: também inventariar substituições obsoletas por módulo - uma configuração mypy que foi editada ao longo dos anos acumula entradas mortas (um caminho de módulo que foi renomeado ou excluído, mas a substituição sobreviveu). Grep para os caminhos referenciados; não leve a configuração morta adiante. Categorize imediatamente, não leia linha por linha ainda: O segundo comando (erros por arquivo) geralmente revela a causa sistêmica imediatamente: um punhado de arquivos concentra uma parcela desproporcional dos erros, e geralmente são eles que tocam em uma dependência opcional/do tipo pato.
Se já existir uma migração parcial no repositório (o CI mudou, mas o pyproject.toml cresceu amplamente ignore-missing-imports = ["*"] / Blanket Missing-attribute = "ignore" sub-configs), trate isso como uma bandeira vermelha, não como um ponto de partida. Supressões amplas acumuladas durante uma migração em andamento geralmente significam que alguém atingiu o atrito e o silenciou, em vez de corrigi-lo. Execute novamente com essas supressões removidas para ver a linha de base real antes de decidir o que vale a pena manter.
O movimento de maior alavancagem nesse tipo de migração quase nunca é “corrigir erros arquivo por arquivo”. É encontrar a única incompatibilidade arquitetônica na qual ambos os verificadores estão tropeçando de forma idêntica em dezenas de locais de chamada. O padrão recorrente: um projeto suporta um back-end opcional e mais rico (lxml sobre xml.etree.ElementTree, orjson sobre json, ujson, um mecanismo regex acelerado por C, etc.) por meio de uma importação try/except em tempo de execução e define um protocolo ou apenas depende da digitação de pato estrutural para abstrair ambos.
mypy tolerou isso por anos via ignore_missing_imports = true, que trata silenciosamente o back-end não digitado como Any em todos os lugares. Nem ty nem pyrefly degradam isso normalmente por padrão - eles resolverão parcialmente as informações reais (mas incompletas) do back-end não digitado ou voltarão a digitá-lo em qualquer ramo do try/except que tenha stubs completos (geralmente o substituto stdlib) e, em seguida, relate todos os métodos/kwarg que o back-end mais rico oferece exclusivamente como inválidos. A correção, aplicada no site de importação (não espalhada por todos os sites de chamada): se o projeto também definir seu próprio protocolo para abstrair ambos os back-ends (por exemplo, types.py: class Element(Protocol): ...), considere dar um passo adiante e fazer com que esse protocolo literalmente apelide o tipo real do back-end mais rico em TYPE_CHECKING, voltando ao protocolo estrutural apenas para fins de tempo de execução/sem verificação de tipo: esta alteração reduziu cerca de 150 de ~ 240 diagnósticos em a migração fastkml, porque corrigiu a classe de erros "kwarg específico de back-end não existe" e a classe "Protocolo estrutural não pode ser atribuído a um tipo de parâmetro stdlib concreto" de uma só vez (veja a armadilha abaixo).
Se existir um pacote stub para o backend mais rico (lxml-stubs, types-ujson, etc.), adicione-o às suas dependências de desenvolvimento de digitação - mas leia a seção de armadilhas antes de assumir que é uma melhoria estrita para ambos os verificadores. Nem ty nem pyrefly analisam a supressão de código de erro entre colchetes do mypy da mesma forma que o mypy. Verifique empiricamente antes de confiar em qualquer uma dessas coisas - o comportamento pode mudar entre as versões da ferramenta: execute ty check e pyrefly check em uma reprodução de duas linhas antes de decidir sobre uma estratégia de supressão para toda a base de código.
Regra prática que funcionou bem: mantenha o # type: ignore[code] comentário original (documenta a intenção, mantém o pyrefly feliz) e anexe # ty: ignore[rule-name] na mesma linha física para ty. Não retire os comentários da era mypy de uma vez; eles são documentação gratuita de por que uma linha é excepcional. Este é o erro mais demorado de se cometer.
pyrefly check --replace-imports-with-any 'lxml.*' funciona na CLI. Escrever o equivalente TOML "óbvio": ... não gera um erro na verificação do pyrefly em alguns caminhos de código, mas falha gravemente com o pyrefly dump-config (variante desconhecida 'replace-imports-with-any'...
Erro de configuração fatal) e, dependendo da ordem de invocação, isso também pode interromper a verificação do pyrefly posteriormente. A chave TOML correta usa sublinhados: Enquanto isso, nomes de tipo de erro (usados como chaves de ditado em [tool.pyrefly.errors] ou dentro de uma tabela regras = {...}) usam hifens (missing-override-decorator, redundant-cast, etc.) - correspondendo à ortografia do nome da regra --error/--ignore, não à ortografia do campo de configuração. Não existe uma convenção única e consistente de case em toda a superfície de configuração; verifique o pyrefly dump-config após cada alteração de configuração, não apenas a verificação do pyrefly, porque a verificação pode parecer limpa enquanto uma chave próxima é ignorada silenciosamente.
Após qualquer edição de configuração do pyrefly, execute pyrefly dump-config (validação de esquema/análise) e verificação pyrefly (validação comportamental) - um detecta erros estruturais e o outro não aparece. As substituições por caminho do Pyrefly usam a sintaxe de matriz de tabelas do TOML: TOML permite que outras tabelas de nível superior não relacionadas apareçam entre [[tool.pyrefly.sub-config]] e seu [tool.pyrefly.sub-config.errors] emparelhado - a tabela aninhada ainda se liga ao elemento de matriz aberto mais recentemente, independentemente do que está intercalado. Isso significa que um pyproject.toml que cresceu organicamente (ferramentas de migração automática anexando blocos próximos ao que quer que esteja no final do arquivo) pode acabar com três blocos de subconfiguração espalhados por mais de 100 linhas de configuração de projeto/ferramenta não relacionadas, e ainda será analisado.
Torna-se uma mina terrestre no momento em que alguém (ou um agente) exclui um cabeçalho [[tool.pyrefly.sub-config]] sem também excluir seu bloco [tool.pyrefly.sub-config.errors] agora órfão - a tabela de erros órfãs então se liga ao elemento de matriz errado ou interrompe totalmente a análise. Correção: mantenha cada bloco de configuração pyrefly (e ty) contíguo em um local no arquivo, mesmo que isso signifique movê-lo para longe de onde uma ferramenta automatizada o inseriu pela primeira vez. Analise novamente após cada edição: se sua abstração de digitação de pato for um protocolo (digamos, types.Element) e o código interno passar valores digitados por elemento para funções que são digitadas na classe stdlib/de terceiros concreta (xml.etree.ElementTree.SubElement(parent: Element[Any], ...)), ty e pyrefly irão rejeitá-lo - mesmo que o protocolo seja estruturalmente compatível em todos os sites de chamada.
Protocolo → atribuibilidade de classe concreta não funciona da mesma maneira que concreto → Protocolo funciona, e um atributo mutável/invariante (texto: str no Protocolo vs. texto: str | Nenhum na classe real) torna tudo pior. Isso se resolve gratuitamente quando você aplica a correção da Fase 2 (também conhecida como Protocolo ao tipo de back-end real em TYPE_CHECKING) para código interno voltado para back-end.
Mantenha o protocolo original apenas para a superfície da API genuinamente pública e independente de back-end da base de código. Um padrão comum: uma tabela de despacho genérica armazena classes: tupla[tipo[objeto], ...] e um protocolo correspondente exige que cada retorno de chamada registrado aceite exatamente aquela assinatura (necessariamente ampla), mesmo que qualquer retorno de chamada individual receba apenas uma classe concreta em tempo de execução. Você não pode restringir o tipo de parâmetro de um retorno de chamada individual à classe concreta que ele realmente espera (tuple[type[SpecificClass], ...]) - isso quebra a atribuibilidade estrutural em relação ao protocolo mais amplo que o despachante exige (pa
💬 Comentários
Carregando comentários…