diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4ee4b013..69332808 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -33,7 +33,7 @@ jobs: python-version: '3.13' - name: Setup uv - uses: astral-sh/setup-uv@v10.2.0 + uses: astral-sh/setup-uv@v10.3.0 with: enable-cache: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 89478015..4bbfd525 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: python-version: '3.13' - name: Setup uv - uses: astral-sh/setup-uv@v10.2.0 + uses: astral-sh/setup-uv@v10.3.0 with: enable-cache: true @@ -60,7 +60,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@v10.2.0 + uses: astral-sh/setup-uv@v10.3.0 with: enable-cache: true @@ -118,7 +118,7 @@ jobs: python-version: '3.11' - name: Setup uv - uses: astral-sh/setup-uv@v10.2.0 + uses: astral-sh/setup-uv@v10.3.0 with: enable-cache: true @@ -150,7 +150,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@v10.2.0 + uses: astral-sh/setup-uv@v10.3.0 with: enable-cache: true @@ -184,7 +184,7 @@ jobs: python-version: '3.13' - name: Setup uv - uses: astral-sh/setup-uv@v10.2.0 + uses: astral-sh/setup-uv@v10.3.0 with: enable-cache: true @@ -210,7 +210,7 @@ jobs: python-version: '3.13' - name: Setup uv - uses: astral-sh/setup-uv@v10.2.0 + uses: astral-sh/setup-uv@v10.3.0 with: enable-cache: true @@ -232,7 +232,7 @@ jobs: python-version: '3.13' - name: Setup uv - uses: astral-sh/setup-uv@v10.2.0 + uses: astral-sh/setup-uv@v10.3.0 with: enable-cache: true diff --git a/CHANGELOG.md b/CHANGELOG.md index aa4c28b0..ee92f42d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR ### Corrigido +- A criação das colunas de saída usa a opção `mode.chained_assignment` só no pandas 2, preservando entradas fatiadas sem emitir aviso de depreciação no pandas 3.1 (#269). + - No provider `codex`, a thread daemon que pede o `thread/unsubscribe` da linha abortada pela morte do app-server deixa de ficar presa para sempre. O processo morto ainda aceita a escrita enquanto termina, e o SDK registra a espera da resposta depois de o leitor do stdout ter acordado os pedidos em voo; agora o fechamento do cliente acorda essa espera (#250). - `read_df` com argumento de leitura que faz o pandas devolver outra coisa que um DataFrame, como `chunksize`, `iterator`, `sheet_name=None` ou lista e o `chunksize` do JSON em linhas, devolve o `TextFileReader`, o `JsonReader` ou o dict de abas como o pandas o devolve, sem normalizar, com ou sem modelo. Antes, sem modelo, ou com modelo que tem campo de lista, dict ou modelo aninhado, e com `normalize=True`, o padrão, a função levantava `AttributeError` ao procurar as colunas no objeto do leitor (#249). - Com entrada polars, a saída deixa de trocar sem aviso o valor que o modelo devolveu quando o pyarrow tipa a coluna pelo primeiro valor e converte os demais. Sai `Object`, com o aviso das colunas que o polars não reúne, o campo cuja anotação junta `timedelta` a outro tipo, `float` a `bool` ou `bytes` a `str`, e a coluna cujos valores trazem essas combinações ou as de data e hora, também em `Any`, onde o escalar do numpy, como o `np.int64`, conta como o número do Python, nos itens de lista e no campo de modelo aninhado, além da coluna cujos dicts do mesmo campo têm chaves diferentes e da que traz `datetime` com e sem fuso, `time` com fuso ou `Decimal` NaN. O NaN de campo `float` sai NaN. Antes, em `Union[timedelta, int]` o 5 virava cinco microssegundos, em `Union[float, bool]` o `True` virava `1.0`, o `datetime` sem fuso ganhava o fuso do primeiro, o `time` perdia o fuso, o `Decimal` NaN e o NaN de campo `float` viravam nulo, `{}` ao lado de `{"a": 1}` virava `{"a": None}`, e em `Union[NewType sobre date, int]` o 5 virava `1970-01-06`. Com `resume=True`, a coluna do campo cuja anotação é `int`, um `Literal` com inteiro ou uma união com um deles é lida com os inteiros do polars, e o inteiro acima de 2**53 deixa de voltar arredondado em `Float64`. O `Decimal` NaN e o NaN de campo `float` de uma linha processada deixam de ser acusados como incompatíveis na retomada, também com entrada pandas, e o reprocessamento campo a campo os repassa às condições como valor; na coluna float64, como a do checkpoint relido, o NaN não se distingue do nulo e segue contando como ausente. O `NewType` e o apelido `type`, inclusive o apelido genérico usado com argumentos, como `Opcional[int]` de `type Opcional[T] = T | None`, passam a contar pelo tipo a que se referem na saída polars, na retomada e em `read_df`: o `NewType` sobre `dict` sai em texto JSON, como o campo `dict`, e é retomado, e o campo `NewType` sobre `str` ou sobre `int` é relido do CSV como texto cru ou como inteiro exato (#241). diff --git a/src/dataframeit/core.py b/src/dataframeit/core.py index 09019e12..5a9e243f 100644 --- a/src/dataframeit/core.py +++ b/src/dataframeit/core.py @@ -15,7 +15,7 @@ import time import warnings from concurrent.futures import ThreadPoolExecutor, as_completed -from contextlib import contextmanager +from contextlib import contextmanager, nullcontext from dataclasses import dataclass from decimal import Decimal from enum import Enum @@ -1722,8 +1722,14 @@ def _setup_columns( # noqa: C901, PLR0912, PLR0913 (uma família de colunas por ): return - # Criar colunas - with pd.option_context("mode.chained_assignment", None): + # O pandas 2 ainda avisa ao escrever numa fatia; no pandas 3, a cópia na + # escrita substitui esse aviso e a opção que o controla não se aplica. + context = ( + pd.option_context("mode.chained_assignment", None) + if hasattr(pd.errors, "SettingWithCopyWarning") + else nullcontext() + ) + with context: for col in new_cols: df[col] = None if needs_status: diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 8568bfd6..91919ee1 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -1,17 +1,24 @@ +from contextlib import nullcontext + import pandas as pd +import pytest from dataframeit import llm as llm_module from dataframeit.core import _setup_columns -def test_setup_columns_mutates_independent_copy_only(): +@pytest.mark.filterwarnings("error") +@pytest.mark.parametrize("copy_input", [False, True]) +def test_setup_columns_mutates_slice_without_warnings(copy_input): df = pd.DataFrame( { "texto": ["a", "b", "c"], "x": [1, 2, 3], } ) - df_copy = df.iloc[:2].copy() + df_copy = df.iloc[:2] + if copy_input: + df_copy = df_copy.copy() _setup_columns( df_copy, @@ -52,3 +59,24 @@ def test_build_prompt_preserves_other_placeholders(): assert "TEXTO" in formatted assert "{texto}" not in formatted assert "{outro}" in formatted # Preserva outros placeholders + + +@pytest.mark.parametrize("legacy_warning", [False, True]) +def test_setup_columns_uses_option_only_with_legacy_warning(monkeypatch, legacy_warning): + calls = [] + + def option_context(*args): + calls.append(args) + return nullcontext() + + monkeypatch.setattr(pd, "option_context", option_context) + if legacy_warning: + monkeypatch.setattr(pd.errors, "SettingWithCopyWarning", UserWarning, raising=False) + else: + monkeypatch.delattr(pd.errors, "SettingWithCopyWarning", raising=False) + + df = pd.DataFrame({"texto": ["a"]}) + _setup_columns(df, ["resposta"], None, track_tokens=False) + + assert calls == ([("mode.chained_assignment", None)] if legacy_warning else []) + assert df["resposta"].isna().all()