Skip to content

ci: roda testes e lint em todo PR - #48

Open
bdcdo wants to merge 2 commits into
mainfrom
ci/testes-e-lint
Open

bdcdo wants to merge 2 commits into
mainfrom
ci/testes-e-lint

Conversation

@bdcdo

@bdcdo bdcdo commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

O que o CI roda

O workflow novo .github/workflows/ci.yml dispara em todo pull_request e em push para a main. Ele tem dois jobs.

O job testes roda em Python 3.11, piso do requires-python, e em Python 3.14, a versão mais nova em que todas as dependências, inclusive as dos extras dev e browser, têm wheel. A 3.15 ficou fora porque o pyyaml, que o pre-commit puxa, ainda não publica wheel para ela. O job cria a .venv com uv, instala -e ".[dev,browser]" e chama pytest sem argumentos, de modo que vale o addopts do pyproject.toml: os testes de integração ficam de fora e a suíte reprova abaixo do fail_under = 80.

O job lint roda pre-commit run --all-files --show-diff-on-failure em Python 3.11, a mesma versão do pythonVersion do mypy e do pyright. A .venv fica na raiz do checkout porque os hooks locais chamam .venv/bin/mypy e .venv/bin/pyright, e o pyright resolve os imports pelo venv = ".venv" do pyproject.toml. O extra browser instala o pacote playwright para o pyright achar os imports dos scrapers de navegador; nenhum navegador é baixado. O cache do ~/.cache/pre-commit usa como chave o sistema, a versão do Python e o hash do .pre-commit-config.yaml.

O workflow declara permissions: contents: read, faz o checkout com persist-credentials: false, não usa segredo e fixa as três actions (actions/checkout v7.0.1, astral-sh/setup-uv v10.3.0, actions/cache v6.1.0) por SHA completo, com a versão em comentário. Escolhi a v10.3.0 do setup-uv e não a v10.3.1 porque esta saiu no mesmo dia deste PR e só corrige um aviso de cache. A rede fica nos passos de instalação: o job de lint monta os ambientes dos hooks com pre-commit install-hooks antes de rodá-los, e PYRIGHT_PYTHON_IGNORE_WARNINGS=1 desliga a consulta ao PyPI que o pacote pyright faz a cada execução para avisar de versão nova. O concurrency agrupa por workflow e ref e cancela o run anterior só em PR; na main cada push termina o seu run.

O que mudou além do workflow

No checkout do runner, o índice coincide com o HEAD e não há merge em curso. Nesse estado, dois hooks do pre-commit-hooks passavam sem examinar nada: o check-added-large-files mede só os arquivos adicionados no índice (git diff --staged --diff-filter=A), e o check-merge-conflict retorna 0 quando não acha MERGE_HEAD nem rebase em curso. Um PR com um arquivo de 600 KB ou com marcadores de conflito commitados passava no lint. O .pre-commit-config.yaml passa agora --enforce-all ao primeiro e --assume-in-merge ao segundo, e os dois examinam todos os arquivos que recebem. A árvore atual passa nos dois: o maior arquivo versionado tem 299 KB e nenhum tem marcador de conflito.

No commit local, os dois argumentos também valem: um arquivo modificado acima de 500 KB passa a ser barrado, e não só um arquivo novo, e uma linha formada por exatamente sete sinais de igual, como o sublinhado de um título em Markdown, reprova em qualquer commit, e não só durante um merge.

O teste novo tests/test_pre_commit_config.py reprova a configuração quando um desses dois hooks perde o argumento, ou quando entra o forbid-new-submodules, que lê só o índice e não tem argumento equivalente (o forbid-submodules serve no lugar). O pyyaml, que o teste importa, passa a constar no extra dev; antes chegava só como dependência do pre-commit.

Fora isso, nada mudou: na base a suíte já passava com 200 testes e 84,47% de cobertura e os outros hooks já passavam, então o PR não toca código do pacote. O CHANGELOG.md fica como está, porque a mudança é interna.

