Skip to content

Campo deque conta como estrutura no checkpoint e volta como deque na retomada - #308

Open
bdcdo wants to merge 3 commits into
mainfrom
fix/deque-como-estrutura
Open

bdcdo wants to merge 3 commits into
mainfrom
fix/deque-como-estrutura

Conversation

@bdcdo

@bdcdo bdcdo commented Oct 29, 2026

Copy link
Copy Markdown
Owner

Problema

Um campo deque[str] não entrava em get_complex_fields, porque _STRUCTURE_CLASSES listava classes concretas e o deque não é subclasse de nenhuma delas. O model_dump devolve o próprio deque, e o checkpoint não o gravava em JSON: o CSV e o XLSX gravavam o repr, e a retomada recusava a linha concluída pedindo reprocess_columns=['obs']; o parquet não conseguia gravar a coluna (ArrowInvalid: Could not convert deque(['a', 'z'])), e a retomada pagava de novo as linhas concluídas.

O que muda

  • is_complex_type reconhece a estrutura por issubclass contra as ABCs Sequence, Set e Mapping, menos str, bytes e bytearray (inclusive o StrEnum), além do modelo Pydantic. Isso cobre o que já era reconhecido (lista, tupla, NamedTuple, conjunto, dict, TypedDict, as origens abstratas e as mutáveis) e acrescenta o deque, de collections ou typing.Deque. A lista de classes concretas e a busca por identidade das origens abstratas saem.
  • _json_default grava a sequência que não é lista nem tupla, como o deque, como lista. Isso vale também para o deque dentro de outra estrutura, como dict[str, deque[str]], e para a comparação da retomada (_same_json), que reconhece o valor relido como o mesmo e devolve o deque.
  • _structures_as_json usa o mesmo critério para decidir o que vai a JSON no checkpoint.
  • A documentação do checkpoint e de get_complex_fields inclui o deque.

As outras coleções que o pydantic aceita

Medido com a função compara da issue, sobre csv, xlsx e parquet (Python 3.11.14, pydantic 2.14.2, pandas 3.1.0, pyarrow 26.0.0):

Anotação Na main No ramo Decisão
collections.deque[str], typing.Deque[str] CSV e XLSX recusam a linha; parquet chama ['a', 'b'] volta deque, chama só ['b'] nos três corrigido
dict[str, deque[str]] recusado nos três volta com o deque dentro corrigido
Counter[str], typing.Counter[str] já volta Counter, chama só ['b'] igual já funcionava, por ser subclasse de dict; teste novo fixa
OrderedDict[str, int], typing.OrderedDict já volta OrderedDict igual idem
defaultdict[str, int], typing.DefaultDict o model_dump devolve dict, que volta dict igual idem
MutableSequence[str], Sequence[str] já volta lista igual já coberto pelo #265
Collection[str], Iterator[str], ChainMap, UserList o pydantic recusa o modelo (PydanticSchemaGenerationError) igual fora: o modelo não chega a rodar
Iterable[str], Generator[str, None, None] ver abaixo igual fora deste PR

O pydantic aceita Iterable e Generator, mas valida de forma preguiçosa: o model_dump devolve um SerializationIterator, e a saída direta, sem checkpoint, já traz esse objeto na célula, e não a lista respondida. O defeito está na execução, antes do checkpoint, e tratar esses campos como estrutura não o resolve. Ficam fora deste PR e merecem issue própria, que decida entre recusar a anotação antes de processar ou materializar o valor em lista.

Closes #287

…retomada

O reconhecimento de estrutura passa a ser por issubclass contra as ABCs
Sequence, Set e Mapping, menos texto e bytes, o que inclui o deque de
collections e o typing.Deque. O checkpoint o grava em JSON, também dentro
de outra estrutura, e a retomada o devolve como deque nos três formatos.

Closes #287
@bdcdo

bdcdo commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

Evidência

Script: a função compara da issue, estendida a uma lista de anotações de coleção (mede.py, fora do repositório), rodado com uv run --extra dev --extra all --extra search-all --extra polars --extra excel --extra codex python mede.py, sem chamada a provider.

Na base (origin/main, ec33265)

collections.deque[str]       model_dump={'obs': deque(['a', 'z'])}                                   complexo=False
collections.deque[str]       csv      direta=[deque(['a', 'z']), deque(['a', 'z'])]  retomada: ValueError: O DataFrame contém linhas processadas incompatíveis com o modelo atual: campos incompatíveis ['obs']. Inclua-o
collections.deque[str]       xlsx     direta=[deque(['a', 'z']), deque(['a', 'z'])]  retomada: ValueError: O DataFrame contém linhas processadas incompatíveis com o modelo atual: campos incompatíveis ['obs']. Inclua-o
collections.deque[str]       parquet  direta=[deque(['a', 'z']), deque(['a', 'z'])]  retomada=[deque(['a', 'z']), deque(['a', 'z'])]  tipos=deque->deque  iguais=True  chamadas na retomada=['a', 'b']

No ramo (2a05419)

