Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
fail-fast: true
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
polars-version: ["0.20.0", "1.33.1"]
polars-version: ["1.20.0", "1.33.1","1.42.1"]
defaults:
run:
shell: bash -l {0}
Expand Down Expand Up @@ -337,9 +337,20 @@ jobs:
- xarray
include:
- extra: polars
polars-version: "0.20.0"
python-version: "3.10"
polars-version: "1.20.0"
- extra: polars
python-version: "3.11"
polars-version: "1.20.0"
- extra: polars
python-version: "3.12"
polars-version: "1.33.1"
- extra: polars
python-version: "3.13"
polars-version: "1.33.1"
- extra: polars
python-version: "3.14"
polars-version: "1.42.1"
- extra: polars
pydantic-version: "2.12.3"
exclude:
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ The nox `tests` session maps extras to test directories: `extra=None` runs
- Python: 3.10, 3.11, 3.12, 3.13, 3.14
- Pandas: 2.1.1, 2.3.3
- Pydantic: 1.10.11, 2.12.3
- Polars: 0.20.0, 1.33.1
- Polars: 1.20.0, 1.33.1, 1.42.1

## Code Quality

Expand Down Expand Up @@ -191,7 +191,7 @@ prek run --all-files
| Extra | Key packages |
|--------------|---------------------------------------|
| `pandas` | numpy, pandas >= 2.1.1 |
| `polars` | polars >= 0.20.0 |
| `polars` | polars >= 1.20.0 |
| `pyspark` | pyspark[connect] >= 3.2.0 |
| `ibis` | ibis-framework >= 9.0.0 |
| `dask` | dask[dataframe], distributed |
Expand Down
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ dependencies:
- pyspark[connect] >= 3.2.0

# polars extra
- polars >= 0.20.0
- polars >= 1.20.0