Como foi validado

  • actionlint 1.7.12, com shellcheck ativo, sem achado. zizmor 1.31.1 em modo offline, sem achado.
  • Suíte em Python 3.11 e 3.14 na base: 200 passaram nas duas, 84,47% de cobertura.
  • pre-commit run --all-files com o cache do pre-commit vazio, para reproduzir a primeira execução no CI: os 11 hooks passam.
  • Os hooks e a suíte rodados de novo dentro de um namespace sem rede (unshare -rn), depois de instalados: passam, o que mostra que os passos que rodam hooks e testes não dependem de rede.
  • O passo de lint reproduzido no estado do runner (clone novo, HEAD destacado, índice igual ao HEAD, sem merge), com o comando do passo, pre-commit run --all-files --show-diff-on-failure, antes e depois da correção dos dois hooks:
Caso Antes (e548e9f) Depois (e58055b)
Árvore do commit, sem nada plantado exit 0 exit 0
Binário de 600 KB commitado exit 0, os 11 hooks Passed exit 1, check-added-large-files acusa tests/grande.bin (600 KB) exceeds 500 KB
Marcadores de conflito commitados no CHANGELOG.md exit 0, os 11 hooks Passed exit 1, check-merge-conflict acusa as três linhas
  • O teste da configuração reprova nos dois casos sem os argumentos e passa com eles. Cada mutação da configuração derruba o caso correspondente: tirar --enforce-all, tirar --assume-in-merge, acrescentar forbid-new-submodules e tirar o check-merge-conflict.
  • Suíte no ramo: 203 passaram em Python 3.11 e 3.14, 84,47% de cobertura.
  • Run do GitHub: os três jobs passaram em e548e9f e em e58055b.

Quanto falta para a tipagem e o lint ficarem estritos

Medi sem aplicar nada. O mypy e o pyright rodaram com mypy 2.4.0 e pyright 1.1.414, da .venv do extra dev.

mypy. mypy --strict src com a configuração do projeto dá 92 erros em 16 dos 21 arquivos: 69 no-untyped-def, 21 no-untyped-call e 2 type-arg. Sem os relaxamentos do pyproject.toml (o ignore_missing_imports global e o override dos três módulos de navegador) e com warn_unreachable, o strict puro dá 143. Os 51 a mais são 34 erros que o override esconde em playwright_scraper.py (23), datalegis.py (7) e saudelegis.py (4), sendo 26 attr-defined, 5 unreachable, 2 no-any-return e 1 union-attr, e 17 import-untyped: 14 do pandas, 2 do tqdm e 1 do playwright_stealth. Com pandas-stubs e types-tqdm instalados, o strict puro cai para 128, e a configuração atual do projeto passa a acusar um único erro novo, uma inferência de tipo do dicionário de agregação em src/raspe/utils.py:122. Dos códigos opcionais do mypy, nove têm zero ocorrência na configuração atual (ignore-without-code, redundant-expr, truthy-bool, truthy-iterable, redundant-self, unused-awaitable, unimported-reveal, exhaustive-match, deprecated), possibly-undefined tem 1, mutable-override tem 6 e explicit-override tem 71. Cinco módulos já passam no strict puro: exceptions.py, html_scraper.py, scrapers/__init__.py, scrapers/ans.py e scrapers/anvisa.py.

O possibly-undefined aponta um defeito real: em BaseScraper._request_with_retry, com max_retries = 0 o laço não roda e o return r levanta UnboundLocalError. Reproduzi sem rede.

pyright. No modo basic do projeto, o pyright dá 0 erros e 24 warnings (23 reportOptionalMemberAccess e 1 reportAttributeAccessIssue), porque o pyproject.toml rebaixa três regras para warning. Com uma configuração temporária fora do repositório, sem esses rebaixamentos, o modo standard dá 31 erros e o modo strict dá 570. Desses, 499 vêm de parâmetro sem anotação ou tipo desconhecido (220 reportUnknownMemberType, 115 reportUnknownVariableType, 58 reportUnknownArgumentType, 55 reportMissingParameterType, 50 reportUnknownParameterType e 1 reportUnknownLambdaType), 15 de stub ausente (14 do pandas e 1 do playwright_stealth), 23 de acesso a atributo opcional, 6 de reportIncompatibleVariableOverride, 6 de isinstance desnecessário em camara.py, 2 de reportMissingTypeArgument e 19 de outras regras. Treze desses 19 vêm de _import_playwright(), que devolve um dict com valores de tipos diferentes: o pyright infere a união e acusa, por exemplo, que Stealth não aceita navigator_languages_override, o que é falso positivo (conferi a assinatura no playwright-stealth 2.0.3). Com os stubs do pandas e do tqdm, o strict cai para 557. Os mesmos cinco módulos do mypy já passam no strict do pyright.