collections.deque[str]       model_dump={'obs': deque(['a', 'z'])}                                   complexo=True
collections.deque[str]       csv      direta=[deque(['a', 'z']), deque(['a', 'z'])]  retomada=[deque(['a', 'z']), deque(['a', 'z'])]  tipos=deque->deque  iguais=True  chamadas na retomada=['b']
collections.deque[str]       xlsx     direta=[deque(['a', 'z']), deque(['a', 'z'])]  retomada=[deque(['a', 'z']), deque(['a', 'z'])]  tipos=deque->deque  iguais=True  chamadas na retomada=['b']
collections.deque[str]       parquet  direta=[deque(['a', 'z']), deque(['a', 'z'])]  retomada=[deque(['a', 'z']), deque(['a', 'z'])]  tipos=deque->deque  iguais=True  chamadas na retomada=['b']

Casos cobertos

Caso Esperado Obtido no ramo
deque[str] em csv, xlsx, parquet volta deque, retomada chama só ['b'] igual, nos três
typing.Deque[str] em csv, xlsx, parquet idem igual, nos três
dict[str, deque[str]] em csv, xlsx, parquet o deque de dentro volta deque igual (na base, recusado nos três)
Counter, OrderedDict, defaultdict em csv, xlsx, parquet tipo e valor gravados, só ['b'] igual (já na base)
is_complex_type de deque, Deque, Counter, OrderedDict, defaultdict, DefaultDict True True
is_complex_type de bytes, bytearray, StrEnum, Literal False False

Mutação: tirar de _json_default o ramo que converte a sequência em lista deixa vermelhos os 9 casos de deque do teste de retomada (3 modelos x 3 formatos), e os 30 outros passam.

Testes

Passados Pulados Falhas
base 2192 0 0
ramo 2219 0 0

Os 27 novos: 18 da retomada (6 modelos x 3 formatos) e 9 de is_complex_type. Cobertura de ramo local de 99,96% na base e no ramo, com as mesmas linhas 723-724 de codex.py descobertas, efeito do extra codex instalado; as linhas tocadas estão cobertas. ruff format --check, ruff check, ty check, vulture e deptry src passam.

Risco: médio, porque o critério de estrutura passa a ser por ABC e vale para toda coluna de objeto que o CSV e o XLSX gravam; desfaz-se com o revert do squash.

SHA revisado: 2a05419

@bdcdo

bdcdo commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

Rodada 2, pela revisão independente do head 2a05419e.

Achado importante corrigido. Com uma coluna do usuário de collections.UserString, sozinha ou dentro de um dict, o checkpoint CSV e o XLSX deixavam de ser gravados. A UserString é Sequence e não é subclasse de str, então o critério novo a tratava como estrutura, e o list(value) de _json_default devolvia outras UserString, numa recursão sem fim (RecursionError, transformado em aviso de falha de gravação a cada lote). A retomada pagava de novo todas as linhas. Na base, o mesmo caso gravava o texto.

  • collections.UserString entra em _TEXT_CLASSES, ao lado de str, bytes e bytearray.
  • Em _json_default, o tolist vem antes do ramo de sequência. A memoryview de duas dimensões aninhada numa coluna voltava a levantar NotImplementedError no list(), e o tolist a monta como lista aninhada, como a base fazia.

Testes novos, todos reprovam no head 2a05419e e passam neste:

Teste Caso
test_coluna_do_usuario_com_userstring_vai_ao_checkpoint UserString e {"k": UserString} em coluna do usuário, CSV e XLSX: o checkpoint é gravado sem aviso de falha, e a retomada chama só a linha pendente
test_is_complex_type_texto_e_bytes_nao_sao_estrutura[UserString] UserString não é estrutura
test_json_default_grava_a_userstring_como_texto json.dumps([UserString("ola")]) dá ["ola"]
test_json_default_monta_a_lista_aninhada_da_memoryview_de_duas_dimensoes memoryview 2D vira [[0, 0], [0, 0]]

Com o src/ do head anterior, os 7 casos novos reprovam.

Execução neste head, antes do merge da main: 2226 passados, 0 pulados, 0 falhas. A única linha descoberta é codex.py 723-724, o except ImportError que só roda sem o extra codex. ruff format --check, ruff check, ty check, vulture e deptry src passam. Depois disso, o branch recebeu a main atual, com o conflito do CHANGELOG resolvido mantendo as duas seções.

Nits da revisão que ficaram como estão. is_complex_type marca como estrutura tipos que só chegam com arbitrary_types_allowed, como UserDict, ChainMap e MappingProxyType, que o JSON grava pelo repr. Os casos de Counter, OrderedDict e defaultdict fixam o critério, e não a correção.

Risco: médio. Para desfazer, basta o revert do squash.

SHA revisado: 2a05419e73531dc690503729e430a1cb3dc1f23c (revisão independente); correção do achado em 93d7ff0a, provada pelos testes acima.

This branch has not been deployed

No deployments
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.

Campo deque não é tratado como estrutura: a retomada do CSV e do XLSX recusa a linha, e a do parquet paga de novo as linhas concluídas

1 participant