From 747687640ad7f686bb3fd330f876a51a0c368778 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 14 Oct 2026 10:36:48 -0300 Subject: [PATCH] =?UTF-8?q?Sa=C3=ADda=20polars:=20p=C3=A2nico=20do=20polar?= =?UTF-8?q?s=20na=20convers=C3=A3o=20final=20sai=20como=20coluna=20Object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Com entrada polars, um campo Decimal com valor acima de 38 dígitos derrubava a saída depois de todas as chamadas: o pyarrow entrega o valor em 256 bits, o polars entra em pânico ao lê-lo, e o PanicException, que herda de BaseException, passava pelo except Exception de from_pandas. O except passa a capturar também pl.exceptions.PanicException, e a coluna sai Object com aviso, como as demais que o polars recusa. BaseException continua fora, para que o KeyboardInterrupt suba. Closes #230 --- CHANGELOG.md | 1 + src/dataframeit/utils.py | 7 +++++-- tests/test_entrada_e_saida.py | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9beeb43..5cfb272d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR ### Corrigido +- Com entrada polars, um campo `Decimal` com valor acima de 38 dígitos deixa de derrubar a saída depois de todas as chamadas com o `PanicException` do polars, que não é `Exception`. A coluna sai `Object`, com aviso, como as demais que o polars recusa (#230). - Com entrada polars, a saída deixa de falhar depois de todas as chamadas quando um campo do modelo guarda valores que o polars não reúne numa coluna tipada, como `Union[int, str]` com número numa linha e texto em outra, `tuple[int, str]`, `Any` com tipos diferentes, lista de `Enum`, inteiro acima de 2**53 ao lado de float, inteiro acima do `Int64` e `datetime` com fuso de deslocamento, como `+05:30`. A coluna sai com o tipo `Object`, com os objetos Python que a entrada pandas devolveria, e um aviso a nomeia. O campo `Enum` sai pelo valor do membro, inclusive na coluna `Object` (#218). - A coluna `_trace` do agente único fica com as demais colunas de trace na ordem da saída, depois dos campos do modelo. Antes, numa entrada que já a trazia, os campos novos saíam depois dela. - Com entrada polars, as colunas que a execução não escreve saem do DataFrame de entrada, com o tipo e os valores de lá. Antes, a saída inteira era convertida de volta do pandas: um `Int64` com nulo voltava `Float64`, o que arredondava ids acima de 2**53, um `Date` voltava `Datetime`, e `Enum`, `Decimal`, `Array`, `Null` e o inteiro dentro de `Struct` e `List` também trocavam de tipo; uma coluna `Object` também trocava de tipo ou, com objetos que o Arrow não infere, levantava `ArrowInvalid` depois das chamadas (#195). diff --git a/src/dataframeit/utils.py b/src/dataframeit/utils.py index 71362aaa..5dc0875a 100644 --- a/src/dataframeit/utils.py +++ b/src/dataframeit/utils.py @@ -255,10 +255,13 @@ def from_pandas( # As chamadas ao provedor já foram feitas, e a coluna que o polars ou o # pyarrow recusam, seja qual for a exceção (ValueError, TypeError, # OverflowError, ComputeError), sai Object em vez de derrubar a saída. - # O pânico do polars não é Exception e passa por aqui. + # O pânico do polars, como o do Decimal acima de 38 dígitos, que o + # pyarrow entrega em 256 bits, herda de BaseException e é capturado + # pelo nome; capturar BaseException engoliria o KeyboardInterrupt. O + # polars ainda escreve a mensagem do pânico no stderr. try: columns.append(_to_polars(values, model_columns.get(col))) - except Exception: # noqa: BLE001 (a recusa muda de tipo com os valores) + except (Exception, pl.exceptions.PanicException): # noqa: BLE001 (a recusa muda de tipo com os valores) # Há versões do polars em que o construtor de Series inspeciona os # valores mesmo com dtype=pl.Object: quando o primeiro valor não nulo # é membro de Enum, troca cada valor pelo `.value`, e levanta erro no diff --git a/tests/test_entrada_e_saida.py b/tests/test_entrada_e_saida.py index f16c2f72..17bd3a7e 100644 --- a/tests/test_entrada_e_saida.py +++ b/tests/test_entrada_e_saida.py @@ -932,6 +932,9 @@ def test_retomada_polars_de_campo_bool_gravado_como_inteiro(modelo): datetime(2024, 1, 3, tzinfo=_FUSO_DE_DESLOCAMENTO), ], ), + # O pyarrow põe o Decimal acima de 38 dígitos numa coluna de 256 bits, que o + # polars não lê: a conversão entra em pânico, e o pânico não é Exception. + "decimal_acima_de_38_digitos": (Decimal, [Decimal("1E+40"), Decimal(2)]), } @@ -1085,6 +1088,17 @@ def test_campo_que_o_arrow_nao_tipa_com_entrada_polars_series(): assert resultado["campo"].to_list() == [1, "dois"] +def test_interrupcao_na_conversao_da_saida_polars_sobe_em_vez_de_virar_object(): + """A saída captura a recusa da conversão e o pânico do polars, e só eles.""" + pl = pytest.importorskip("polars") + + with ( + patch("dataframeit.utils._to_polars", side_effect=KeyboardInterrupt), + pytest.raises(KeyboardInterrupt), + ): + _rodar(pl.DataFrame({"texto": ["a", "b"]})) + + def test_saida_polars_com_coluna_object_e_retomada(): pl = pytest.importorskip("polars") modelo = create_model("ModeloDoCaso", campo=(Union[int, str], ...))