pylint. Das 42 regras desligadas em [tool.pylint.messages_control], estas são de correção e não de estilo, com as ocorrências medidas pelo pylint 4.1.3 da .venv:

Regra Ocorrências Observação
import-error 0 27 no ambiente isolado do hook (pylint 3.0.0), que não tem pandas, bs4, requests, tqdm nem urllib3; é por isso que está desligada
no-member 0 também 0 no ambiente isolado do hook
broad-except (hoje broad-exception-caught) 31 except Exception que pode engolir erro
unused-argument 2 ScraperIpea.__init__ aceita download_path e o ignora; o **kwargs de camara._prep_scrape é exigência do override
attribute-defined-outside-init 0
redefined-outer-name 0
reimported 0
f-string-without-interpolation 0
differing-param-doc e missing-param-doc 5 e 6 docstrings dos atalhos de raspe/__init__.py que documentam parâmetros que a assinatura não tem, ou omitem o **kwargs

As demais são de estilo, documentação ou desenho (missing-*-docstring, too-many-*, invalid-name, logging-fstring-interpolation, duplicate-code, import-outside-toplevel, que é intencional para os extras opcionais, e outras) e ficam a critério do projeto.

Fora do pylint, o flake8-bugbear, que o projeto não usa, acusa 7 itens em src: 5 B030, os except pw['PlaywrightTimeout'] do mesmo padrão de dict, e 2 B042 em RateLimitError e APIError; pickle e copy.copy dessas exceções funcionam hoje, conferi. Nos testes, o F401 ignorado esconde 1 import sem uso (re em tests/test_utils.py).

Proposta de catraca

Cada passo é um PR próprio que só aperta a régua, com o CI deste PR como portão.

  1. O que custa zero. Ligar no mypy os nove códigos opcionais sem ocorrência e religar no pylint no-member, attribute-defined-outside-init, redefined-outer-name, reimported e f-string-without-interpolation, todas com 0 ocorrência tanto no pylint da .venv quanto no do hook.
  2. pylint com as dependências do projeto. Trocar o hook remoto pycqa/pylint v3.0.0 por hook local .venv/bin/pylint, no mesmo padrão do mypy e do pyright, e religar import-error. No mesmo PR, pre-commit autoupdate dos hooks remotos; o pre-commit-hooks v4.4.0 já emite aviso de nome de stage obsoleto.
  3. Stubs. Pôr pandas-stubs e types-tqdm no extra dev, trocar o ignore_missing_imports global por um override só para playwright_stealth e anotar o dicionário de src/raspe/utils.py:122.
  4. Navegador tipado. Trocar o dict de _import_playwright() por um retorno tipado e estreitar _page, _browser e afins com um acesso que levanta erro claro quando o navegador não foi iniciado. Isso resolve os 23 acessos opcionais, os 13 erros do pyright derivados do dict e os 5 B030, permite apagar o override do mypy dos três módulos e devolver as três regras rebaixadas do pyright para erro. O pyright passa a standard.
  5. Defeitos pequenos. Corrigir o r possivelmente indefinido de _request_with_retry, o download_path ignorado da IPEA, a redeclaração de _type e _api_method com Literal mais estreito em capes.py, ipea.py e presidencia.py (6 erros em cada verificador) e as docstrings de raspe/__init__.py. Depois, ligar possibly-undefined e mutable-override no mypy e religar unused-argument, differing-param-doc e missing-param-doc no pylint.
  6. Anotação por módulo. Começar o strict = true por override do mypy e a lista strict = [...] do pyright pelos cinco módulos já limpos, e acrescentar módulos a cada PR. O grosso está em __init__.py, playwright_scraper.py, base_scraper.py, camara.py e capes.py. No fim, strict = true global no mypy e typeCheckingMode = "strict" no pyright.
  7. Exceções largas. Revisar os 31 except Exception, estreitar o que dá e manter o resto com desligamento local comentado; então religar a regra.
  8. Opcional. flake8-bugbear (depois do passo 4 sobram os 2 B042) e explicit-override (71 ocorrências; em Python 3.11 o @override exige typing_extensions declarado como dependência).