# xarray extra
- xarray >= 2024.10.0
Expand Down
4 changes: 2 additions & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
PYTHON_VERSIONS = ["3.10", "3.11", "3.12", "3.13", "3.14"]
PANDAS_VERSIONS = ["2.3.3", "3.0.0"]
PYDANTIC_VERSIONS = ["1.10.11", "2.12.3"]
POLARS_VERSIONS = ["0.20.0", "1.33.1"]
POLARS_VERSIONS = ["1.20.0", "1.33.1", "1.42.1"]
PACKAGE = "pandera"
SOURCE_PATHS = PACKAGE, "tests", "noxfile.py"
REQUIREMENT_PATH = "requirements.txt"
Expand Down Expand Up @@ -184,7 +184,7 @@ def _testing_requirements(
req = "pyarrow >= 13"
if req == "ibis-framework" or req.startswith("ibis-framework "):
req = "ibis-framework[duckdb] >= 11.0.0"
if req == "polars":
if req == "polars" or req.startswith("polars "):
req = f"polars=={polars}"

# for some reason uv will try to install an old version of dask,
Expand Down
22 changes: 0 additions & 22 deletions pandera/api/polars/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,13 @@
import polars as pl

from pandera.api.polars.types import PolarsCheckObjects
from pandera.backends.polars.utils import polars_version
from pandera.config import (
ValidationDepth,
get_config_context,
get_config_global,
)


def get_lazyframe_schema(lf: pl.LazyFrame) -> dict[str, pl.DataType]:
"""Get a dict of column names and dtypes from a polars LazyFrame."""
if polars_version().release >= (1, 0, 0):
return lf.collect_schema()
return lf.schema


def get_lazyframe_column_dtypes(lf: pl.LazyFrame) -> list[pl.DataType]:
"""Get a list of column dtypes from a polars LazyFrame."""
if polars_version().release >= (1, 0, 0):
return lf.collect_schema().dtypes()
return [*lf.schema.values()]


def get_lazyframe_column_names(lf: pl.LazyFrame) -> list[str]:
"""Get a list of column names from a polars LazyFrame."""
if polars_version().release >= (1, 0, 0):
return lf.collect_schema().names()
return lf.columns


def get_validation_depth(check_obj: PolarsCheckObjects) -> ValidationDepth:
"""Get validation depth for a given polars check object."""
is_dataframe = isinstance(check_obj, pl.DataFrame)
Expand Down
6 changes: 1 addition & 5 deletions pandera/backends/narwhals/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,7 @@ def _build_eager_failure_case(fc, err: SchemaError, check_identifier):

if resolved_co is not None:
co_eager = _materialize(resolved_co)
try:
co_indexed = co_eager.with_row_index("index")
except AttributeError:
# Older polars: ``with_row_index`` was called ``with_row_count``.
co_indexed = co_eager.with_row_count("index")
co_indexed = co_eager.with_row_index("index")
failing_indices = co_indexed.filter(~nw.col(CHECK_OUTPUT_KEY))[
"index"
].to_list()
Expand Down
3 changes: 1 addition & 2 deletions pandera/backends/narwhals/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,7 @@ def run_schema_component_checks(

def collect_column_info(self, check_obj, schema):
"""Collect column metadata for the dataframe."""
# Use collect_schema().names() — lazy-safe Narwhals equivalent of
# get_lazyframe_column_names()

frame_column_names = check_obj.collect_schema().names()

column_names: list[Any] = []
Expand Down
18 changes: 6 additions & 12 deletions pandera/backends/polars/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@

from pandera.api.base.error_handler import ErrorHandler
from pandera.api.polars.types import CheckResult, PolarsFrame
from pandera.api.polars.utils import (
get_lazyframe_column_dtypes,
get_lazyframe_schema,
)
from pandera.backends.base import BaseSchemaBackend, CoreCheckResult
from pandera.constants import CHECK_OUTPUT_KEY
from pandera.errors import (
Expand All @@ -26,9 +22,9 @@ def is_float_dtype(check_obj: pl.LazyFrame, selector):
"""Check if a column/selector is a float."""
return all(
dtype in {pl.Float32, pl.Float64}
for dtype in get_lazyframe_column_dtypes(
check_obj.select(pl.col(selector))
)
for dtype in check_obj.select(pl.col(selector))
.collect_schema()
.dtypes()
)


Expand Down Expand Up @@ -96,7 +92,7 @@ def run_check(
else:
# use check_result
_failure_cases = check_result.failure_cases
if CHECK_OUTPUT_KEY in get_lazyframe_schema(_failure_cases):
if CHECK_OUTPUT_KEY in _failure_cases.collect_schema():
_failure_cases = _failure_cases.drop(CHECK_OUTPUT_KEY)

failure_cases = _failure_cases.collect()
Expand Down Expand Up @@ -167,10 +163,8 @@ def failure_cases_metadata(
failure_cases_df = err.failure_cases

# get row number of the failure cases
if hasattr(err.check_output, "with_row_index"):
_index_lf = err.check_output.with_row_index("index")
else:
_index_lf = err.check_output.with_row_count("index")
assert err.check_output is not None
_index_lf = err.check_output.with_row_index("index")

index = _index_lf.filter(pl.col(CHECK_OUTPUT_KEY).eq(False))[
"index"
Expand Down
4 changes: 3 additions & 1 deletion pandera/backends/polars/builtin_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def in_range(
max_value.
"""
col = pl.col(data.key)

compare_min = col.ge(min_value) if include_min else col.gt(min_value)
compare_max = col.le(max_value) if include_max else col.lt(max_value)

Expand Down Expand Up @@ -296,6 +297,7 @@ def unique_values_eq(data: PolarsData, values: Iterable) -> bool:
:param values: The set of values that must be present. May be any iterable.
"""

# Use to_list(): polars < 1.21.0 can't call `unique` on Decimal columns.
return (
set(data.lazyframe.collect().get_column(data.key).unique()) == values
set(data.lazyframe.collect().get_column(data.key).to_list()) == values
)
8 changes: 2 additions & 6 deletions pandera/backends/polars/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
from pandera.api.base.checks import CheckResult
from pandera.api.checks import Check
from pandera.api.polars.types import PolarsData
from pandera.api.polars.utils import (
get_lazyframe_column_names,
get_lazyframe_schema,
)
from pandera.backends.base import BaseCheckBackend
from pandera.backends.polars.utils import horizontal_concat
from pandera.constants import CHECK_OUTPUT_KEY
Expand Down Expand Up @@ -57,7 +53,7 @@ def apply(self, check_obj: PolarsData):
if isinstance(out, bool):
return out

if len(get_lazyframe_schema(out)) > 1:
if len(out.collect_schema()) > 1:
# for checks that return a boolean dataframe, reduce to a single
# boolean column.
out = out.select(
Expand All @@ -69,7 +65,7 @@ def apply(self, check_obj: PolarsData):
)
else:
out = out.rename(
{get_lazyframe_column_names(out)[0]: CHECK_OUTPUT_KEY}
{out.collect_schema().names()[0]: CHECK_OUTPUT_KEY}
)

return out
Expand Down
14 changes: 4 additions & 10 deletions pandera/backends/polars/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
from pandera.api.base.error_handler import ErrorHandler, get_error_category
from pandera.api.polars.components import Column
from pandera.api.polars.types import PolarsData
from pandera.api.polars.utils import (
get_lazyframe_column_names,
get_lazyframe_schema,
)
from pandera.backends.base import CoreCheckResult
from pandera.backends.polars.base import PolarsSchemaBackend, is_float_dtype
from pandera.backends.polars.utils import horizontal_concat
Expand Down Expand Up @@ -99,7 +95,7 @@ def validate(
"regex pattern so that it matches at least one "
"column."
),
failure_cases=get_lazyframe_column_names(check_obj),
failure_cases=check_obj.collect_schema().names(),
check=f"no_regex_column_match('{schema.selector}')",
reason_code=SchemaErrorReason.INVALID_COLUMN_NAME,
)
Expand Down Expand Up @@ -155,7 +151,7 @@ def validate(
return check_obj

def get_regex_columns(self, schema, check_obj) -> Iterable:
return get_lazyframe_schema(check_obj.select(pl.col(schema.selector)))
return check_obj.select(pl.col(schema.selector)).collect_schema()

def run_checks_and_handle_errors(
self,
Expand Down Expand Up @@ -269,7 +265,7 @@ def check_nullable(
isna = check_obj.select(expr)
passed = isna.select([pl.col("*").all()]).collect()
results = []
for column in get_lazyframe_column_names(isna):
for column in isna.collect_schema().names():
if passed.select(column).item():
continue
failure_cases = (
Expand Down Expand Up @@ -378,9 +374,7 @@ def check_dtype(

results = []
check_obj_subset = check_obj.select(schema.selector)
for column, obj_dtype in get_lazyframe_schema(
check_obj_subset
).items():
for column, obj_dtype in check_obj_subset.collect_schema().items():
results.append(
CoreCheckResult(
passed=schema.dtype.check(
Expand Down
17 changes: 9 additions & 8 deletions pandera/backends/polars/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from pandera.api.base.error_handler import ErrorHandler, get_error_category
from pandera.api.polars.container import DataFrameSchema
from pandera.api.polars.types import PolarsData, PolarsFrame
from pandera.api.polars.utils import get_lazyframe_column_names
from pandera.backends.base import ColumnInfo, CoreCheckResult
from pandera.backends.polars.base import PolarsSchemaBackend
from pandera.config import ValidationDepth, ValidationScope, get_config_context
Expand Down Expand Up @@ -242,7 +241,7 @@ def collect_column_info(self, check_obj: pl.LazyFrame, schema):
for col_name, col_schema in schema.columns.items():
if (
not col_schema.regex
and col_name not in get_lazyframe_column_names(check_obj)
and col_name not in check_obj.collect_schema().names()
and col_schema.required
):
absent_column_names.append(col_name)
Expand All @@ -257,11 +256,11 @@ def collect_column_info(self, check_obj: pl.LazyFrame, schema):
regex_match_patterns.append(col_schema.selector)
except SchemaError:
pass
elif col_name in get_lazyframe_column_names(check_obj):
elif col_name in check_obj.collect_schema().names():
column_names.append(col_name)

# drop adjacent duplicated column names
destuttered_column_names = [*get_lazyframe_column_names(check_obj)]
destuttered_column_names = [*check_obj.collect_schema().names()]

return ColumnInfo(
sorted_column_names=dict.fromkeys(column_names),
Expand Down Expand Up @@ -298,7 +297,7 @@ def collect_schema_components(
# PydanticModel applies row-wise, so per-column components
# are not created for it.
columns = {}
for col_name in get_lazyframe_column_names(check_obj):
for col_name in check_obj.collect_schema().names():
columns[col_name] = Column(schema.dtype, name=str(col_name))

schema_components = []
Expand Down Expand Up @@ -506,7 +505,7 @@ def _coerce_dtype_helper(
else "coerce"
)

lf_columns = get_lazyframe_column_names(obj)
lf_columns = obj.collect_schema().names()

try:
if schema.dtype is not None:
Expand All @@ -525,8 +524,10 @@ def _coerce_dtype_helper(
# a coercion failure reports the concrete column
# name instead of the regex pattern (issue #2363,
# mirroring the check path fixed in #2221).
matched_columns = get_lazyframe_column_names(
matched_columns = (
obj.select(pl.col(col_schema.selector))
.collect_schema()
.names()
)
for matched_column in matched_columns:
obj = getattr(col_schema.dtype, coerce_fn)(
Expand Down Expand Up @@ -660,7 +661,7 @@ def check_column_values_are_unique(
check_output = None
for lst in temp_unique:
subset = [
x for x in lst if x in get_lazyframe_column_names(check_obj)
x for x in lst if x in check_obj.collect_schema().names()
]
duplicates = check_obj.select(subset).collect().is_duplicated()

Expand Down
Loading
Loading