bdcdo added 2 commits October 21, 2026 21:54
Workflow novo em .github/workflows/ci.yml, disparado em pull_request e em
push para a main. O job de testes roda a suíte em Python 3.11 e 3.14 com o
addopts do projeto, inclusive o gate de cobertura. O job de lint roda
pre-commit run --all-files com a .venv na raiz, onde os hooks locais de
mypy e pyright a procuram.

Actions fixadas por SHA, permissão só de leitura e nenhum segredo.
…arquivos

No checkout do CI o índice coincide com o HEAD e não há merge em curso.
Nesse estado, check-added-large-files só mede arquivos adicionados no
índice e check-merge-conflict retorna 0 sem olhar o conteúdo, então os
dois apareciam como Passed com um arquivo de 600 KB ou com marcadores de
conflito commitados.

--enforce-all e --assume-in-merge fazem os dois examinarem os arquivos
recebidos. A árvore atual passa, porque o maior arquivo versionado tem
299 KB e nenhum tem marcador de conflito.

tests/test_pre_commit_config.py reprova a configuração quando um hook
que decide pelo estado do git entra sem o argumento que o obriga a olhar
os arquivos, ou quando entra um hook que não tem esse argumento. O pyyaml,
que o teste importa, passa a constar no extra dev, em vez de chegar só
como dependência do pre-commit.
@bdcdo

bdcdo commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

Evidência

Caso concreto na base (c02aad4)

git ls-tree -r --name-only c02aad4 -- .github | wc -l
0

.venv/bin/pre-commit run --all-files
trim trailing whitespace....Passed   (os 11 hooks: Passed, exit 0)

.venv/bin/pytest
200 passed; Required test coverage of 80.0% reached. Total coverage: 84.47%

Na base não há workflow, então nenhum PR roda suíte nem hooks no GitHub. Localmente, suíte e hooks já passam, e por isso este PR não precisa mexer em código para o CI ficar verde.

O mesmo caso no ramo (e58055b)

actionlint -verbose .github/workflows/ci.yml        # 1.7.12, shellcheck ativo
verbose: Found total 0 errors in 4 ms for .github/workflows/ci.yml   (exit 0)

uvx zizmor --offline .github/workflows/ci.yml       # 1.31.1
No findings to report.   (exit 0)

PRE_COMMIT_HOME=<vazio> pre-commit install-hooks && pre-commit run --all-files --show-diff-on-failure
os 11 hooks: Passed, check yaml inclui o ci.yml   (exit 0)

unshare -rn pre-commit run --all-files      # sem rede, hooks já instalados
os 11 hooks: Passed   (exit 0)

unshare -rn .venv/bin/pytest                # sem rede
203 passed; Total coverage: 84.47%   (exit 0)

O ci.yml é o mesmo de e548e9f, então actionlint e zizmor valem para os dois commits. A instalação com PRE_COMMIT_HOME vazio foi medida localmente em e548e9f; em e58055b o run do GitHub fez o mesmo caminho, porque a chave do cache inclui o hash do .pre-commit-config.yaml ("Cache not found for input keys: pre-commit-Linux-py3.11-cd78f6d6...").

Hooks que não examinavam nada no CI (achado da revisão, corrigido em e58055b). Passo de lint reproduzido num clone novo com HEAD destacado, índice igual ao HEAD e sem merge:

bash -e -c ".venv/bin/pre-commit run --all-files --show-diff-on-failure"
e548e9f + binário de 600 KB commitado:        11 Passed (exit 0)
e548e9f + marcadores de conflito commitados:  11 Passed (exit 0)
e58055b + binário de 600 KB commitado:        check for added large files...Failed
                                              tests/grande.bin (600 KB) exceeds 500 KB.   (exit 1)
e58055b + marcadores de conflito commitados:  check for merge conflicts...Failed
                                              CHANGELOG.md:91: Merge conflict string '<<<<<<<' found   (exit 1)

pytest tests/test_pre_commit_config.py --no-cov
config de e548e9f: 2 failed, 1 passed (exit 1)
config de e58055b: 3 passed (exit 0)
Caso Esperado Obtido
Workflow válido actionlint sem erro 0 erros
Higiene de segurança zizmor sem achado; actions por SHA; contents: read; sem segredo sem achado; 5 uses: (3 actions distintas) por SHA de 40 caracteres com versão em comentário
Suíte no piso de Python 3.11 passa com o gate de 80% 200 passaram, 84,47%
Suíte na versão mais nova com wheel 3.14 passa 200 passaram, 84,47% (3.14.7)
3.15 fora da matriz alguma dependência sem wheel uv pip compile --only-binary :all: --python-version 3.15: pyyaml (via pre-commit) sem wheel
Lint da primeira execução no CI hooks montados do zero passam 11 Passed com PRE_COMMIT_HOME vazio; ambientes dos hooks montados em 3.11
Rede só na instalação hooks e suíte rodam sem rede 11 Passed e 200 passaram dentro de unshare -rn
Pyright sem consulta ao PyPI PYRIGHT_PYTHON_IGNORE_WARNINGS=1 desliga get_latest_version() conferido no código do pacote pyright 1.1.414 (_should_warn_version retorna antes da consulta)
Hooks dependentes do git no CI binário de 600 KB e marcador de conflito commitados reprovam o passo de lint antes da correção, exit 0 nos dois; em e58055b, exit 1 em cada um, só no hook alvo; árvore sem nada plantado continua exit 0
Gate da classe tests/test_pre_commit_config.py reprova sem os argumentos 2 failed na config de e548e9f; 3 passed em e58055b; mutantes (sem --enforce-all, sem --assume-in-merge, com forbid-new-submodules, sem check-merge-conflict) reprovam cada um o seu caso
Run do GitHub os 3 jobs verdes verdes em e548e9f (run 39703308936) e em e58055b (run 39706121539, 203 passed nos dois jobs de teste)

Testes. Base (c02aad4): 200 passaram, 0 pulados, cobertura 84,47%. Ramo (e58055b), medido na árvore mesclada com a main atual: 203 passaram, 0 pulados, cobertura 84,47%, em 3.11 (exit 0); em 3.14 também 203 passaram, no run 39706121539 (os 3 novos são o teste da configuração do pre-commit; o ramo não toca código do pacote). Hooks: 11 Passed na base e na árvore mesclada (pre-commit run --all-files, exit 0).

Árvore mesclada contra origin/main em c02aad476e7978e8440303f7552568581529c212, conferido por git fetch antes da medição. Esse SHA é ancestral de e58055b, então o merge não gera commit novo e a árvore mesclada é o próprio head.

Risco: baixo para usuário, porque o PR acrescenta .github/workflows/ci.yml, dois argumentos de hook, um teste e o pyyaml no extra dev, e não toca o pacote. Para quem commita localmente, --assume-in-merge faz uma linha de exatamente sete = reprovar em qualquer commit, e --enforce-all barra também arquivo modificado acima de 500 KB. O risco é de processo: o run pode falhar no GitHub por diferença de ambiente que a reprodução local não pegou (runner sem node no PATH faria o pyright baixar node pelo nodeenv; versão nova de uv ou de hook remoto), e a partir do merge todo PR aberto, inclusive os que já estão abertos, passa a receber o resultado do CI. Como desfazer: reverter o commit de merge, que tira o workflow, os dois argumentos de hook, o teste e o pyyaml do extra dev.

SHA revisado: e58055b

@bdcdo
bdcdo marked this pull request as ready for review October 22, 2026 01:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant