From 100ac3b08e85259e4c8e592a7fbcb71e58a72db7 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 15 Apr 2026 20:40:33 +0200 Subject: [PATCH 01/71] feat: pytest plugin --- docs/api-reference/testing.md | 64 ++++ narwhals/testing/constructors/__init__.py | 65 ++++ narwhals/testing/constructors/_classes.py | 346 +++++++++++++++++++++ narwhals/testing/constructors/_name.py | 158 ++++++++++ narwhals/testing/constructors/_registry.py | 162 ++++++++++ narwhals/testing/pytest_plugin.py | 147 +++++++++ narwhals/testing/typing.py | 23 ++ pyproject.toml | 4 + utils/import_check.py | 14 + 9 files changed, 983 insertions(+) create mode 100644 narwhals/testing/constructors/__init__.py create mode 100644 narwhals/testing/constructors/_classes.py create mode 100644 narwhals/testing/constructors/_name.py create mode 100644 narwhals/testing/constructors/_registry.py create mode 100644 narwhals/testing/pytest_plugin.py create mode 100644 narwhals/testing/typing.py diff --git a/docs/api-reference/testing.md b/docs/api-reference/testing.md index db83c6930e..4a8109fe32 100644 --- a/docs/api-reference/testing.md +++ b/docs/api-reference/testing.md @@ -1,8 +1,72 @@ # `narwhals.testing` +## Asserts + ::: narwhals.testing handler: python options: members: - assert_frame_equal - assert_series_equal + +## `pytest` plugin + +Narwhals register a pytest plugin that exposes parametrized fixtures with callables +to build native frames from a column-oriented python `dict`. + +### Available fixtures + +| Fixture | Backends | +|---|---| +| `constructor` | every selected backend (eager + lazy) | +| `constructor_eager` | only eager backends | + +The selection is controlled by two CLI options: + +* `--constructors=pandas,polars[lazy],duckdb`: comma-separated list. + Defaults to [`DEFAULT_CONSTRUCTORS`][narwhals.testing.constructors.DEFAULT_CONSTRUCTORS] + intersected with the backends installed in the current environment. +* `--all-cpu-constructors`: shortcut for "every CPU backend that is installed". +* `--use-external-constructor`: Skip narwhals.testing's parametrisation and let + another plugin provide the `constructor*` fixtures. + +Set the `NARWHALS_DEFAULT_CONSTRUCTORS` environment variable to override the default +list (useful e.g. when running under `cudf.pandas`). + +### Quick start + +The plugin auto-loads as soon as you `pip install narwhals`. Just write a test: + +```python +from typing import TYPE_CHECKING + +import narwhals as nw + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager, Data + + +def test_shape(constructor_eager: ConstructorEager) -> None: + data: Data = {"x": [1, 2, 3]} + df = nw.from_native(constructor_eager(data), eager_only=True) + assert df.shape == (3, 1) +``` + +The fixtures are parametrised against every supported backend that is installed +in the current environment. Filter the matrix on the command line: + +```bash +pytest --constructors="pandas,polars[lazy]" +pytest --all-cpu-constructors +``` + +### Type aliases + +::: narwhals.testing.typing + handler: python + options: + members: + - Constructor + - ConstructorEager + - ConstructorLazy + - Data diff --git a/narwhals/testing/constructors/__init__.py b/narwhals/testing/constructors/__init__.py new file mode 100644 index 0000000000..6c30b061d9 --- /dev/null +++ b/narwhals/testing/constructors/__init__.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from narwhals.testing.constructors._classes import ( + ConstructorBase, + ConstructorEagerBase, + ConstructorLazyBase, + CudfConstructor, + DaskConstructor, + DuckDBConstructor, + IbisConstructor, + ModinConstructor, + ModinPyArrowConstructor, + PandasConstructor, + PandasNullableConstructor, + PandasPyArrowConstructor, + PolarsEagerConstructor, + PolarsLazyConstructor, + PyArrowConstructor, + PySparkConnectConstructor, + PySparkConstructor, + SQLFrameConstructor, + pyspark_session, + sqlframe_session, +) +from narwhals.testing.constructors._name import ConstructorName +from narwhals.testing.constructors._registry import ( + ALL_CONSTRUCTORS, + ALL_CPU_CONSTRUCTORS, + DEFAULT_CONSTRUCTORS, + available_constructors, + get_constructor, + is_backend_available, + resolve_constructors, +) + +__all__ = [ + "ALL_CONSTRUCTORS", + "ALL_CPU_CONSTRUCTORS", + "DEFAULT_CONSTRUCTORS", + "ConstructorBase", + "ConstructorEagerBase", + "ConstructorLazyBase", + "ConstructorName", + "CudfConstructor", + "DaskConstructor", + "DuckDBConstructor", + "IbisConstructor", + "ModinConstructor", + "ModinPyArrowConstructor", + "PandasConstructor", + "PandasNullableConstructor", + "PandasPyArrowConstructor", + "PolarsEagerConstructor", + "PolarsLazyConstructor", + "PyArrowConstructor", + "PySparkConnectConstructor", + "PySparkConstructor", + "SQLFrameConstructor", + "available_constructors", + "get_constructor", + "is_backend_available", + "pyspark_session", + "resolve_constructors", + "sqlframe_session", +] diff --git a/narwhals/testing/constructors/_classes.py b/narwhals/testing/constructors/_classes.py new file mode 100644 index 0000000000..61b75c0c50 --- /dev/null +++ b/narwhals/testing/constructors/_classes.py @@ -0,0 +1,346 @@ +from __future__ import annotations + +import os +import uuid +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy +from functools import lru_cache +from typing import TYPE_CHECKING, Any, ClassVar, cast + +from narwhals._utils import generate_temporary_column_name +from narwhals.testing.constructors._name import ConstructorName + +if TYPE_CHECKING: + import ibis + import pandas as pd + import polars as pl + import pyarrow as pa + from ibis.backends.duckdb import Backend as IbisDuckDBBackend + from pyspark.sql import SparkSession + from sqlframe.duckdb import DuckDBSession + + from narwhals._native import NativeDask, NativeDuckDB, NativePySpark, NativeSQLFrame + from narwhals.testing.typing import Data + from narwhals.typing import IntoDataFrame + + +def sqlframe_session() -> DuckDBSession: + """Return a fresh in-memory `sqlframe` DuckDB session.""" + from sqlframe.duckdb import DuckDBSession + + # NOTE: `__new__` override inferred by `pyright` only + # https://github.com/eakmanrq/sqlframe/blob/772b3a6bfe5a1ffd569b7749d84bea2f3a314510/sqlframe/base/session.py#L181-L184 + return cast("DuckDBSession", DuckDBSession()) # type: ignore[redundant-cast] + + +def pyspark_session() -> SparkSession: # pragma: no cover + """Return a singleton local `pyspark` (or pyspark[connect]) session.""" + if is_spark_connect := os.environ.get("SPARK_CONNECT", None): + from pyspark.sql.connect.session import SparkSession + else: + from pyspark.sql import SparkSession + builder = cast("SparkSession.Builder", SparkSession.builder).appName("unit-tests") + builder = ( + builder.remote(f"sc://localhost:{os.environ.get('SPARK_PORT', '15002')}") + if is_spark_connect + else builder.master("local[1]").config("spark.ui.enabled", "false") + ) + return ( + builder.config("spark.default.parallelism", "1") + .config("spark.sql.shuffle.partitions", "2") + .config("spark.sql.session.timeZone", "UTC") + .getOrCreate() + ) + + +@lru_cache(maxsize=1) +def _ibis_backend() -> IbisDuckDBBackend: # pragma: no cover + """Cached singleton in-memory ibis backend, so all tables share one database.""" + import ibis + + return ibis.duckdb.connect() + + +# Legacy `_constructor` function names, exposed via +# `ConstructorBase.__name__` so existing tests in the Narwhals suite that do +# `constructor.__name__ in {"pandas_pyarrow_constructor", ...}` keep working. +_LEGACY_NAMES: dict[ConstructorName, str] = { + ConstructorName.PANDAS: "pandas_constructor", + ConstructorName.PANDAS_NULLABLE: "pandas_nullable_constructor", + ConstructorName.PANDAS_PYARROW: "pandas_pyarrow_constructor", + ConstructorName.PYARROW: "pyarrow_table_constructor", + ConstructorName.MODIN: "modin_constructor", + ConstructorName.MODIN_PYARROW: "modin_pyarrow_constructor", + ConstructorName.CUDF: "cudf_constructor", + ConstructorName.POLARS_EAGER: "polars_eager_constructor", + ConstructorName.POLARS_LAZY: "polars_lazy_constructor", + ConstructorName.DASK: "dask_lazy_p2_constructor", + ConstructorName.DUCKDB: "duckdb_lazy_constructor", + ConstructorName.PYSPARK: "pyspark_lazy_constructor", + ConstructorName.PYSPARK_CONNECT: "pyspark_lazy_constructor", + ConstructorName.SQLFRAME: "sqlframe_pyspark_lazy_constructor", + ConstructorName.IBIS: "ibis_lazy_constructor", +} + + +@lru_cache(maxsize=1) +def _pyspark_session_lazy() -> SparkSession: # pragma: no cover + """Cached pyspark session; created on first use, stopped at interpreter exit.""" + from atexit import register + + with warnings.catch_warnings(): + # The spark session seems to trigger a polars warning. + warnings.filterwarnings( + "ignore", r"Using fork\(\) can cause Polars", category=RuntimeWarning + ) + session = pyspark_session() + register(session.stop) + return session + + +# --- Base classes ------------------------------------------------------------ + + +class ConstructorBase(ABC): + """Abstract base for any constructor exposed by `narwhals.testing`. + + A constructor is a callable that turns a column-oriented `dict` (typed as + [`Data`][narwhals.testing.typing.Data] but accepted as `Any` for + backwards compatibility with mappings, ranges, numpy arrays, …) into a + native dataframe / lazy frame, plus a typed [`ConstructorName`][] that + identifies the backend. + """ + + name: ClassVar[ConstructorName] + + @abstractmethod + def __call__(self, obj: Any) -> Any: + """Build a native frame from `obj`.""" + + def __str__(self) -> str: + # Returns the legacy `_constructor` identifier so existing + # downstream `"_constructor" in str(c)` substring checks + # continue to work. Use [`ConstructorBase.name`][] for the typed, + # CLI-style identifier (e.g. `"pandas[pyarrow]"`). + return _LEGACY_NAMES[self.name] + + @property + def __name__(self) -> str: + """Legacy `_constructor` identifier (kept for backwards compatibility).""" + return _LEGACY_NAMES[self.name] + + def __repr__(self) -> str: + return f"{type(self).__name__}()" + + def __hash__(self) -> int: + return hash((type(self), self.name)) + + def __eq__(self, other: object) -> bool: + return ( + type(self) is type(other) and self.name == cast("ConstructorBase", other).name + ) + + +class ConstructorEagerBase(ConstructorBase): + """A constructor that returns an *eager* native dataframe.""" + + @abstractmethod + def __call__(self, obj: Any) -> IntoDataFrame: ... + + +class ConstructorLazyBase(ConstructorBase): + """A constructor that returns a *lazy* native frame.""" + + @abstractmethod + def __call__(self, obj: Any) -> Any: ... + + +# --- Eager constructors ------------------------------------------------------ + + +class PandasConstructor(ConstructorEagerBase): + name = ConstructorName.PANDAS + + def __call__(self, obj: Data) -> pd.DataFrame: + import pandas as pd + + return pd.DataFrame(obj) + + +class PandasNullableConstructor(ConstructorEagerBase): + name = ConstructorName.PANDAS_NULLABLE + + def __call__(self, obj: Data) -> pd.DataFrame: + import pandas as pd + + return pd.DataFrame(obj).convert_dtypes(dtype_backend="numpy_nullable") + + +class PandasPyArrowConstructor(ConstructorEagerBase): + name = ConstructorName.PANDAS_PYARROW + + def __call__(self, obj: Data) -> pd.DataFrame: + import pandas as pd + + return pd.DataFrame(obj).convert_dtypes(dtype_backend="pyarrow") + + +class PyArrowConstructor(ConstructorEagerBase): + name = ConstructorName.PYARROW + + def __call__(self, obj: Data) -> pa.Table: + import pyarrow as pa + + return pa.table(obj) # type:ignore[arg-type] + + +class ModinConstructor(ConstructorEagerBase): # pragma: no cover + name = ConstructorName.MODIN + + def __call__(self, obj: Data) -> IntoDataFrame: + import modin.pandas as mpd + import pandas as pd + + return cast("IntoDataFrame", mpd.DataFrame(pd.DataFrame(obj))) + + +class ModinPyArrowConstructor(ConstructorEagerBase): # pragma: no cover + name = ConstructorName.MODIN_PYARROW + + def __call__(self, obj: Data) -> IntoDataFrame: + import modin.pandas as mpd + import pandas as pd + + df = mpd.DataFrame(pd.DataFrame(obj)).convert_dtypes(dtype_backend="pyarrow") + return cast("IntoDataFrame", df) + + +class CudfConstructor(ConstructorEagerBase): # pragma: no cover + name = ConstructorName.CUDF + + def __call__(self, obj: Data) -> IntoDataFrame: + import cudf + + return cast("IntoDataFrame", cudf.DataFrame(obj)) + + +class PolarsEagerConstructor(ConstructorEagerBase): + name = ConstructorName.POLARS_EAGER + + def __call__(self, obj: Data) -> pl.DataFrame: + import polars as pl + + return pl.DataFrame(obj) + + +# --- Lazy constructors ------------------------------------------------------- + + +class PolarsLazyConstructor(ConstructorLazyBase): + name = ConstructorName.POLARS_LAZY + + def __call__(self, obj: Data) -> pl.LazyFrame: + import polars as pl + + return pl.LazyFrame(obj) + + +class DaskConstructor(ConstructorLazyBase): # pragma: no cover + name = ConstructorName.DASK + + def __init__(self, npartitions: int = 2) -> None: + self.npartitions = npartitions + + def __call__(self, obj: Data) -> NativeDask: + import dask.dataframe as dd + + return cast("NativeDask", dd.from_dict(obj, npartitions=self.npartitions)) + + def __repr__(self) -> str: + return f"{type(self).__name__}(npartitions={self.npartitions})" + + def __hash__(self) -> int: + return hash((type(self), self.name, self.npartitions)) + + def __eq__(self, other: object) -> bool: + return ( + type(self) is type(other) + and self.npartitions == cast("DaskConstructor", other).npartitions + ) + + +class DuckDBConstructor(ConstructorLazyBase): + name = ConstructorName.DUCKDB + + def __call__(self, obj: Data) -> NativeDuckDB: + import duckdb + import pyarrow as pa + + duckdb.sql("""set timezone = 'UTC'""") + _df = pa.table(obj) # type:ignore[arg-type] + return duckdb.sql("select * from _df") + + +class PySparkConstructor(ConstructorLazyBase): # pragma: no cover + name = ConstructorName.PYSPARK + + def __call__(self, obj: Data) -> NativePySpark: + session = _pyspark_session_lazy() + _obj = deepcopy(obj) + index_col_name = generate_temporary_column_name(n_bytes=8, columns=list(_obj)) + _obj[index_col_name] = list(range(len(_obj[next(iter(_obj))]))) + result = ( + session.createDataFrame([*zip(*_obj.values())], schema=[*_obj.keys()]) + .repartition(2) + .orderBy(index_col_name) + .drop(index_col_name) + ) + return cast("NativePySpark", result) + + +class PySparkConnectConstructor(PySparkConstructor): # pragma: no cover + name = ConstructorName.PYSPARK_CONNECT + + +class SQLFrameConstructor(ConstructorLazyBase): # pragma: no cover + name = ConstructorName.SQLFRAME + + def __call__(self, obj: Data) -> NativeSQLFrame: + session = sqlframe_session() + return session.createDataFrame([*zip(*obj.values())], schema=[*obj.keys()]) + + +class IbisConstructor(ConstructorLazyBase): # pragma: no cover + name = ConstructorName.IBIS + + def __call__(self, obj: Data) -> ibis.Table: + import pyarrow as pa + + table = pa.table(obj) # type:ignore[arg-type] + table_name = str(uuid.uuid4()) + return _ibis_backend().create_table(table_name, table) + + +__all__ = [ + "ConstructorBase", + "ConstructorEagerBase", + "ConstructorLazyBase", + "CudfConstructor", + "DaskConstructor", + "DuckDBConstructor", + "IbisConstructor", + "ModinConstructor", + "ModinPyArrowConstructor", + "PandasConstructor", + "PandasNullableConstructor", + "PandasPyArrowConstructor", + "PolarsEagerConstructor", + "PolarsLazyConstructor", + "PyArrowConstructor", + "PySparkConnectConstructor", + "PySparkConstructor", + "SQLFrameConstructor", + "pyspark_session", + "sqlframe_session", +] diff --git a/narwhals/testing/constructors/_name.py b/narwhals/testing/constructors/_name.py new file mode 100644 index 0000000000..317ed5eb37 --- /dev/null +++ b/narwhals/testing/constructors/_name.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import pytest + + +class ConstructorName(str, Enum): + """Typed identifier for each backend exposed by `narwhals.testing.constructors`. + + The string values are byte-identical to the identifiers accepted by the + `--constructors` pytest CLI option (e.g. `pandas[pyarrow]`, `polars[lazy]`). + + Examples: + >>> from narwhals.testing.constructors import ConstructorName + >>> ConstructorName.PANDAS_PYARROW.value + 'pandas[pyarrow]' + >>> ConstructorName.PANDAS_PYARROW.is_pandas_like + True + >>> ConstructorName.PANDAS_PYARROW.needs_pyarrow + True + """ + + PANDAS = "pandas" + PANDAS_NULLABLE = "pandas[nullable]" + PANDAS_PYARROW = "pandas[pyarrow]" + PYARROW = "pyarrow" + MODIN = "modin" + MODIN_PYARROW = "modin[pyarrow]" + CUDF = "cudf" + POLARS_EAGER = "polars[eager]" + POLARS_LAZY = "polars[lazy]" + DASK = "dask" + DUCKDB = "duckdb" + PYSPARK = "pyspark" + PYSPARK_CONNECT = "pyspark[connect]" + SQLFRAME = "sqlframe" + IBIS = "ibis" + + def __str__(self) -> str: + return str(self.value) + + @property + def is_pandas(self) -> bool: + """Whether this is one of the pandas constructors.""" + return self in { + ConstructorName.PANDAS, + ConstructorName.PANDAS_NULLABLE, + ConstructorName.PANDAS_PYARROW, + } + + @property + def is_modin(self) -> bool: + """Whether this is one of the modin constructors.""" + return self in {ConstructorName.MODIN, ConstructorName.MODIN_PYARROW} + + @property + def is_cudf(self) -> bool: + """Whether this is the cudf constructor.""" + return self is ConstructorName.CUDF + + @property + def is_pandas_like(self) -> bool: + """Whether this constructor produces a pandas-like dataframe (pandas, modin, cudf).""" + return self.is_pandas or self.is_modin or self.is_cudf + + @property + def is_polars(self) -> bool: + """Whether this is one of the polars constructors.""" + return self in {ConstructorName.POLARS_EAGER, ConstructorName.POLARS_LAZY} + + @property + def is_pyarrow(self) -> bool: + """Whether this is the pyarrow table constructor.""" + return self is ConstructorName.PYARROW + + @property + def is_dask(self) -> bool: + """Whether this is the dask constructor.""" + return self is ConstructorName.DASK + + @property + def is_duckdb(self) -> bool: + """Whether this is the duckdb constructor.""" + return self is ConstructorName.DUCKDB + + @property + def is_pyspark(self) -> bool: + """Whether this is one of the pyspark constructors.""" + return self in {ConstructorName.PYSPARK, ConstructorName.PYSPARK_CONNECT} + + @property + def is_sqlframe(self) -> bool: + """Whether this is the sqlframe constructor.""" + return self is ConstructorName.SQLFRAME + + @property + def is_ibis(self) -> bool: + """Whether this is the ibis constructor.""" + return self is ConstructorName.IBIS + + @property + def is_spark_like(self) -> bool: + """Whether this constructor uses a spark-like backend (pyspark, sqlframe).""" + return self.is_pyspark or self.is_sqlframe + + @property + def is_eager(self) -> bool: + """Whether this constructor produces an eager native dataframe.""" + return self in { + ConstructorName.PANDAS, + ConstructorName.PANDAS_NULLABLE, + ConstructorName.PANDAS_PYARROW, + ConstructorName.PYARROW, + ConstructorName.MODIN, + ConstructorName.MODIN_PYARROW, + ConstructorName.CUDF, + ConstructorName.POLARS_EAGER, + } + + @property + def is_lazy(self) -> bool: + """Whether this constructor produces a lazy native frame.""" + return not self.is_eager + + @property + def needs_pyarrow(self) -> bool: + """Whether this constructor requires `pyarrow` to be installed.""" + return self in { + ConstructorName.PYARROW, + ConstructorName.PANDAS_PYARROW, + ConstructorName.MODIN_PYARROW, + ConstructorName.DUCKDB, + ConstructorName.IBIS, + } + + @property + def needs_gpu(self) -> bool: + """Whether this constructor requires GPU hardware.""" + return self is ConstructorName.CUDF + + @classmethod + def from_pytest_request(cls, request: pytest.FixtureRequest) -> ConstructorName: + """Resolve the [`ConstructorName`][] from the current parametrised pytest request. + + Examples: + >>> import pytest + >>> def test_example(constructor, request): # doctest: +SKIP + ... name = ConstructorName.from_pytest_request(request) + ... if name.is_pandas_like: + ... ... + """ + return cls(str(request.node.callspec.id)) + + +__all__ = ["ConstructorName"] diff --git a/narwhals/testing/constructors/_registry.py b/narwhals/testing/constructors/_registry.py new file mode 100644 index 0000000000..f831752a96 --- /dev/null +++ b/narwhals/testing/constructors/_registry.py @@ -0,0 +1,162 @@ +"""Registry of constructors that ship with `narwhals.testing`.""" + +from __future__ import annotations + +from importlib.util import find_spec +from typing import TYPE_CHECKING + +from narwhals.testing.constructors._classes import ( + ConstructorBase, + CudfConstructor, + DaskConstructor, + DuckDBConstructor, + IbisConstructor, + ModinConstructor, + ModinPyArrowConstructor, + PandasConstructor, + PandasNullableConstructor, + PandasPyArrowConstructor, + PolarsEagerConstructor, + PolarsLazyConstructor, + PyArrowConstructor, + PySparkConnectConstructor, + PySparkConstructor, + SQLFrameConstructor, +) +from narwhals.testing.constructors._name import ConstructorName + +if TYPE_CHECKING: + from collections.abc import Iterable + + +# Singleton instance per backend. Users that need a non-default parametrisation +# (e.g. `DaskConstructor(npartitions=1)`) can instantiate the class directly. +ALL_CONSTRUCTORS: dict[ConstructorName, ConstructorBase] = { + ConstructorName.PANDAS: PandasConstructor(), + ConstructorName.PANDAS_NULLABLE: PandasNullableConstructor(), + ConstructorName.PANDAS_PYARROW: PandasPyArrowConstructor(), + ConstructorName.PYARROW: PyArrowConstructor(), + ConstructorName.MODIN: ModinConstructor(), + ConstructorName.MODIN_PYARROW: ModinPyArrowConstructor(), + ConstructorName.CUDF: CudfConstructor(), + ConstructorName.POLARS_EAGER: PolarsEagerConstructor(), + ConstructorName.POLARS_LAZY: PolarsLazyConstructor(), + ConstructorName.DASK: DaskConstructor(), + ConstructorName.DUCKDB: DuckDBConstructor(), + ConstructorName.PYSPARK: PySparkConstructor(), + ConstructorName.PYSPARK_CONNECT: PySparkConnectConstructor(), + ConstructorName.SQLFRAME: SQLFrameConstructor(), + ConstructorName.IBIS: IbisConstructor(), +} + +DEFAULT_CONSTRUCTORS: frozenset[ConstructorName] = frozenset( + { + ConstructorName.PANDAS, + ConstructorName.PANDAS_PYARROW, + ConstructorName.POLARS_EAGER, + ConstructorName.PYARROW, + ConstructorName.DUCKDB, + ConstructorName.SQLFRAME, + ConstructorName.IBIS, + } +) +"""Subset of constructors enabled by default for parametrised tests when the +user does not pass `--constructors` (mirrors the historical Narwhals defaults). +""" + +# All constructors that don't require a GPU. Useful for `--all-cpu-constructors`. +ALL_CPU_CONSTRUCTORS: frozenset[ConstructorName] = frozenset( + name for name in ALL_CONSTRUCTORS if not name.needs_gpu +) + + +# Map from `ConstructorName` to the package import name that needs to be +# importable for that constructor to work. Some backends have extra +# requirements (e.g. `pandas[pyarrow]` also needs `pyarrow` installed); we +# encode those as tuples here so `is_backend_available` can check all of them. +_BACKEND_REQUIREMENTS: dict[ConstructorName, tuple[str, ...]] = { + ConstructorName.PANDAS: ("pandas",), + ConstructorName.PANDAS_NULLABLE: ("pandas",), + ConstructorName.PANDAS_PYARROW: ("pandas", "pyarrow"), + ConstructorName.PYARROW: ("pyarrow",), + ConstructorName.MODIN: ("modin",), + ConstructorName.MODIN_PYARROW: ("modin", "pyarrow"), + ConstructorName.CUDF: ("cudf",), + ConstructorName.POLARS_EAGER: ("polars",), + ConstructorName.POLARS_LAZY: ("polars",), + ConstructorName.DASK: ("dask",), + ConstructorName.DUCKDB: ("duckdb", "pyarrow"), + ConstructorName.PYSPARK: ("pyspark",), + ConstructorName.PYSPARK_CONNECT: ("pyspark",), + ConstructorName.SQLFRAME: ("sqlframe", "duckdb"), + ConstructorName.IBIS: ("ibis", "duckdb", "pyarrow"), +} + + +def is_backend_available(name: ConstructorName) -> bool: + """Whether all backends required by `name` can be imported in this environment. + + Examples: + >>> from narwhals.testing.constructors import ( + ... ConstructorName, + ... is_backend_available, + ... ) + >>> is_backend_available(ConstructorName.PANDAS) + True + """ + return all(find_spec(pkg) is not None for pkg in _BACKEND_REQUIREMENTS[name]) + + +def available_constructors() -> frozenset[ConstructorName]: + """Return every [`ConstructorName`][] whose backend is importable. + + Examples: + >>> from narwhals.testing.constructors import available_constructors + >>> ConstructorName.PANDAS in available_constructors() + True + """ + return frozenset(name for name in ALL_CONSTRUCTORS if is_backend_available(name)) + + +def get_constructor(name: ConstructorName | str) -> ConstructorBase: + """Return the registered singleton constructor for `name`. + + Arguments: + name: A [`ConstructorName`][] member or its string value + (e.g. `"pandas[pyarrow]"`). + + Raises: + ValueError: If `name` is not a registered constructor identifier. + + Examples: + >>> from narwhals.testing.constructors import get_constructor + >>> get_constructor("pandas") + PandasConstructor() + """ + try: + key = ConstructorName(name) if isinstance(name, str) else name + except ValueError as exc: + valid = sorted(c.value for c in ConstructorName) + msg = f"Unknown constructor {name!r}. Expected one of: {valid}." + raise ValueError(msg) from exc + return ALL_CONSTRUCTORS[key] + + +def resolve_constructors(names: Iterable[ConstructorName | str]) -> list[ConstructorBase]: + """Resolve an iterable of names / identifiers into a list of constructor instances. + + Order is preserved; duplicates are kept (so the same constructor can be + parametrised multiple times if explicitly requested). + """ + return [get_constructor(n) for n in names] + + +__all__ = [ + "ALL_CONSTRUCTORS", + "ALL_CPU_CONSTRUCTORS", + "DEFAULT_CONSTRUCTORS", + "available_constructors", + "get_constructor", + "is_backend_available", + "resolve_constructors", +] diff --git a/narwhals/testing/pytest_plugin.py b/narwhals/testing/pytest_plugin.py new file mode 100644 index 0000000000..cf8e7d326a --- /dev/null +++ b/narwhals/testing/pytest_plugin.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import os +from typing import TYPE_CHECKING, cast + +from narwhals._utils import parse_version +from narwhals.testing.constructors._classes import ConstructorBase, ConstructorEagerBase +from narwhals.testing.constructors._name import ConstructorName +from narwhals.testing.constructors._registry import ( + ALL_CPU_CONSTRUCTORS, + DEFAULT_CONSTRUCTORS, + available_constructors, + get_constructor, +) + +if TYPE_CHECKING: + from collections.abc import Iterable + + import pytest + + +_MIN_PANDAS_NULLABLE_VERSION: tuple[int, ...] = (2, 0, 0) +"""`pandas.convert_dtypes(dtype_backend=...)` requires pandas >= 2.0.0.""" + +_ALL_CPU_EXCLUSIONS: frozenset[ConstructorName] = frozenset( + {ConstructorName.MODIN, ConstructorName.PYSPARK_CONNECT} +) +"""Backends excluded from `--all-cpu-constructors` even when installed: + +* modin is too slow for the full matrix +* pyspark[connect] needs a different local setup and can't run alongside pyspark +""" + + +def _pandas_version() -> tuple[int, ...]: + try: + import pandas as pd + except ImportError: # pragma: no cover + return (0, 0, 0) + return parse_version(pd.__version__) + + +def _default_constructor_ids() -> list[str]: + """Resolve the default `--constructors` value for the current environment. + + Honours `NARWHALS_DEFAULT_CONSTRUCTORS` if set, otherwise restricts + [`DEFAULT_CONSTRUCTORS`][] to backends whose libraries are importable. + """ + if env := os.environ.get("NARWHALS_DEFAULT_CONSTRUCTORS"): # pragma: no cover + return env.split(",") + available = available_constructors() + return [name.value for name in DEFAULT_CONSTRUCTORS if name in available] + + +def pytest_addoption(parser: pytest.Parser) -> None: + group = parser.getgroup("narwhals", "narwhals.testing") + defaults = ", ".join(f"'{c.value}'" for c in sorted(DEFAULT_CONSTRUCTORS)) + group.addoption( + "--constructors", + action="store", + default=",".join(_default_constructor_ids()), + type=str, + help=( + "Comma-separated list of backend constructors to parametrise. " + f"Defaults to the installed subset of ({defaults})" + ), + ) + group.addoption( + "--all-cpu-constructors", + action="store_true", + default=False, + help=( + "Run tests against every installed CPU constructor " + "(overrides --constructors)." + ), + ) + # Escape hatch for downstream test suites that ship their own constructor + # plugin. When set, this plugin still adds the CLI options but stops + # parametrising the fixtures. + group.addoption( + "--use-external-constructor", + action="store_true", + default=False, + help=( + "Skip narwhals.testing's parametrisation and let another plugin " + "provide the `constructor*` fixtures." + ), + ) + + +def _select_constructors( + config: pytest.Config, +) -> list[ConstructorName]: # pragma: no cover + if config.getoption("all_cpu_constructors"): + names: Iterable[ConstructorName] = sorted( + ALL_CPU_CONSTRUCTORS - _ALL_CPU_EXCLUSIONS, key=lambda c: c.value + ) + else: + opt = cast("str", config.getoption("constructors")) + names = [ConstructorName(c) for c in opt.split(",") if c] + + pandas_version = _pandas_version() + selected: list[ConstructorName] = [] + for name in names: + if ( + name in {ConstructorName.PANDAS_NULLABLE, ConstructorName.PANDAS_PYARROW} + and pandas_version < _MIN_PANDAS_NULLABLE_VERSION + ): + continue + selected.append(name) + return selected + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + if metafunc.config.getoption("use_external_constructor"): # pragma: no cover + return + + fixturenames = set(metafunc.fixturenames) + if not fixturenames & {"constructor", "constructor_eager", "constructor_pandas_like"}: + return + + selected = _select_constructors(metafunc.config) + + constructors: list[ConstructorBase] = [] + constructor_ids: list[str] = [] + eager: list[ConstructorEagerBase] = [] + eager_ids: list[str] = [] + pandas_like: list[ConstructorEagerBase] = [] + pandas_like_ids: list[str] = [] + + for name in selected: + constructor = get_constructor(name) + constructors.append(constructor) + constructor_ids.append(name.value) + if isinstance(constructor, ConstructorEagerBase): + eager.append(constructor) + eager_ids.append(name.value) + if name.is_pandas_like: + pandas_like.append(constructor) + pandas_like_ids.append(name.value) + + if "constructor_eager" in fixturenames: + metafunc.parametrize("constructor_eager", eager, ids=eager_ids) + elif "constructor" in fixturenames: + metafunc.parametrize("constructor", constructors, ids=constructor_ids) + elif "constructor_pandas_like" in metafunc.fixturenames: + metafunc.parametrize("constructor_pandas_like", pandas_like, ids=pandas_like_ids) diff --git a/narwhals/testing/typing.py b/narwhals/testing/typing.py new file mode 100644 index 0000000000..3bcec0cc8d --- /dev/null +++ b/narwhals/testing/typing.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + + from narwhals._native import NativeLazyFrame + from narwhals.typing import IntoDataFrame + +Data: TypeAlias = dict[str, list[Any]] +"""A column-oriented mapping used as input to a [`Constructor`][].""" + +Constructor: TypeAlias = Callable[[Data], "NativeLazyFrame | IntoDataFrame"] +"""Any constructor (eager or lazy) — anything callable that returns a native frame.""" + +ConstructorEager: TypeAlias = Callable[[Data], "IntoDataFrame"] +"""A constructor that returns an eager native dataframe.""" + +ConstructorLazy: TypeAlias = Callable[[Data], "NativeLazyFrame"] +"""A constructor that returns a lazy native frame.""" + +__all__ = ["Constructor", "ConstructorEager", "ConstructorLazy", "Data"] diff --git a/pyproject.toml b/pyproject.toml index de466357bc..8b688effda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,10 @@ Documentation = "https://narwhals-dev.github.io/narwhals/" Repository = "https://github.com/narwhals-dev/narwhals" "Bug Tracker" = "https://github.com/narwhals-dev/narwhals/issues" +[project.entry-points.pytest11] +narwhals_testing = "narwhals.testing.pytest_plugin" +# See: https://docs.pytest.org/en/stable/how-to/writing_plugins.html#making-your-plugin-installable-by-others + [project.optional-dependencies] # These should be aligned with MIN_VERSIONS in narwhals/utils.py # Exception: modin, because `modin.__version__` isn't aligned with diff --git a/utils/import_check.py b/utils/import_check.py index d292b40790..d97b488509 100644 --- a/utils/import_check.py +++ b/utils/import_check.py @@ -27,6 +27,20 @@ "_polars": {"polars"}, "_duckdb": {"duckdb"}, "_ibis": {"ibis", "ibis._", "ibis.expr.types"}, + # narwhals.testing constructors deliberately lazy-import every supported + # backend inside `__call__` so test fixtures can build native frames. + "testing": { + "cudf", + "dask", + "dask.dataframe", + "duckdb", + "ibis", + "modin", + "pandas", + "polars", + "pyarrow", + "pyspark", + }, } From 1fd776da009646e088957413eebc6453751776b5 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 15 Apr 2026 22:39:38 +0200 Subject: [PATCH 02/71] simplify a bit --- narwhals/testing/constructors/__init__.py | 43 +-- narwhals/testing/constructors/_classes.py | 96 ++++--- narwhals/testing/constructors/_name.py | 37 +++ narwhals/testing/constructors/_registry.py | 80 +----- narwhals/testing/typing.py | 17 +- tests/conftest.py | 293 +-------------------- 6 files changed, 110 insertions(+), 456 deletions(-) diff --git a/narwhals/testing/constructors/__init__.py b/narwhals/testing/constructors/__init__.py index 6c30b061d9..9bac95a9fd 100644 --- a/narwhals/testing/constructors/__init__.py +++ b/narwhals/testing/constructors/__init__.py @@ -1,27 +1,6 @@ from __future__ import annotations -from narwhals.testing.constructors._classes import ( - ConstructorBase, - ConstructorEagerBase, - ConstructorLazyBase, - CudfConstructor, - DaskConstructor, - DuckDBConstructor, - IbisConstructor, - ModinConstructor, - ModinPyArrowConstructor, - PandasConstructor, - PandasNullableConstructor, - PandasPyArrowConstructor, - PolarsEagerConstructor, - PolarsLazyConstructor, - PyArrowConstructor, - PySparkConnectConstructor, - PySparkConstructor, - SQLFrameConstructor, - pyspark_session, - sqlframe_session, -) +from narwhals.testing.constructors._classes import pyspark_session, sqlframe_session from narwhals.testing.constructors._name import ConstructorName from narwhals.testing.constructors._registry import ( ALL_CONSTRUCTORS, @@ -29,7 +8,6 @@ DEFAULT_CONSTRUCTORS, available_constructors, get_constructor, - is_backend_available, resolve_constructors, ) @@ -37,28 +15,9 @@ "ALL_CONSTRUCTORS", "ALL_CPU_CONSTRUCTORS", "DEFAULT_CONSTRUCTORS", - "ConstructorBase", - "ConstructorEagerBase", - "ConstructorLazyBase", "ConstructorName", - "CudfConstructor", - "DaskConstructor", - "DuckDBConstructor", - "IbisConstructor", - "ModinConstructor", - "ModinPyArrowConstructor", - "PandasConstructor", - "PandasNullableConstructor", - "PandasPyArrowConstructor", - "PolarsEagerConstructor", - "PolarsLazyConstructor", - "PyArrowConstructor", - "PySparkConnectConstructor", - "PySparkConstructor", - "SQLFrameConstructor", "available_constructors", "get_constructor", - "is_backend_available", "pyspark_session", "resolve_constructors", "sqlframe_session", diff --git a/narwhals/testing/constructors/_classes.py b/narwhals/testing/constructors/_classes.py index 61b75c0c50..1827cbbee6 100644 --- a/narwhals/testing/constructors/_classes.py +++ b/narwhals/testing/constructors/_classes.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod from copy import deepcopy from functools import lru_cache -from typing import TYPE_CHECKING, Any, ClassVar, cast +from typing import TYPE_CHECKING, ClassVar, cast from narwhals._utils import generate_temporary_column_name from narwhals.testing.constructors._name import ConstructorName @@ -22,7 +22,7 @@ from narwhals._native import NativeDask, NativeDuckDB, NativePySpark, NativeSQLFrame from narwhals.testing.typing import Data - from narwhals.typing import IntoDataFrame + from narwhals.typing import IntoDataFrame, IntoFrame, IntoLazyFrame def sqlframe_session() -> DuckDBSession: @@ -62,28 +62,6 @@ def _ibis_backend() -> IbisDuckDBBackend: # pragma: no cover return ibis.duckdb.connect() -# Legacy `_constructor` function names, exposed via -# `ConstructorBase.__name__` so existing tests in the Narwhals suite that do -# `constructor.__name__ in {"pandas_pyarrow_constructor", ...}` keep working. -_LEGACY_NAMES: dict[ConstructorName, str] = { - ConstructorName.PANDAS: "pandas_constructor", - ConstructorName.PANDAS_NULLABLE: "pandas_nullable_constructor", - ConstructorName.PANDAS_PYARROW: "pandas_pyarrow_constructor", - ConstructorName.PYARROW: "pyarrow_table_constructor", - ConstructorName.MODIN: "modin_constructor", - ConstructorName.MODIN_PYARROW: "modin_pyarrow_constructor", - ConstructorName.CUDF: "cudf_constructor", - ConstructorName.POLARS_EAGER: "polars_eager_constructor", - ConstructorName.POLARS_LAZY: "polars_lazy_constructor", - ConstructorName.DASK: "dask_lazy_p2_constructor", - ConstructorName.DUCKDB: "duckdb_lazy_constructor", - ConstructorName.PYSPARK: "pyspark_lazy_constructor", - ConstructorName.PYSPARK_CONNECT: "pyspark_lazy_constructor", - ConstructorName.SQLFRAME: "sqlframe_pyspark_lazy_constructor", - ConstructorName.IBIS: "ibis_lazy_constructor", -} - - @lru_cache(maxsize=1) def _pyspark_session_lazy() -> SparkSession: # pragma: no cover """Cached pyspark session; created on first use, stopped at interpreter exit.""" @@ -99,37 +77,20 @@ def _pyspark_session_lazy() -> SparkSession: # pragma: no cover return session -# --- Base classes ------------------------------------------------------------ - - class ConstructorBase(ABC): """Abstract base for any constructor exposed by `narwhals.testing`. A constructor is a callable that turns a column-oriented `dict` (typed as - [`Data`][narwhals.testing.typing.Data] but accepted as `Any` for - backwards compatibility with mappings, ranges, numpy arrays, …) into a - native dataframe / lazy frame, plus a typed [`ConstructorName`][] that - identifies the backend. + [`Data`][narwhals.testing.typing.Data]) into a native dataframe / lazy frame, + plus a typed [`ConstructorName`][] that identifies the backend. """ name: ClassVar[ConstructorName] @abstractmethod - def __call__(self, obj: Any) -> Any: + def __call__(self, obj: Data) -> IntoFrame: """Build a native frame from `obj`.""" - def __str__(self) -> str: - # Returns the legacy `_constructor` identifier so existing - # downstream `"_constructor" in str(c)` substring checks - # continue to work. Use [`ConstructorBase.name`][] for the typed, - # CLI-style identifier (e.g. `"pandas[pyarrow]"`). - return _LEGACY_NAMES[self.name] - - @property - def __name__(self) -> str: - """Legacy `_constructor` identifier (kept for backwards compatibility).""" - return _LEGACY_NAMES[self.name] - def __repr__(self) -> str: return f"{type(self).__name__}()" @@ -146,14 +107,14 @@ class ConstructorEagerBase(ConstructorBase): """A constructor that returns an *eager* native dataframe.""" @abstractmethod - def __call__(self, obj: Any) -> IntoDataFrame: ... + def __call__(self, obj: Data) -> IntoDataFrame: ... class ConstructorLazyBase(ConstructorBase): """A constructor that returns a *lazy* native frame.""" @abstractmethod - def __call__(self, obj: Any) -> Any: ... + def __call__(self, obj: Data) -> IntoLazyFrame: ... # --- Eager constructors ------------------------------------------------------ @@ -205,7 +166,7 @@ def __call__(self, obj: Data) -> IntoDataFrame: return cast("IntoDataFrame", mpd.DataFrame(pd.DataFrame(obj))) -class ModinPyArrowConstructor(ConstructorEagerBase): # pragma: no cover +class ModinPyArrowConstructor(ConstructorEagerBase): name = ConstructorName.MODIN_PYARROW def __call__(self, obj: Data) -> IntoDataFrame: @@ -303,7 +264,7 @@ class PySparkConnectConstructor(PySparkConstructor): # pragma: no cover name = ConstructorName.PYSPARK_CONNECT -class SQLFrameConstructor(ConstructorLazyBase): # pragma: no cover +class SQLFrameConstructor(ConstructorLazyBase): name = ConstructorName.SQLFRAME def __call__(self, obj: Data) -> NativeSQLFrame: @@ -311,7 +272,7 @@ def __call__(self, obj: Data) -> NativeSQLFrame: return session.createDataFrame([*zip(*obj.values())], schema=[*obj.keys()]) -class IbisConstructor(ConstructorLazyBase): # pragma: no cover +class IbisConstructor(ConstructorLazyBase): name = ConstructorName.IBIS def __call__(self, obj: Data) -> ibis.Table: @@ -322,6 +283,43 @@ def __call__(self, obj: Data) -> ibis.Table: return _ibis_backend().create_table(table_name, table) +_ALL_CONSTRUCTORS: dict[ConstructorName, ConstructorBase] = { + ConstructorName.PANDAS: PandasConstructor(), + ConstructorName.PANDAS_NULLABLE: PandasNullableConstructor(), + ConstructorName.PANDAS_PYARROW: PandasPyArrowConstructor(), + ConstructorName.PYARROW: PyArrowConstructor(), + ConstructorName.MODIN: ModinConstructor(), + ConstructorName.MODIN_PYARROW: ModinPyArrowConstructor(), + ConstructorName.CUDF: CudfConstructor(), + ConstructorName.POLARS_EAGER: PolarsEagerConstructor(), + ConstructorName.POLARS_LAZY: PolarsLazyConstructor(), + ConstructorName.DASK: DaskConstructor(), + ConstructorName.DUCKDB: DuckDBConstructor(), + ConstructorName.PYSPARK: PySparkConstructor(), + ConstructorName.PYSPARK_CONNECT: PySparkConnectConstructor(), + ConstructorName.SQLFRAME: SQLFrameConstructor(), + ConstructorName.IBIS: IbisConstructor(), +} + +_BACKEND_REQUIREMENTS: dict[ConstructorName, tuple[str, ...]] = { + ConstructorName.PANDAS: ("pandas",), + ConstructorName.PANDAS_NULLABLE: ("pandas",), + ConstructorName.PANDAS_PYARROW: ("pandas", "pyarrow"), + ConstructorName.PYARROW: ("pyarrow",), + ConstructorName.MODIN: ("modin",), + ConstructorName.MODIN_PYARROW: ("modin", "pyarrow"), + ConstructorName.CUDF: ("cudf",), + ConstructorName.POLARS_EAGER: ("polars",), + ConstructorName.POLARS_LAZY: ("polars",), + ConstructorName.DASK: ("dask",), + ConstructorName.DUCKDB: ("duckdb", "pyarrow"), + ConstructorName.PYSPARK: ("pyspark",), + ConstructorName.PYSPARK_CONNECT: ("pyspark",), + ConstructorName.SQLFRAME: ("sqlframe", "duckdb"), + ConstructorName.IBIS: ("ibis", "duckdb", "pyarrow"), +} + + __all__ = [ "ConstructorBase", "ConstructorEagerBase", diff --git a/narwhals/testing/constructors/_name.py b/narwhals/testing/constructors/_name.py index 317ed5eb37..e1fd9c8442 100644 --- a/narwhals/testing/constructors/_name.py +++ b/narwhals/testing/constructors/_name.py @@ -1,11 +1,28 @@ from __future__ import annotations from enum import Enum +from importlib.util import find_spec from typing import TYPE_CHECKING if TYPE_CHECKING: import pytest + from narwhals.testing.constructors._classes import ConstructorBase + + +def is_backend_available(*packages: str) -> bool: + """Whether all backends required by `name` can be imported in this environment. + + Examples: + >>> from narwhals.testing.constructors import ( + ... ConstructorName, + ... is_backend_available, + ... ) + >>> is_backend_available(ConstructorName.PANDAS) + True + """ + return all(find_spec(pkg) is not None for pkg in packages) + class ConstructorName(str, Enum): """Typed identifier for each backend exposed by `narwhals.testing.constructors`. @@ -136,6 +153,14 @@ def needs_pyarrow(self) -> bool: ConstructorName.IBIS, } + @property + def is_non_nullable(self) -> bool: + return self in { + ConstructorName.PANDAS, + ConstructorName.MODIN, + ConstructorName.DASK, + } + @property def needs_gpu(self) -> bool: """Whether this constructor requires GPU hardware.""" @@ -154,5 +179,17 @@ def from_pytest_request(cls, request: pytest.FixtureRequest) -> ConstructorName: """ return cls(str(request.node.callspec.id)) + @property + def constructor(self) -> ConstructorBase: + from narwhals.testing.constructors._classes import _ALL_CONSTRUCTORS + + return _ALL_CONSTRUCTORS[self] + + @property + def is_available(self) -> bool: + from narwhals.testing.constructors._classes import _BACKEND_REQUIREMENTS + + return is_backend_available(*_BACKEND_REQUIREMENTS[self]) + __all__ = ["ConstructorName"] diff --git a/narwhals/testing/constructors/_registry.py b/narwhals/testing/constructors/_registry.py index f831752a96..ac5b68a98e 100644 --- a/narwhals/testing/constructors/_registry.py +++ b/narwhals/testing/constructors/_registry.py @@ -2,51 +2,20 @@ from __future__ import annotations -from importlib.util import find_spec from typing import TYPE_CHECKING -from narwhals.testing.constructors._classes import ( - ConstructorBase, - CudfConstructor, - DaskConstructor, - DuckDBConstructor, - IbisConstructor, - ModinConstructor, - ModinPyArrowConstructor, - PandasConstructor, - PandasNullableConstructor, - PandasPyArrowConstructor, - PolarsEagerConstructor, - PolarsLazyConstructor, - PyArrowConstructor, - PySparkConnectConstructor, - PySparkConstructor, - SQLFrameConstructor, -) from narwhals.testing.constructors._name import ConstructorName if TYPE_CHECKING: from collections.abc import Iterable + from narwhals.testing.constructors._classes import ConstructorBase + # Singleton instance per backend. Users that need a non-default parametrisation # (e.g. `DaskConstructor(npartitions=1)`) can instantiate the class directly. ALL_CONSTRUCTORS: dict[ConstructorName, ConstructorBase] = { - ConstructorName.PANDAS: PandasConstructor(), - ConstructorName.PANDAS_NULLABLE: PandasNullableConstructor(), - ConstructorName.PANDAS_PYARROW: PandasPyArrowConstructor(), - ConstructorName.PYARROW: PyArrowConstructor(), - ConstructorName.MODIN: ModinConstructor(), - ConstructorName.MODIN_PYARROW: ModinPyArrowConstructor(), - ConstructorName.CUDF: CudfConstructor(), - ConstructorName.POLARS_EAGER: PolarsEagerConstructor(), - ConstructorName.POLARS_LAZY: PolarsLazyConstructor(), - ConstructorName.DASK: DaskConstructor(), - ConstructorName.DUCKDB: DuckDBConstructor(), - ConstructorName.PYSPARK: PySparkConstructor(), - ConstructorName.PYSPARK_CONNECT: PySparkConnectConstructor(), - ConstructorName.SQLFRAME: SQLFrameConstructor(), - ConstructorName.IBIS: IbisConstructor(), + name: name.constructor for name in ConstructorName } DEFAULT_CONSTRUCTORS: frozenset[ConstructorName] = frozenset( @@ -64,49 +33,11 @@ user does not pass `--constructors` (mirrors the historical Narwhals defaults). """ -# All constructors that don't require a GPU. Useful for `--all-cpu-constructors`. ALL_CPU_CONSTRUCTORS: frozenset[ConstructorName] = frozenset( - name for name in ALL_CONSTRUCTORS if not name.needs_gpu + name for name in ConstructorName if not name.needs_gpu ) -# Map from `ConstructorName` to the package import name that needs to be -# importable for that constructor to work. Some backends have extra -# requirements (e.g. `pandas[pyarrow]` also needs `pyarrow` installed); we -# encode those as tuples here so `is_backend_available` can check all of them. -_BACKEND_REQUIREMENTS: dict[ConstructorName, tuple[str, ...]] = { - ConstructorName.PANDAS: ("pandas",), - ConstructorName.PANDAS_NULLABLE: ("pandas",), - ConstructorName.PANDAS_PYARROW: ("pandas", "pyarrow"), - ConstructorName.PYARROW: ("pyarrow",), - ConstructorName.MODIN: ("modin",), - ConstructorName.MODIN_PYARROW: ("modin", "pyarrow"), - ConstructorName.CUDF: ("cudf",), - ConstructorName.POLARS_EAGER: ("polars",), - ConstructorName.POLARS_LAZY: ("polars",), - ConstructorName.DASK: ("dask",), - ConstructorName.DUCKDB: ("duckdb", "pyarrow"), - ConstructorName.PYSPARK: ("pyspark",), - ConstructorName.PYSPARK_CONNECT: ("pyspark",), - ConstructorName.SQLFRAME: ("sqlframe", "duckdb"), - ConstructorName.IBIS: ("ibis", "duckdb", "pyarrow"), -} - - -def is_backend_available(name: ConstructorName) -> bool: - """Whether all backends required by `name` can be imported in this environment. - - Examples: - >>> from narwhals.testing.constructors import ( - ... ConstructorName, - ... is_backend_available, - ... ) - >>> is_backend_available(ConstructorName.PANDAS) - True - """ - return all(find_spec(pkg) is not None for pkg in _BACKEND_REQUIREMENTS[name]) - - def available_constructors() -> frozenset[ConstructorName]: """Return every [`ConstructorName`][] whose backend is importable. @@ -115,7 +46,7 @@ def available_constructors() -> frozenset[ConstructorName]: >>> ConstructorName.PANDAS in available_constructors() True """ - return frozenset(name for name in ALL_CONSTRUCTORS if is_backend_available(name)) + return frozenset(name for name in ConstructorName if name.is_available) def get_constructor(name: ConstructorName | str) -> ConstructorBase: @@ -157,6 +88,5 @@ def resolve_constructors(names: Iterable[ConstructorName | str]) -> list[Constru "DEFAULT_CONSTRUCTORS", "available_constructors", "get_constructor", - "is_backend_available", "resolve_constructors", ] diff --git a/narwhals/testing/typing.py b/narwhals/testing/typing.py index 3bcec0cc8d..3893b4d709 100644 --- a/narwhals/testing/typing.py +++ b/narwhals/testing/typing.py @@ -1,23 +1,26 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from typing_extensions import TypeAlias - from narwhals._native import NativeLazyFrame - from narwhals.typing import IntoDataFrame + from narwhals.testing.constructors._classes import ( + ConstructorBase, + ConstructorEagerBase, + ConstructorLazyBase, + ) Data: TypeAlias = dict[str, list[Any]] """A column-oriented mapping used as input to a [`Constructor`][].""" -Constructor: TypeAlias = Callable[[Data], "NativeLazyFrame | IntoDataFrame"] -"""Any constructor (eager or lazy) — anything callable that returns a native frame.""" +Constructor: TypeAlias = "ConstructorBase" +"""Any constructor (eager or lazy): callable that returns a native frame.""" -ConstructorEager: TypeAlias = Callable[[Data], "IntoDataFrame"] +ConstructorEager: TypeAlias = "ConstructorEagerBase" """A constructor that returns an eager native dataframe.""" -ConstructorLazy: TypeAlias = Callable[[Data], "NativeLazyFrame"] +ConstructorLazy: TypeAlias = "ConstructorLazyBase" """A constructor that returns a lazy native frame.""" __all__ = ["Constructor", "ConstructorEager", "ConstructorLazy", "Data"] diff --git a/tests/conftest.py b/tests/conftest.py index 3e80bcdff4..dbfef9e335 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,80 +1,37 @@ from __future__ import annotations -import os -import uuid -from copy import deepcopy -from functools import lru_cache from importlib.util import find_spec -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from narwhals._utils import Implementation, generate_temporary_column_name -from tests.utils import ID_PANDAS_LIKE, PANDAS_VERSION, pyspark_session, sqlframe_session +from narwhals._utils import Implementation + +# `narwhals.testing.pytest_plugin` registers itself via the `pytest11` entry point (see pyproject.toml) +# so it auto-loads as soon as Narwhals is installed. +# That plugin is what owns the `--constructors`, `--all-cpu-constructors`, and `--use-external-constructor` +# CLI options as well as parametrising the `constructor*` fixtures. if TYPE_CHECKING: from collections.abc import Sequence - import ibis - import pandas as pd - import polars as pl - import pyarrow as pa - from ibis.backends.duckdb import Backend as IbisDuckDBBackend from typing_extensions import TypeAlias - from narwhals._native import NativeDask, NativeDuckDB, NativePySpark, NativeSQLFrame from narwhals._typing import EagerAllowed - from narwhals.typing import IntoDataFrame, NonNestedDType - from tests.utils import ( - Constructor, - ConstructorEager, - ConstructorLazy, - NestedOrEnumDType, - ) + from narwhals.typing import NonNestedDType + from tests.utils import NestedOrEnumDType Data: TypeAlias = "dict[str, list[Any]]" -MIN_PANDAS_NULLABLE_VERSION = (2,) - -# When testing cudf.pandas in Kaggle, we get an error if we try to run -# python -m cudf.pandas -m pytest --constructors=pandas. This gives us -# a way to run `python -m cudf.pandas -m pytest` and control which constructors -# get tested. -if default_constructors := os.environ.get( - "NARWHALS_DEFAULT_CONSTRUCTORS", None -): # pragma: no cover - DEFAULT_CONSTRUCTORS = default_constructors -else: - DEFAULT_CONSTRUCTORS = ( - "pandas,pandas[pyarrow],polars[eager],pyarrow,duckdb,sqlframe,ibis" - ) +# Narwhals-internal pytest options (not part of the public testing plugin) def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption( "--runslow", action="store_true", default=False, help="run slow tests" ) - parser.addoption( - "--all-cpu-constructors", - action="store_true", - default=False, - help="run tests with all cpu constructors", - ) - parser.addoption( - "--use-external-constructor", - action="store_true", - default=False, - help="run tests with external constructor", - ) - parser.addoption( - "--constructors", - action="store", - default=DEFAULT_CONSTRUCTORS, - type=str, - help="libraries to test", - ) def pytest_configure(config: pytest.Config) -> None: @@ -85,7 +42,6 @@ def pytest_collection_modifyitems( config: pytest.Config, items: Sequence[pytest.Function] ) -> None: # pragma: no cover if config.getoption("--runslow"): - # --runslow given in cli: do not skip slow tests return skip_slow = pytest.mark.skip(reason="need --runslow option to run") for item in items: @@ -93,235 +49,6 @@ def pytest_collection_modifyitems( item.add_marker(skip_slow) -def pandas_constructor(obj: Data) -> pd.DataFrame: - import pandas as pd - - return pd.DataFrame(obj) - - -def pandas_nullable_constructor(obj: Data) -> pd.DataFrame: - import pandas as pd - - return pd.DataFrame(obj).convert_dtypes(dtype_backend="numpy_nullable") - - -def pandas_pyarrow_constructor(obj: Data) -> pd.DataFrame: - pytest.importorskip("pyarrow") - import pandas as pd - - return pd.DataFrame(obj).convert_dtypes(dtype_backend="pyarrow") - - -def modin_constructor(obj: Data) -> IntoDataFrame: # pragma: no cover - import modin.pandas as mpd - import pandas as pd - - df = mpd.DataFrame(pd.DataFrame(obj)) - return cast("IntoDataFrame", df) - - -def modin_pyarrow_constructor(obj: Data) -> IntoDataFrame: # pragma: no cover - import modin.pandas as mpd - import pandas as pd - - df = mpd.DataFrame(pd.DataFrame(obj)).convert_dtypes(dtype_backend="pyarrow") - return cast("IntoDataFrame", df) - - -def cudf_constructor(obj: Data) -> IntoDataFrame: # pragma: no cover - import cudf - - df = cudf.DataFrame(obj) - return cast("IntoDataFrame", df) - - -def polars_eager_constructor(obj: Data) -> pl.DataFrame: - pytest.importorskip("polars") - import polars as pl - - return pl.DataFrame(obj) - - -def polars_lazy_constructor(obj: Data) -> pl.LazyFrame: - import polars as pl - - return pl.LazyFrame(obj) - - -def duckdb_lazy_constructor(obj: dict[str, Any]) -> NativeDuckDB: - pytest.importorskip("duckdb") - pytest.importorskip("pyarrow") - import duckdb - import pyarrow as pa - - duckdb.sql("""set timezone = 'UTC'""") - - _df = pa.table(obj) - return duckdb.sql("select * from _df") - - -def dask_lazy_p1_constructor(obj: Data) -> NativeDask: # pragma: no cover - import dask.dataframe as dd - - return cast("NativeDask", dd.from_dict(obj, npartitions=1)) - - -def dask_lazy_p2_constructor(obj: Data) -> NativeDask: # pragma: no cover - import dask.dataframe as dd - - return cast("NativeDask", dd.from_dict(obj, npartitions=2)) - - -def pyarrow_table_constructor(obj: dict[str, Any]) -> pa.Table: - pytest.importorskip("pyarrow") - import pyarrow as pa - - return pa.table(obj) - - -def pyspark_lazy_constructor() -> Callable[[Data], NativePySpark]: # pragma: no cover - pytest.importorskip("pyspark") - import warnings - from atexit import register - - with warnings.catch_warnings(): - # The spark session seems to trigger a polars warning. - # Polars is imported in the tests, but not used in the spark operations - warnings.filterwarnings( - "ignore", r"Using fork\(\) can cause Polars", category=RuntimeWarning - ) - session = pyspark_session() - - register(session.stop) - - def _constructor(obj: Data) -> NativePySpark: - _obj = deepcopy(obj) - index_col_name = generate_temporary_column_name(n_bytes=8, columns=list(_obj)) - _obj[index_col_name] = list(range(len(_obj[next(iter(_obj))]))) - result = ( - session.createDataFrame([*zip(*_obj.values())], schema=[*_obj.keys()]) - .repartition(2) - .orderBy(index_col_name) - .drop(index_col_name) - ) - return cast("NativePySpark", result) - - return _constructor - - -def sqlframe_pyspark_lazy_constructor(obj: Data) -> NativeSQLFrame: # pragma: no cover - pytest.importorskip("sqlframe") - pytest.importorskip("duckdb") - session = sqlframe_session() - return session.createDataFrame([*zip(*obj.values())], schema=[*obj.keys()]) - - -@lru_cache(maxsize=1) -def _ibis_backend() -> IbisDuckDBBackend: # pragma: no cover - """Cached (singleton) in-memory backend to ensure all tables exist within the same in-memory database.""" - import ibis - - return ibis.duckdb.connect() - - -def ibis_lazy_constructor(obj: Data) -> ibis.Table: # pragma: no cover - pytest.importorskip("ibis") - pytest.importorskip("polars") - import polars as pl - - ldf = pl.LazyFrame(obj) - table_name = str(uuid.uuid4()) - return _ibis_backend().create_table(table_name, ldf) - - -EAGER_CONSTRUCTORS: dict[str, ConstructorEager] = { - "pandas": pandas_constructor, - "pandas[nullable]": pandas_nullable_constructor, - "pandas[pyarrow]": pandas_pyarrow_constructor, - "pyarrow": pyarrow_table_constructor, - "modin": modin_constructor, - "modin[pyarrow]": modin_pyarrow_constructor, - "cudf": cudf_constructor, - "polars[eager]": polars_eager_constructor, -} -LAZY_CONSTRUCTORS: dict[str, ConstructorLazy] = { - "dask": dask_lazy_p2_constructor, - "polars[lazy]": polars_lazy_constructor, - "duckdb": duckdb_lazy_constructor, - "pyspark": pyspark_lazy_constructor, # type: ignore[dict-item] - "sqlframe": sqlframe_pyspark_lazy_constructor, - "ibis": ibis_lazy_constructor, -} -GPU_CONSTRUCTORS: dict[str, ConstructorEager] = {"cudf": cudf_constructor} - - -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - if metafunc.config.getoption("use_external_constructor"): # pragma: no cover - return # let the plugin handle this - if metafunc.config.getoption("all_cpu_constructors"): # pragma: no cover - selected_constructors: list[str] = [ - *iter(EAGER_CONSTRUCTORS.keys()), - *iter(LAZY_CONSTRUCTORS.keys()), - ] - selected_constructors = [ - x - for x in selected_constructors - if x not in GPU_CONSTRUCTORS - and x - not in { - "modin", # too slow - "spark[connect]", # complex local setup; can't run together with local spark - } - ] - else: # pragma: no cover - opt = cast("str", metafunc.config.getoption("constructors")) - selected_constructors = opt.split(",") - - eager_constructors: list[ConstructorEager] = [] - eager_constructors_ids: list[str] = [] - constructors: list[Constructor] = [] - constructors_ids: list[str] = [] - - for constructor in selected_constructors: - if ( - constructor in {"pandas[nullable]", "pandas[pyarrow]"} - and MIN_PANDAS_NULLABLE_VERSION > PANDAS_VERSION - ): - continue # pragma: no cover - - if constructor in EAGER_CONSTRUCTORS: - eager_constructors.append(EAGER_CONSTRUCTORS[constructor]) - eager_constructors_ids.append(constructor) - constructors.append(EAGER_CONSTRUCTORS[constructor]) - elif constructor in {"pyspark", "pyspark[connect]"}: # pragma: no cover - constructors.append(pyspark_lazy_constructor()) - elif constructor in LAZY_CONSTRUCTORS: - constructors.append(LAZY_CONSTRUCTORS[constructor]) - else: # pragma: no cover - msg = f"Expected one of {EAGER_CONSTRUCTORS.keys()} or {LAZY_CONSTRUCTORS.keys()}, got {constructor}" - raise ValueError(msg) - constructors_ids.append(constructor) - - if "constructor_eager" in metafunc.fixturenames: - metafunc.parametrize( - "constructor_eager", eager_constructors, ids=eager_constructors_ids - ) - elif "constructor" in metafunc.fixturenames: - metafunc.parametrize("constructor", constructors, ids=constructors_ids) - elif "constructor_pandas_like" in metafunc.fixturenames: - pandas_like_constructors = [] - pandas_like_constructors_ids = [] - for fn, name in zip(eager_constructors, eager_constructors_ids): - if name in ID_PANDAS_LIKE: - pandas_like_constructors.append(fn) - pandas_like_constructors_ids.append(name) - metafunc.parametrize( - "constructor_pandas_like", - pandas_like_constructors, - ids=pandas_like_constructors_ids, - ) - - TEST_EAGER_BACKENDS: list[EagerAllowed] = [] TEST_EAGER_BACKENDS.extend( (Implementation.POLARS, "polars") if find_spec("polars") is not None else () From 6ba28de018d851807959bf66d38f7740e75a48bf Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 00:39:30 +0200 Subject: [PATCH 03/71] Simplify narwhals codebase --- narwhals/testing/constructors/__init__.py | 81 ++++++++++++++++--- narwhals/testing/constructors/_classes.py | 56 ++++++------- narwhals/testing/constructors/_name.py | 7 +- narwhals/testing/constructors/_registry.py | 92 ---------------------- narwhals/testing/pytest_plugin.py | 7 +- 5 files changed, 104 insertions(+), 139 deletions(-) delete mode 100644 narwhals/testing/constructors/_registry.py diff --git a/narwhals/testing/constructors/__init__.py b/narwhals/testing/constructors/__init__.py index 9bac95a9fd..9b57ff7230 100644 --- a/narwhals/testing/constructors/__init__.py +++ b/narwhals/testing/constructors/__init__.py @@ -1,24 +1,81 @@ from __future__ import annotations -from narwhals.testing.constructors._classes import pyspark_session, sqlframe_session -from narwhals.testing.constructors._name import ConstructorName -from narwhals.testing.constructors._registry import ( - ALL_CONSTRUCTORS, - ALL_CPU_CONSTRUCTORS, - DEFAULT_CONSTRUCTORS, - available_constructors, - get_constructor, - resolve_constructors, +from narwhals.testing.constructors._classes import ( + ConstructorBase, + ConstructorEagerBase, + pyspark_session, + sqlframe_session, ) +from narwhals.testing.constructors._name import ConstructorName -__all__ = [ +__all__ = ( "ALL_CONSTRUCTORS", "ALL_CPU_CONSTRUCTORS", "DEFAULT_CONSTRUCTORS", + "ConstructorBase", + "ConstructorEagerBase", "ConstructorName", "available_constructors", "get_constructor", "pyspark_session", - "resolve_constructors", "sqlframe_session", -] +) + + +ALL_CONSTRUCTORS: dict[ConstructorName, ConstructorBase] = { + name: name.constructor for name in ConstructorName +} + +DEFAULT_CONSTRUCTORS: frozenset[ConstructorName] = frozenset( + { + ConstructorName.PANDAS, + ConstructorName.PANDAS_PYARROW, + ConstructorName.POLARS_EAGER, + ConstructorName.PYARROW, + ConstructorName.DUCKDB, + ConstructorName.SQLFRAME, + ConstructorName.IBIS, + } +) +"""Subset of constructors enabled by default for parametrised tests when the +user does not pass `--constructors` (mirrors the historical Narwhals defaults). +""" + +ALL_CPU_CONSTRUCTORS: frozenset[ConstructorName] = frozenset( + name for name in ConstructorName if not name.needs_gpu +) + + +def available_constructors() -> frozenset[ConstructorName]: + """Return every [`ConstructorName`][] whose backend is importable. + + Examples: + >>> from narwhals.testing.constructors import available_constructors + >>> ConstructorName.PANDAS in available_constructors() + True + """ + return frozenset(name for name in ConstructorName if name.is_available) + + +def get_constructor(name: ConstructorName | str) -> ConstructorBase: + """Return the registered singleton constructor for `name`. + + Arguments: + name: A [`ConstructorName`][] member or its string value + (e.g. `"pandas[pyarrow]"`). + + Raises: + ValueError: If `name` is not a registered constructor identifier. + + Examples: + >>> from narwhals.testing.constructors import get_constructor + >>> get_constructor("pandas") + PandasConstructor() + """ + try: + key = ConstructorName(name) if isinstance(name, str) else name + except ValueError as exc: + valid = sorted(c.value for c in ConstructorName) + msg = f"Unknown constructor {name!r}. Expected one of: {valid}." + raise ValueError(msg) from exc + return ALL_CONSTRUCTORS[key] diff --git a/narwhals/testing/constructors/_classes.py b/narwhals/testing/constructors/_classes.py index 1827cbbee6..e541aa19fb 100644 --- a/narwhals/testing/constructors/_classes.py +++ b/narwhals/testing/constructors/_classes.py @@ -91,6 +91,12 @@ class ConstructorBase(ABC): def __call__(self, obj: Data) -> IntoFrame: """Build a native frame from `obj`.""" + def __str__(self) -> str: + # NOTE: This is a temporary hack + # TODO(Unassigned): Remove once all the `"backend" in str(constructor)` + # statements in the test suite are properly replaced + return _LEGACY_NAME[self.name] + def __repr__(self) -> str: return f"{type(self).__name__}()" @@ -117,7 +123,7 @@ class ConstructorLazyBase(ConstructorBase): def __call__(self, obj: Data) -> IntoLazyFrame: ... -# --- Eager constructors ------------------------------------------------------ +# Eager constructors class PandasConstructor(ConstructorEagerBase): @@ -195,7 +201,7 @@ def __call__(self, obj: Data) -> pl.DataFrame: return pl.DataFrame(obj) -# --- Lazy constructors ------------------------------------------------------- +# Lazy constructors class PolarsLazyConstructor(ConstructorLazyBase): @@ -210,7 +216,7 @@ def __call__(self, obj: Data) -> pl.LazyFrame: class DaskConstructor(ConstructorLazyBase): # pragma: no cover name = ConstructorName.DASK - def __init__(self, npartitions: int = 2) -> None: + def __init__(self, npartitions: int = 1) -> None: self.npartitions = npartitions def __call__(self, obj: Data) -> NativeDask: @@ -283,7 +289,7 @@ def __call__(self, obj: Data) -> ibis.Table: return _ibis_backend().create_table(table_name, table) -_ALL_CONSTRUCTORS: dict[ConstructorName, ConstructorBase] = { +_NAME_TO_CONSTRUCTOR: dict[ConstructorName, ConstructorBase] = { ConstructorName.PANDAS: PandasConstructor(), ConstructorName.PANDAS_NULLABLE: PandasNullableConstructor(), ConstructorName.PANDAS_PYARROW: PandasPyArrowConstructor(), @@ -319,26 +325,22 @@ def __call__(self, obj: Data) -> ibis.Table: ConstructorName.IBIS: ("ibis", "duckdb", "pyarrow"), } - -__all__ = [ - "ConstructorBase", - "ConstructorEagerBase", - "ConstructorLazyBase", - "CudfConstructor", - "DaskConstructor", - "DuckDBConstructor", - "IbisConstructor", - "ModinConstructor", - "ModinPyArrowConstructor", - "PandasConstructor", - "PandasNullableConstructor", - "PandasPyArrowConstructor", - "PolarsEagerConstructor", - "PolarsLazyConstructor", - "PyArrowConstructor", - "PySparkConnectConstructor", - "PySparkConstructor", - "SQLFrameConstructor", - "pyspark_session", - "sqlframe_session", -] +# TODO(Unassigned): Remove once all the `"backend" in str(constructor)` +# statements in the test suite are properly replaced +_LEGACY_NAME: dict[ConstructorName, str] = { + ConstructorName.PANDAS: "pandas_constructor", + ConstructorName.PANDAS_NULLABLE: "pandas_nullable_constructor", + ConstructorName.PANDAS_PYARROW: "pandas_pyarrow_constructor", + ConstructorName.PYARROW: "pyarrow_table_constructor", + ConstructorName.MODIN: "modin_constructor", + ConstructorName.MODIN_PYARROW: "modin_pyarrow_constructor", + ConstructorName.CUDF: "cudf_constructor", + ConstructorName.POLARS_EAGER: "polars_eager_constructor", + ConstructorName.POLARS_LAZY: "polars_lazy_constructor", + ConstructorName.DASK: "dask_lazy_p1_constructor", + ConstructorName.DUCKDB: "duckdb_lazy_constructor", + ConstructorName.PYSPARK: "pyspark_lazy_constructor", + ConstructorName.PYSPARK_CONNECT: "pyspark_lazy_constructor", + ConstructorName.SQLFRAME: "sqlframe_pyspark_lazy_constructor", + ConstructorName.IBIS: "ibis_lazy_constructor", +} diff --git a/narwhals/testing/constructors/_name.py b/narwhals/testing/constructors/_name.py index e1fd9c8442..f71f184e62 100644 --- a/narwhals/testing/constructors/_name.py +++ b/narwhals/testing/constructors/_name.py @@ -181,15 +181,12 @@ def from_pytest_request(cls, request: pytest.FixtureRequest) -> ConstructorName: @property def constructor(self) -> ConstructorBase: - from narwhals.testing.constructors._classes import _ALL_CONSTRUCTORS + from narwhals.testing.constructors._classes import _NAME_TO_CONSTRUCTOR - return _ALL_CONSTRUCTORS[self] + return _NAME_TO_CONSTRUCTOR[self] @property def is_available(self) -> bool: from narwhals.testing.constructors._classes import _BACKEND_REQUIREMENTS return is_backend_available(*_BACKEND_REQUIREMENTS[self]) - - -__all__ = ["ConstructorName"] diff --git a/narwhals/testing/constructors/_registry.py b/narwhals/testing/constructors/_registry.py deleted file mode 100644 index ac5b68a98e..0000000000 --- a/narwhals/testing/constructors/_registry.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Registry of constructors that ship with `narwhals.testing`.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from narwhals.testing.constructors._name import ConstructorName - -if TYPE_CHECKING: - from collections.abc import Iterable - - from narwhals.testing.constructors._classes import ConstructorBase - - -# Singleton instance per backend. Users that need a non-default parametrisation -# (e.g. `DaskConstructor(npartitions=1)`) can instantiate the class directly. -ALL_CONSTRUCTORS: dict[ConstructorName, ConstructorBase] = { - name: name.constructor for name in ConstructorName -} - -DEFAULT_CONSTRUCTORS: frozenset[ConstructorName] = frozenset( - { - ConstructorName.PANDAS, - ConstructorName.PANDAS_PYARROW, - ConstructorName.POLARS_EAGER, - ConstructorName.PYARROW, - ConstructorName.DUCKDB, - ConstructorName.SQLFRAME, - ConstructorName.IBIS, - } -) -"""Subset of constructors enabled by default for parametrised tests when the -user does not pass `--constructors` (mirrors the historical Narwhals defaults). -""" - -ALL_CPU_CONSTRUCTORS: frozenset[ConstructorName] = frozenset( - name for name in ConstructorName if not name.needs_gpu -) - - -def available_constructors() -> frozenset[ConstructorName]: - """Return every [`ConstructorName`][] whose backend is importable. - - Examples: - >>> from narwhals.testing.constructors import available_constructors - >>> ConstructorName.PANDAS in available_constructors() - True - """ - return frozenset(name for name in ConstructorName if name.is_available) - - -def get_constructor(name: ConstructorName | str) -> ConstructorBase: - """Return the registered singleton constructor for `name`. - - Arguments: - name: A [`ConstructorName`][] member or its string value - (e.g. `"pandas[pyarrow]"`). - - Raises: - ValueError: If `name` is not a registered constructor identifier. - - Examples: - >>> from narwhals.testing.constructors import get_constructor - >>> get_constructor("pandas") - PandasConstructor() - """ - try: - key = ConstructorName(name) if isinstance(name, str) else name - except ValueError as exc: - valid = sorted(c.value for c in ConstructorName) - msg = f"Unknown constructor {name!r}. Expected one of: {valid}." - raise ValueError(msg) from exc - return ALL_CONSTRUCTORS[key] - - -def resolve_constructors(names: Iterable[ConstructorName | str]) -> list[ConstructorBase]: - """Resolve an iterable of names / identifiers into a list of constructor instances. - - Order is preserved; duplicates are kept (so the same constructor can be - parametrised multiple times if explicitly requested). - """ - return [get_constructor(n) for n in names] - - -__all__ = [ - "ALL_CONSTRUCTORS", - "ALL_CPU_CONSTRUCTORS", - "DEFAULT_CONSTRUCTORS", - "available_constructors", - "get_constructor", - "resolve_constructors", -] diff --git a/narwhals/testing/pytest_plugin.py b/narwhals/testing/pytest_plugin.py index cf8e7d326a..256b21d382 100644 --- a/narwhals/testing/pytest_plugin.py +++ b/narwhals/testing/pytest_plugin.py @@ -4,11 +4,12 @@ from typing import TYPE_CHECKING, cast from narwhals._utils import parse_version -from narwhals.testing.constructors._classes import ConstructorBase, ConstructorEagerBase -from narwhals.testing.constructors._name import ConstructorName -from narwhals.testing.constructors._registry import ( +from narwhals.testing.constructors import ( ALL_CPU_CONSTRUCTORS, DEFAULT_CONSTRUCTORS, + ConstructorBase, + ConstructorEagerBase, + ConstructorName, available_constructors, get_constructor, ) From 7e2c50d8bf2205e27d04addb4199f64823af083f Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 00:39:58 +0200 Subject: [PATCH 04/71] bulk tests change with minimal impact --- .../is_narwhals_dataframe_test.py | 2 +- .../is_narwhals_lazyframe_test.py | 4 +- tests/dependencies/is_narwhals_series_test.py | 2 +- tests/dtypes/dtypes_test.py | 12 +-- tests/expr_and_series/abs_test.py | 7 +- tests/expr_and_series/all_horizontal_test.py | 7 +- tests/expr_and_series/any_all_test.py | 7 +- tests/expr_and_series/any_horizontal_test.py | 6 +- tests/expr_and_series/any_value_test.py | 2 +- tests/expr_and_series/arithmetic_test.py | 14 ++- tests/expr_and_series/binary_test.py | 7 +- tests/expr_and_series/cast_test.py | 3 +- .../cat/get_categories_test.py | 7 +- tests/expr_and_series/clip_test.py | 7 +- tests/expr_and_series/coalesce_test.py | 7 +- tests/expr_and_series/concat_str_test.py | 7 +- tests/expr_and_series/corr_test.py | 6 +- tests/expr_and_series/cos_test.py | 9 +- tests/expr_and_series/count_test.py | 7 +- tests/expr_and_series/cum_count_test.py | 13 ++- tests/expr_and_series/cum_max_test.py | 14 ++- tests/expr_and_series/cum_min_test.py | 7 +- tests/expr_and_series/cum_prod_test.py | 14 ++- tests/expr_and_series/cum_sum_test.py | 7 +- tests/expr_and_series/diff_test.py | 13 ++- .../expr_and_series/division_by_zero_test.py | 7 +- tests/expr_and_series/double_selected_test.py | 7 +- tests/expr_and_series/double_test.py | 7 +- tests/expr_and_series/drop_nulls_test.py | 7 +- .../dt/convert_time_zone_test.py | 13 +-- .../dt/datetime_attributes_test.py | 4 +- .../dt/datetime_duration_test.py | 7 +- tests/expr_and_series/dt/offset_by_test.py | 13 ++- .../dt/replace_time_zone_test.py | 12 +-- tests/expr_and_series/dt/timestamp_test.py | 4 +- tests/expr_and_series/dt/to_string_test.py | 7 +- tests/expr_and_series/dt/truncate_test.py | 6 +- tests/expr_and_series/ewm_test.py | 7 +- tests/expr_and_series/exclude_test.py | 2 +- tests/expr_and_series/exp_test.py | 7 +- tests/expr_and_series/fill_nan_test.py | 29 +++--- tests/expr_and_series/fill_null_test.py | 10 +-- tests/expr_and_series/filter_test.py | 7 +- tests/expr_and_series/first_last_test.py | 11 +-- tests/expr_and_series/floor_ceil_test.py | 7 +- tests/expr_and_series/format_test.py | 7 +- .../horizontal_broadcasts_test.py | 7 +- tests/expr_and_series/is_between_test.py | 7 +- tests/expr_and_series/is_close_test.py | 37 +++----- tests/expr_and_series/is_duplicated_test.py | 7 +- tests/expr_and_series/is_finite_test.py | 29 +++--- .../expr_and_series/is_first_distinct_test.py | 13 ++- tests/expr_and_series/is_in_test.py | 6 +- .../expr_and_series/is_last_distinct_test.py | 13 ++- tests/expr_and_series/is_nan_test.py | 31 +++---- tests/expr_and_series/is_null_test.py | 7 +- tests/expr_and_series/is_unique_test.py | 7 +- tests/expr_and_series/kurtosis_test.py | 7 +- tests/expr_and_series/len_test.py | 7 +- tests/expr_and_series/list/contains_test.py | 3 +- tests/expr_and_series/list/get_test.py | 12 +-- tests/expr_and_series/list/len_test.py | 7 +- tests/expr_and_series/list/max_test.py | 2 +- tests/expr_and_series/list/mean_test.py | 2 +- tests/expr_and_series/list/median_test.py | 2 +- tests/expr_and_series/list/min_test.py | 2 +- tests/expr_and_series/list/sort_test.py | 2 +- tests/expr_and_series/list/sum_test.py | 3 +- tests/expr_and_series/list/unique_test.py | 2 +- tests/expr_and_series/lit_test.py | 2 +- tests/expr_and_series/log_test.py | 6 +- tests/expr_and_series/map_batches_test.py | 9 +- tests/expr_and_series/max_horizontal_test.py | 7 +- tests/expr_and_series/max_test.py | 7 +- tests/expr_and_series/mean_horizontal_test.py | 7 +- tests/expr_and_series/mean_test.py | 7 +- tests/expr_and_series/median_test.py | 6 +- tests/expr_and_series/min_horizontal_test.py | 7 +- tests/expr_and_series/min_test.py | 7 +- tests/expr_and_series/mode_test.py | 7 +- tests/expr_and_series/n_unique_test.py | 7 +- tests/expr_and_series/name/keep_test.py | 7 +- tests/expr_and_series/name/map_test.py | 7 +- tests/expr_and_series/name/prefix_test.py | 7 +- tests/expr_and_series/name/suffix_test.py | 7 +- .../expr_and_series/name/to_lowercase_test.py | 7 +- .../expr_and_series/name/to_uppercase_test.py | 7 +- tests/expr_and_series/nth_test.py | 6 +- tests/expr_and_series/null_count_test.py | 7 +- tests/expr_and_series/operators_test.py | 7 +- .../order_dependent_lazy_test.py | 2 +- tests/expr_and_series/over_pushdown_test.py | 7 +- tests/expr_and_series/over_test.py | 13 ++- tests/expr_and_series/pipe_test.py | 7 +- tests/expr_and_series/quantile_test.py | 9 +- tests/expr_and_series/rank_test.py | 7 +- tests/expr_and_series/reduction_test.py | 7 +- tests/expr_and_series/replace_strict_test.py | 10 +-- tests/expr_and_series/rolling_mean_test.py | 13 ++- tests/expr_and_series/rolling_std_test.py | 14 ++- tests/expr_and_series/rolling_sum_test.py | 14 ++- tests/expr_and_series/rolling_var_test.py | 14 ++- tests/expr_and_series/round_test.py | 7 +- tests/expr_and_series/sample_test.py | 7 +- tests/expr_and_series/shift_test.py | 13 ++- tests/expr_and_series/sin_test.py | 9 +- tests/expr_and_series/skew_test.py | 7 +- tests/expr_and_series/sqrt_test.py | 9 +- tests/expr_and_series/std_test.py | 7 +- tests/expr_and_series/str/contains_test.py | 7 +- tests/expr_and_series/str/head_test.py | 7 +- tests/expr_and_series/str/len_chars_test.py | 7 +- tests/expr_and_series/str/pad_test.py | 7 +- tests/expr_and_series/str/replace_test.py | 7 +- tests/expr_and_series/str/slice_test.py | 7 +- tests/expr_and_series/str/split_test.py | 14 +-- .../str/starts_with_ends_with_test.py | 8 +- tests/expr_and_series/str/strip_chars_test.py | 7 +- tests/expr_and_series/str/tail_test.py | 7 +- tests/expr_and_series/str/to_date_test.py | 2 +- tests/expr_and_series/str/to_datetime_test.py | 2 +- .../expr_and_series/str/to_titlecase_test.py | 7 +- .../str/to_uppercase_to_lowercase_test.py | 7 +- tests/expr_and_series/str/zfill_test.py | 7 +- tests/expr_and_series/struct_/field_test.py | 7 +- tests/expr_and_series/struct_test.py | 14 ++- tests/expr_and_series/sum_horizontal_test.py | 7 +- tests/expr_and_series/sum_test.py | 7 +- tests/expr_and_series/unary_test.py | 6 +- tests/expr_and_series/unique_test.py | 6 +- tests/expr_and_series/var_test.py | 7 +- tests/expr_and_series/when_test.py | 9 +- tests/expression_parsing_test.py | 7 +- tests/frame/add_test.py | 7 +- tests/frame/array_dunder_test.py | 13 ++- tests/frame/clone_test.py | 7 +- tests/frame/collect_test.py | 3 +- tests/frame/columns_test.py | 2 +- tests/frame/concat_test.py | 4 +- tests/frame/double_test.py | 7 +- tests/frame/drop_nulls_test.py | 7 +- tests/frame/drop_test.py | 2 +- tests/frame/eq_test.py | 2 +- tests/frame/estimated_size_test.py | 2 +- tests/frame/explode_test.py | 4 +- tests/frame/filter_test.py | 7 +- tests/frame/from_dict_test.py | 3 +- tests/frame/get_column_test.py | 7 +- tests/frame/getitem_test.py | 3 +- tests/frame/group_by_test.py | 3 +- tests/frame/head_test.py | 7 +- tests/frame/invalid_test.py | 5 +- tests/frame/is_duplicated_test.py | 7 +- tests/frame/is_empty_test.py | 2 +- tests/frame/is_unique_test.py | 7 +- tests/frame/item_test.py | 7 +- tests/frame/join_test.py | 9 +- tests/frame/lazy_test.py | 11 +-- tests/frame/len_test.py | 2 +- tests/frame/null_count_test.py | 7 +- tests/frame/pipe_test.py | 7 +- tests/frame/pivot_test.py | 7 +- tests/frame/rename_test.py | 7 +- tests/frame/row_test.py | 2 +- tests/frame/rows_test.py | 2 +- tests/frame/sample_test.py | 2 +- tests/frame/schema_test.py | 6 +- tests/frame/select_test.py | 14 ++- tests/frame/shape_test.py | 2 +- tests/frame/sink_parquet_test.py | 2 +- tests/frame/sort_test.py | 7 +- tests/frame/tail_test.py | 7 +- tests/frame/to_arrow_test.py | 2 +- tests/frame/to_dict_test.py | 7 +- tests/frame/to_native_test.py | 2 +- tests/frame/to_numpy_test.py | 2 +- tests/frame/to_pandas_test.py | 4 +- tests/frame/to_polars_test.py | 2 +- tests/frame/top_k_test.py | 7 +- tests/frame/unique_test.py | 7 +- tests/frame/unpivot_test.py | 3 +- tests/frame/with_columns_sequence_test.py | 7 +- tests/frame/with_columns_test.py | 13 ++- tests/frame/with_row_index_test.py | 12 +-- tests/frame/write_csv_test.py | 4 +- tests/frame/write_parquet_test.py | 2 +- tests/from_dict_test.py | 3 +- tests/from_numpy_test.py | 3 +- tests/hypothesis/getitem_test.py | 10 ++- tests/ibis_test.py | 2 +- tests/joblib_test.py | 7 +- tests/modern_polars/ewm_mean_test.py | 7 +- tests/modern_polars/filter_test.py | 6 +- tests/modern_polars/method_chaining_2_test.py | 6 +- tests/modern_polars/method_chaining_test.py | 11 +-- tests/modern_polars/performance_test.py | 7 +- tests/modern_polars/pivot_test.py | 6 +- tests/modern_polars/unpivot_test.py | 6 +- tests/namespace_test.py | 2 +- tests/new_series_test.py | 7 +- tests/read_scan_test.py | 10 +-- tests/selectors_test.py | 6 +- tests/series_only/__contains___test.py | 2 +- tests/series_only/__iter___test.py | 2 +- tests/series_only/alias_rename_test.py | 7 +- tests/series_only/arg_max_test.py | 7 +- tests/series_only/arg_min_test.py | 7 +- tests/series_only/arg_true_test.py | 7 +- tests/series_only/array_dunder_test.py | 12 +-- tests/series_only/cast_test.py | 2 +- tests/series_only/gather_every_test.py | 7 +- tests/series_only/getitem_test.py | 2 +- tests/series_only/head_test.py | 7 +- tests/series_only/hist_test.py | 4 +- tests/series_only/is_empty_test.py | 2 +- .../is_ordered_categorical_test.py | 2 +- tests/series_only/is_sorted_test.py | 7 +- tests/series_only/item_test.py | 6 +- tests/series_only/scatter_test.py | 4 +- tests/series_only/shape_test.py | 2 +- tests/series_only/sort_test.py | 2 +- tests/series_only/tail_test.py | 7 +- tests/series_only/to_arrow_test.py | 2 +- tests/series_only/to_dummy_test.py | 7 +- tests/series_only/to_frame_test.py | 7 +- tests/series_only/to_list_test.py | 7 +- tests/series_only/to_native_test.py | 2 +- tests/series_only/to_numpy_test.py | 2 +- tests/series_only/to_pandas_test.py | 2 +- tests/series_only/to_polars_test.py | 2 +- tests/series_only/value_counts_test.py | 7 +- tests/series_only/zip_with_test.py | 7 +- tests/testing/assert_frame_equal_test.py | 2 +- tests/testing/assert_series_equal_test.py | 2 +- tests/testing/constructors_test.py | 90 +++++++++++++++++++ tests/translate/from_native_test.py | 12 ++- tests/translate/get_native_namespace_test.py | 4 +- tests/translate/to_native_test.py | 2 +- tests/utils.py | 43 +-------- tests/v1_test.py | 4 +- tests/v2_test.py | 3 +- 241 files changed, 1131 insertions(+), 636 deletions(-) create mode 100644 tests/testing/constructors_test.py diff --git a/tests/dependencies/is_narwhals_dataframe_test.py b/tests/dependencies/is_narwhals_dataframe_test.py index aeedf15981..5269f6899b 100644 --- a/tests/dependencies/is_narwhals_dataframe_test.py +++ b/tests/dependencies/is_narwhals_dataframe_test.py @@ -6,7 +6,7 @@ from narwhals.stable.v1.dependencies import is_narwhals_dataframe if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager def test_is_narwhals_dataframe(constructor_eager: ConstructorEager) -> None: diff --git a/tests/dependencies/is_narwhals_lazyframe_test.py b/tests/dependencies/is_narwhals_lazyframe_test.py index 0e4c6e1bd9..3d78fbad7e 100644 --- a/tests/dependencies/is_narwhals_lazyframe_test.py +++ b/tests/dependencies/is_narwhals_lazyframe_test.py @@ -4,10 +4,10 @@ import narwhals as nw from narwhals.stable.v1.dependencies import is_narwhals_lazyframe -from tests.utils import Constructor +from narwhals.testing.typing import Constructor if TYPE_CHECKING: - from tests.utils import Constructor + from narwhals.testing.typing import Constructor def test_is_narwhals_lazyframe(constructor: Constructor) -> None: diff --git a/tests/dependencies/is_narwhals_series_test.py b/tests/dependencies/is_narwhals_series_test.py index 659696d108..92678f03e4 100644 --- a/tests/dependencies/is_narwhals_series_test.py +++ b/tests/dependencies/is_narwhals_series_test.py @@ -6,7 +6,7 @@ from narwhals.stable.v1.dependencies import is_narwhals_series if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager def test_is_narwhals_series(constructor_eager: ConstructorEager) -> None: diff --git a/tests/dtypes/dtypes_test.py b/tests/dtypes/dtypes_test.py index 33fa61ac08..64c4827043 100644 --- a/tests/dtypes/dtypes_test.py +++ b/tests/dtypes/dtypes_test.py @@ -9,19 +9,15 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError, PerformanceWarning -from tests.utils import ( - PANDAS_VERSION, - POLARS_VERSION, - PYARROW_VERSION, - assert_equal_hash, - pyspark_session, -) +from narwhals.testing.constructors import pyspark_session +from tests.utils import PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, assert_equal_hash if TYPE_CHECKING: from collections.abc import Iterable + from narwhals.testing.typing import Constructor from narwhals.typing import IntoFrame, IntoSeries, NonNestedDType - from tests.utils import Constructor, ConstructorPandasLike, NestedOrEnumDType + from tests.utils import ConstructorPandasLike, NestedOrEnumDType @pytest.mark.parametrize("time_unit", ["us", "ns", "ms"]) diff --git a/tests/expr_and_series/abs_test.py b/tests/expr_and_series/abs_test.py index 547ff9d8dc..35a6c26c0e 100644 --- a/tests/expr_and_series/abs_test.py +++ b/tests/expr_and_series/abs_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_abs(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/all_horizontal_test.py b/tests/expr_and_series/all_horizontal_test.py index d980b3def3..1ac6e98cb7 100644 --- a/tests/expr_and_series/all_horizontal_test.py +++ b/tests/expr_and_series/all_horizontal_test.py @@ -1,12 +1,15 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_allh(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/any_all_test.py b/tests/expr_and_series/any_all_test.py index 6b6cc6e35f..e08d03c3cb 100644 --- a/tests/expr_and_series/any_all_test.py +++ b/tests/expr_and_series/any_all_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_any_all(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/any_horizontal_test.py b/tests/expr_and_series/any_horizontal_test.py index 04f0cba76c..886efe092d 100644 --- a/tests/expr_and_series/any_horizontal_test.py +++ b/tests/expr_and_series/any_horizontal_test.py @@ -1,11 +1,15 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise +from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_anyh(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/any_value_test.py b/tests/expr_and_series/any_value_test.py index e665fbd03c..4c286943f5 100644 --- a/tests/expr_and_series/any_value_test.py +++ b/tests/expr_and_series/any_value_test.py @@ -8,7 +8,7 @@ from tests.utils import DUCKDB_VERSION, PYARROW_VERSION, assert_equal_data if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager data = { "a": [1, 1, 1, 2, 2, 3], diff --git a/tests/expr_and_series/arithmetic_test.py b/tests/expr_and_series/arithmetic_test.py index af0c464e5b..47c2d6ad6b 100644 --- a/tests/expr_and_series/arithmetic_test.py +++ b/tests/expr_and_series/arithmetic_test.py @@ -1,20 +1,16 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import hypothesis.strategies as st import pytest from hypothesis import assume, given import narwhals as nw -from tests.utils import ( - DASK_VERSION, - DUCKDB_VERSION, - PANDAS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DASK_VERSION, DUCKDB_VERSION, PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager @pytest.mark.parametrize( diff --git a/tests/expr_and_series/binary_test.py b/tests/expr_and_series/binary_test.py index 6140ead120..cd6484b904 100644 --- a/tests/expr_and_series/binary_test.py +++ b/tests/expr_and_series/binary_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import DASK_VERSION, Constructor, assert_equal_data +from tests.utils import DASK_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_expr_binary(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/cast_test.py b/tests/expr_and_series/cast_test.py index 90282ea596..ad3015c0cd 100644 --- a/tests/expr_and_series/cast_test.py +++ b/tests/expr_and_series/cast_test.py @@ -9,8 +9,6 @@ from tests.utils import ( PANDAS_VERSION, PYARROW_VERSION, - Constructor, - ConstructorEager, assert_equal_data, is_pyarrow_windows_no_tzdata, time_unit_compat, @@ -19,6 +17,7 @@ if TYPE_CHECKING: from collections.abc import Mapping + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import NonNestedDType DATA = { diff --git a/tests/expr_and_series/cat/get_categories_test.py b/tests/expr_and_series/cat/get_categories_test.py index 6f984ff7ce..ed5520a91b 100644 --- a/tests/expr_and_series/cat/get_categories_test.py +++ b/tests/expr_and_series/cat/get_categories_test.py @@ -1,10 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import PYARROW_VERSION, ConstructorEager, assert_equal_data +from tests.utils import PYARROW_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = {"a": ["one", "two", "two"]} diff --git a/tests/expr_and_series/clip_test.py b/tests/expr_and_series/clip_test.py index 382133f23c..f2f4494b42 100644 --- a/tests/expr_and_series/clip_test.py +++ b/tests/expr_and_series/clip_test.py @@ -1,10 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw from narwhals.exceptions import MultiOutputExpressionError -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager @pytest.mark.parametrize( diff --git a/tests/expr_and_series/coalesce_test.py b/tests/expr_and_series/coalesce_test.py index 5311b39c85..f9cd2205dc 100644 --- a/tests/expr_and_series/coalesce_test.py +++ b/tests/expr_and_series/coalesce_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_coalesce_numeric(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/concat_str_test.py b/tests/expr_and_series/concat_str_test.py index 91e359c8fb..5ea68e010b 100644 --- a/tests/expr_and_series/concat_str_test.py +++ b/tests/expr_and_series/concat_str_test.py @@ -1,15 +1,18 @@ from __future__ import annotations -from typing import Any, Callable +from typing import TYPE_CHECKING, Any, Callable import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, POLARS_VERSION, Constructor, assert_equal_data +from tests.utils import PANDAS_VERSION, POLARS_VERSION, assert_equal_data pytest.importorskip("pyarrow") import pyarrow as pa +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor + data = {"a": [1, 2, 3], "b": ["dogs", "cats", None], "c": ["play", "swim", "walk"]} diff --git a/tests/expr_and_series/corr_test.py b/tests/expr_and_series/corr_test.py index 9152df69c8..3c70b2623b 100644 --- a/tests/expr_and_series/corr_test.py +++ b/tests/expr_and_series/corr_test.py @@ -1,11 +1,15 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise +from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 3, 3], "b": [1, 2, 3], "c": [1, None, 1]} diff --git a/tests/expr_and_series/cos_test.py b/tests/expr_and_series/cos_test.py index 4aa936d9bf..ba96a9a083 100644 --- a/tests/expr_and_series/cos_test.py +++ b/tests/expr_and_series/cos_test.py @@ -6,15 +6,10 @@ import pytest import narwhals as nw -from tests.utils import ( - PANDAS_VERSION, - PYARROW_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import PANDAS_VERSION, PYARROW_VERSION, assert_equal_data if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import DTypeBackend data = {"a": [-pi, -pi / 2, 0.0, pi / 2, pi]} diff --git a/tests/expr_and_series/count_test.py b/tests/expr_and_series/count_test.py index c3f34ee132..8ba620a28f 100644 --- a/tests/expr_and_series/count_test.py +++ b/tests/expr_and_series/count_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_count(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/cum_count_test.py b/tests/expr_and_series/cum_count_test.py index f25c27f1c3..16af5089be 100644 --- a/tests/expr_and_series/cum_count_test.py +++ b/tests/expr_and_series/cum_count_test.py @@ -1,15 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": ["x", "y", None, "z"]} diff --git a/tests/expr_and_series/cum_max_test.py b/tests/expr_and_series/cum_max_test.py index 82cf5ba179..65ccb49392 100644 --- a/tests/expr_and_series/cum_max_test.py +++ b/tests/expr_and_series/cum_max_test.py @@ -1,16 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - PANDAS_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 3, None, 2]} diff --git a/tests/expr_and_series/cum_min_test.py b/tests/expr_and_series/cum_min_test.py index 43fc1f5b81..1a5194ed71 100644 --- a/tests/expr_and_series/cum_min_test.py +++ b/tests/expr_and_series/cum_min_test.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw @@ -8,12 +10,13 @@ PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, - Constructor, - ConstructorEager, assert_equal_data, is_windows, ) +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager + data = {"a": [3, 1, None, 2]} expected = {"cum_min": [3, 1, None, 1], "reverse_cum_min": [1, 1, None, 2]} diff --git a/tests/expr_and_series/cum_prod_test.py b/tests/expr_and_series/cum_prod_test.py index 778c48bc67..397cb7f7fb 100644 --- a/tests/expr_and_series/cum_prod_test.py +++ b/tests/expr_and_series/cum_prod_test.py @@ -1,16 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - PANDAS_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 2, None, 3]} diff --git a/tests/expr_and_series/cum_sum_test.py b/tests/expr_and_series/cum_sum_test.py index f3f0f780db..a68969a949 100644 --- a/tests/expr_and_series/cum_sum_test.py +++ b/tests/expr_and_series/cum_sum_test.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw @@ -7,12 +9,13 @@ DUCKDB_VERSION, POLARS_VERSION, PYARROW_VERSION, - Constructor, - ConstructorEager, assert_equal_data, is_windows, ) +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager + data = {"arg entina": [1, 2, None, 4]} expected = {"cum_sum": [1, 3, None, 7], "reverse_cum_sum": [7, 6, None, 4]} diff --git a/tests/expr_and_series/diff_test.py b/tests/expr_and_series/diff_test.py index dc7440541e..02e897b276 100644 --- a/tests/expr_and_series/diff_test.py +++ b/tests/expr_and_series/diff_test.py @@ -1,15 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"i": [0, 1, 2, 3, 4], "b": [1, 2, 3, 5, 3], "c": [5, 4, 3, 2, 1]} diff --git a/tests/expr_and_series/division_by_zero_test.py b/tests/expr_and_series/division_by_zero_test.py index 6e8313b522..cd3d86c29e 100644 --- a/tests/expr_and_series/division_by_zero_test.py +++ b/tests/expr_and_series/division_by_zero_test.py @@ -1,12 +1,15 @@ from __future__ import annotations -from typing import Any, Callable +from typing import TYPE_CHECKING, Any, Callable import pytest import narwhals as nw from narwhals._utils import zip_strict -from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data: dict[str, list[float]] = { "int": [-2, 0, 2], diff --git a/tests/expr_and_series/double_selected_test.py b/tests/expr_and_series/double_selected_test.py index 48b76222ec..ba5e9851d3 100644 --- a/tests/expr_and_series/double_selected_test.py +++ b/tests/expr_and_series/double_selected_test.py @@ -1,10 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw from narwhals.exceptions import MultiOutputExpressionError -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_double_selected(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/double_test.py b/tests/expr_and_series/double_test.py index de6a1afb09..7064b879c9 100644 --- a/tests/expr_and_series/double_test.py +++ b/tests/expr_and_series/double_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_double(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/drop_nulls_test.py b/tests/expr_and_series/drop_nulls_test.py index d067552eb6..1115137a27 100644 --- a/tests/expr_and_series/drop_nulls_test.py +++ b/tests/expr_and_series/drop_nulls_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_drop_nulls(constructor_eager: ConstructorEager) -> None: diff --git a/tests/expr_and_series/dt/convert_time_zone_test.py b/tests/expr_and_series/dt/convert_time_zone_test.py index 65d1a6e3b6..5145ddc0ca 100644 --- a/tests/expr_and_series/dt/convert_time_zone_test.py +++ b/tests/expr_and_series/dt/convert_time_zone_test.py @@ -7,17 +7,12 @@ import pytest import narwhals as nw -from tests.utils import ( - PANDAS_VERSION, - POLARS_VERSION, - Constructor, - assert_equal_data, - is_windows, - pyspark_session, -) +from narwhals.testing.constructors import pyspark_session +from narwhals.testing.typing import ConstructorEager +from tests.utils import PANDAS_VERSION, POLARS_VERSION, assert_equal_data, is_windows if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager def test_convert_time_zone( diff --git a/tests/expr_and_series/dt/datetime_attributes_test.py b/tests/expr_and_series/dt/datetime_attributes_test.py index c7bf55e7c0..a1f125f54a 100644 --- a/tests/expr_and_series/dt/datetime_attributes_test.py +++ b/tests/expr_and_series/dt/datetime_attributes_test.py @@ -6,11 +6,13 @@ import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data if TYPE_CHECKING: import dask.dataframe as dd + from narwhals.testing.typing import Constructor, ConstructorEager + data = { "a": [datetime(2021, 3, 1, 12, 34, 56, 49000), datetime(2020, 1, 2, 2, 4, 14, 715000)] } diff --git a/tests/expr_and_series/dt/datetime_duration_test.py b/tests/expr_and_series/dt/datetime_duration_test.py index b84ecfa66e..637a6a95fb 100644 --- a/tests/expr_and_series/dt/datetime_duration_test.py +++ b/tests/expr_and_series/dt/datetime_duration_test.py @@ -1,12 +1,15 @@ from __future__ import annotations from datetime import timedelta -from typing import Literal +from typing import TYPE_CHECKING, Literal import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = { "a": [None, timedelta(minutes=1, seconds=1, milliseconds=1, microseconds=1)], diff --git a/tests/expr_and_series/dt/offset_by_test.py b/tests/expr_and_series/dt/offset_by_test.py index 9085edb185..495b0bab4d 100644 --- a/tests/expr_and_series/dt/offset_by_test.py +++ b/tests/expr_and_series/dt/offset_by_test.py @@ -1,18 +1,15 @@ from __future__ import annotations from datetime import date, datetime, timezone +from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - PANDAS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, - is_windows, -) +from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, assert_equal_data, is_windows + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = { "a": [datetime(2021, 3, 1, 12, 34, 56, 49012), datetime(2020, 1, 2, 2, 4, 14, 715123)] diff --git a/tests/expr_and_series/dt/replace_time_zone_test.py b/tests/expr_and_series/dt/replace_time_zone_test.py index 1c9dff7d59..94f61b4efe 100644 --- a/tests/expr_and_series/dt/replace_time_zone_test.py +++ b/tests/expr_and_series/dt/replace_time_zone_test.py @@ -7,16 +7,12 @@ import pytest import narwhals as nw -from tests.utils import ( - PANDAS_VERSION, - Constructor, - assert_equal_data, - is_windows, - pyspark_session, -) +from narwhals.testing.constructors import pyspark_session +from narwhals.testing.typing import ConstructorEager +from tests.utils import PANDAS_VERSION, assert_equal_data, is_windows if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager def test_replace_time_zone( diff --git a/tests/expr_and_series/dt/timestamp_test.py b/tests/expr_and_series/dt/timestamp_test.py index e199fcac76..b216989fcc 100644 --- a/tests/expr_and_series/dt/timestamp_test.py +++ b/tests/expr_and_series/dt/timestamp_test.py @@ -11,16 +11,16 @@ from tests.utils import ( PANDAS_VERSION, POLARS_VERSION, - Constructor, - ConstructorEager, assert_equal_data, is_pyarrow_windows_no_tzdata, time_unit_compat, ) if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import IntoSeriesT + data = { "a": [datetime(2021, 3, 1, 12, 34, 56, 49000), datetime(2020, 1, 2, 2, 4, 14, 715000)] } diff --git a/tests/expr_and_series/dt/to_string_test.py b/tests/expr_and_series/dt/to_string_test.py index 4fd4c14c66..6b0a45d9c0 100644 --- a/tests/expr_and_series/dt/to_string_test.py +++ b/tests/expr_and_series/dt/to_string_test.py @@ -1,12 +1,15 @@ from __future__ import annotations from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data, is_windows +from tests.utils import assert_equal_data, is_windows + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = { "a": [datetime(2021, 3, 1, 12, 34, 56, 49000), datetime(2020, 1, 2, 2, 4, 14, 715000)] diff --git a/tests/expr_and_series/dt/truncate_test.py b/tests/expr_and_series/dt/truncate_test.py index 40ce18d428..71bcd083d4 100644 --- a/tests/expr_and_series/dt/truncate_test.py +++ b/tests/expr_and_series/dt/truncate_test.py @@ -1,12 +1,16 @@ from __future__ import annotations from datetime import datetime +from typing import TYPE_CHECKING from zoneinfo import ZoneInfo import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = { "a": [datetime(2021, 3, 1, 12, 34, 56, 49012), datetime(2020, 1, 2, 2, 4, 14, 715123)] diff --git a/tests/expr_and_series/ewm_test.py b/tests/expr_and_series/ewm_test.py index a3afae154b..30f9511f5e 100644 --- a/tests/expr_and_series/ewm_test.py +++ b/tests/expr_and_series/ewm_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = {"a": [1, 1, 2], "b": [1, 2, 3]} diff --git a/tests/expr_and_series/exclude_test.py b/tests/expr_and_series/exclude_test.py index 4aa39478d2..805f0218e2 100644 --- a/tests/expr_and_series/exclude_test.py +++ b/tests/expr_and_series/exclude_test.py @@ -8,7 +8,7 @@ from tests.utils import assert_equal_data if TYPE_CHECKING: - from tests.utils import Constructor + from narwhals.testing.typing import Constructor @pytest.mark.parametrize( diff --git a/tests/expr_and_series/exp_test.py b/tests/expr_and_series/exp_test.py index 7bfcd5c404..63df1f6923 100644 --- a/tests/expr_and_series/exp_test.py +++ b/tests/expr_and_series/exp_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [-1, 0, 1, 2, 4]} diff --git a/tests/expr_and_series/fill_nan_test.py b/tests/expr_and_series/fill_nan_test.py index 132b553c50..a00e25eea2 100644 --- a/tests/expr_and_series/fill_nan_test.py +++ b/tests/expr_and_series/fill_nan_test.py @@ -1,22 +1,21 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.conftest import ( - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, - pandas_constructor, -) -from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data +from narwhals.testing.constructors import ConstructorName +from tests.utils import PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager -NON_NULLABLE_CONSTRUCTORS = [ - pandas_constructor, - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, -] +NON_NULLABLE_CONSTRUCTORS = { + ConstructorName.MODIN, + ConstructorName.PANDAS, + ConstructorName.DASK, +} def test_fill_nan(request: pytest.FixtureRequest, constructor: Constructor) -> None: @@ -36,7 +35,7 @@ def test_fill_nan(request: pytest.FixtureRequest, constructor: Constructor) -> N assert_equal_data(result, expected) assert result.lazy().collect()["float_na"].null_count() == 2 result = df.select(nw.all().fill_nan(3.0)) - if any(constructor is c for c in NON_NULLABLE_CONSTRUCTORS): + if constructor.name in NON_NULLABLE_CONSTRUCTORS: # no nan vs null distinction expected = {"float": [-1.0, 1.0, 3.0], "float_na": [3.0, 1.0, 3.0]} assert result.lazy().collect()["float_na"].null_count() == 0 @@ -55,7 +54,7 @@ def test_fill_nan_series(constructor_eager: ConstructorEager) -> None: "float_na" ] result = s.fill_nan(999) - if any(constructor_eager is c for c in NON_NULLABLE_CONSTRUCTORS): + if constructor_eager.name in NON_NULLABLE_CONSTRUCTORS: # no nan vs null distinction assert_equal_data({"a": result}, {"a": [999.0, 1.0, 999.0]}) elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): diff --git a/tests/expr_and_series/fill_null_test.py b/tests/expr_and_series/fill_null_test.py index 213b695176..a2f61223df 100644 --- a/tests/expr_and_series/fill_null_test.py +++ b/tests/expr_and_series/fill_null_test.py @@ -7,16 +7,10 @@ import pytest import narwhals as nw -from tests.utils import ( - DASK_VERSION, - DUCKDB_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DASK_VERSION, DUCKDB_VERSION, POLARS_VERSION, assert_equal_data if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import FillNullStrategy diff --git a/tests/expr_and_series/filter_test.py b/tests/expr_and_series/filter_test.py index 5f300ed4b8..4e420fcb73 100644 --- a/tests/expr_and_series/filter_test.py +++ b/tests/expr_and_series/filter_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = { "i": [0, 1, 2, 3, 4], diff --git a/tests/expr_and_series/first_last_test.py b/tests/expr_and_series/first_last_test.py index 0dab322cd1..19570c8691 100644 --- a/tests/expr_and_series/first_last_test.py +++ b/tests/expr_and_series/first_last_test.py @@ -7,17 +7,12 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import ( - DUCKDB_VERSION, - POLARS_VERSION, - PYARROW_VERSION, - Constructor, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, POLARS_VERSION, PYARROW_VERSION, assert_equal_data if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import PythonLiteral - from tests.utils import ConstructorEager + data: dict[str, list[PythonLiteral]] = { "a": [8, 2, 1, None], diff --git a/tests/expr_and_series/floor_ceil_test.py b/tests/expr_and_series/floor_ceil_test.py index ae0df72712..a8cedb974f 100644 --- a/tests/expr_and_series/floor_ceil_test.py +++ b/tests/expr_and_series/floor_ceil_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_floor_expr(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/format_test.py b/tests/expr_and_series/format_test.py index 5bd4ba75a9..c5231bfc68 100644 --- a/tests/expr_and_series/format_test.py +++ b/tests/expr_and_series/format_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_format(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/horizontal_broadcasts_test.py b/tests/expr_and_series/horizontal_broadcasts_test.py index 6c6bb8966d..a689be6a8b 100644 --- a/tests/expr_and_series/horizontal_broadcasts_test.py +++ b/tests/expr_and_series/horizontal_broadcasts_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, Constructor, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_sumh_broadcasting(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/is_between_test.py b/tests/expr_and_series/is_between_test.py index 6cec08ad64..5d97af51fa 100644 --- a/tests/expr_and_series/is_between_test.py +++ b/tests/expr_and_series/is_between_test.py @@ -1,13 +1,16 @@ from __future__ import annotations from datetime import datetime -from typing import Literal +from typing import TYPE_CHECKING, Literal import pytest import narwhals as nw from narwhals.exceptions import MultiOutputExpressionError -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager @pytest.mark.parametrize( diff --git a/tests/expr_and_series/is_close_test.py b/tests/expr_and_series/is_close_test.py index 16c59536ca..e1d8762a18 100644 --- a/tests/expr_and_series/is_close_test.py +++ b/tests/expr_and_series/is_close_test.py @@ -12,29 +12,20 @@ import narwhals as nw from narwhals.exceptions import ComputeError, InvalidOperationError -from tests.conftest import ( - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, - pandas_constructor, -) -from tests.utils import ( - PANDAS_VERSION, - PYARROW_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from narwhals.testing.constructors import ConstructorName +from tests.utils import PANDAS_VERSION, PYARROW_VERSION, assert_equal_data if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import NumericLiteral -NON_NULLABLE_CONSTRUCTORS = ( - pandas_constructor, - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, -) + +NON_NULLABLE_CONSTRUCTORS = { + ConstructorName.MODIN, + ConstructorName.PANDAS, + ConstructorName.DASK, +} + NULL_PLACEHOLDER, NAN_PLACEHOLDER = 9999.0, -1.0 INF_POS, INF_NEG = float("inf"), float("-inf") @@ -126,7 +117,7 @@ def test_is_close_series_with_series( y = y.zip_with(y != NAN_PLACEHOLDER, y**0.5).zip_with(y != NULL_PLACEHOLDER, nulls) result = x.is_close(y, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal) - if constructor_eager in NON_NULLABLE_CONSTRUCTORS: + if constructor_eager.name in NON_NULLABLE_CONSTRUCTORS: expected = [v if v is not None else nans_equal for v in expected] elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): expected = [ @@ -154,7 +145,7 @@ def test_is_close_series_with_scalar( y = y.zip_with(y != NAN_PLACEHOLDER, y**0.5).zip_with(y != NULL_PLACEHOLDER, nulls) result = y.is_close(other, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal) - if constructor_eager in NON_NULLABLE_CONSTRUCTORS: + if constructor_eager.name in NON_NULLABLE_CONSTRUCTORS: expected = [v if v is not None else False for v in expected] elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): expected = [ @@ -199,7 +190,7 @@ def test_is_close_expr_with_expr( ) .sort("idx") ) - if constructor in NON_NULLABLE_CONSTRUCTORS: + if constructor.name in NON_NULLABLE_CONSTRUCTORS: expected = [v if v is not None else nans_equal for v in expected] elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): expected = [ @@ -240,7 +231,7 @@ def test_is_close_expr_with_scalar( ) .sort("idx") ) - if constructor in NON_NULLABLE_CONSTRUCTORS: + if constructor.name in NON_NULLABLE_CONSTRUCTORS: expected = [v if v is not None else False for v in expected] elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): expected = [ diff --git a/tests/expr_and_series/is_duplicated_test.py b/tests/expr_and_series/is_duplicated_test.py index 42f07a91e5..405143292f 100644 --- a/tests/expr_and_series/is_duplicated_test.py +++ b/tests/expr_and_series/is_duplicated_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_is_duplicated_expr(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/is_finite_test.py b/tests/expr_and_series/is_finite_test.py index eb07b2a41e..8b46b20ee6 100644 --- a/tests/expr_and_series/is_finite_test.py +++ b/tests/expr_and_series/is_finite_test.py @@ -1,24 +1,21 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.conftest import ( - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, - pandas_constructor, -) -from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data - -NON_NULLABLE_CONSTRUCTORS = [ - pandas_constructor, - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, -] +from narwhals.testing.constructors import ConstructorName +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager + +NON_NULLABLE_CONSTRUCTORS = { + ConstructorName.MODIN, + ConstructorName.PANDAS, + ConstructorName.DASK, +} data = {"a": [float("nan"), float("inf"), 2.0, None]} @@ -77,7 +74,7 @@ def test_is_finite_column_with_null(constructor: Constructor, data: list[float]) result = df.select(nw.col("a").is_finite()) expected: dict[str, list[Any]] - if any(constructor is c for c in NON_NULLABLE_CONSTRUCTORS): + if constructor.name in NON_NULLABLE_CONSTRUCTORS: # Null values are coerced to NaN for non-nullable datatypes expected = {"a": [True, True, False]} else: diff --git a/tests/expr_and_series/is_first_distinct_test.py b/tests/expr_and_series/is_first_distinct_test.py index 6f0501b03c..90cfdc97e4 100644 --- a/tests/expr_and_series/is_first_distinct_test.py +++ b/tests/expr_and_series/is_first_distinct_test.py @@ -1,15 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 1, 2, 3, 2], "b": [1, 2, 3, 2, 1]} diff --git a/tests/expr_and_series/is_in_test.py b/tests/expr_and_series/is_in_test.py index 2ae6cabea5..aeec14b862 100644 --- a/tests/expr_and_series/is_in_test.py +++ b/tests/expr_and_series/is_in_test.py @@ -1,11 +1,15 @@ from __future__ import annotations import re +from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 4, 2, 5]} diff --git a/tests/expr_and_series/is_last_distinct_test.py b/tests/expr_and_series/is_last_distinct_test.py index b4a96b5b9f..cb0860af51 100644 --- a/tests/expr_and_series/is_last_distinct_test.py +++ b/tests/expr_and_series/is_last_distinct_test.py @@ -1,15 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 1, 2, 3, 2], "b": [1, 2, 3, 2, 1]} diff --git a/tests/expr_and_series/is_nan_test.py b/tests/expr_and_series/is_nan_test.py index 27790e27b2..672c5864e1 100644 --- a/tests/expr_and_series/is_nan_test.py +++ b/tests/expr_and_series/is_nan_test.py @@ -1,24 +1,21 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.conftest import ( - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, - pandas_constructor, -) -from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data - -NON_NULLABLE_CONSTRUCTORS = [ - pandas_constructor, - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, -] +from narwhals.testing.constructors import ConstructorName +from tests.utils import PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager + +NON_NULLABLE_CONSTRUCTORS = { + ConstructorName.MODIN, + ConstructorName.PANDAS, + ConstructorName.DASK, +} def test_nan(constructor: Constructor) -> None: @@ -33,7 +30,7 @@ def test_nan(constructor: Constructor) -> None: ) expected: dict[str, list[Any]] - if any(constructor is c for c in NON_NULLABLE_CONSTRUCTORS): + if constructor.name in NON_NULLABLE_CONSTRUCTORS: # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], @@ -70,7 +67,7 @@ def test_nan_series(constructor_eager: ConstructorEager) -> None: "float_na": df["float_na"].is_nan(), } expected: dict[str, list[Any]] - if any(constructor_eager is c for c in NON_NULLABLE_CONSTRUCTORS): + if constructor_eager.name in NON_NULLABLE_CONSTRUCTORS: # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], diff --git a/tests/expr_and_series/is_null_test.py b/tests/expr_and_series/is_null_test.py index 7542af4513..9e51c63201 100644 --- a/tests/expr_and_series/is_null_test.py +++ b/tests/expr_and_series/is_null_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_null(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/is_unique_test.py b/tests/expr_and_series/is_unique_test.py index 2df0518360..9f125d1044 100644 --- a/tests/expr_and_series/is_unique_test.py +++ b/tests/expr_and_series/is_unique_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_is_unique_expr(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/kurtosis_test.py b/tests/expr_and_series/kurtosis_test.py index 55727445fe..3e653e416e 100644 --- a/tests/expr_and_series/kurtosis_test.py +++ b/tests/expr_and_series/kurtosis_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager @pytest.mark.parametrize( diff --git a/tests/expr_and_series/len_test.py b/tests/expr_and_series/len_test.py index 755c389a28..86e1b88068 100644 --- a/tests/expr_and_series/len_test.py +++ b/tests/expr_and_series/len_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_len_no_filter(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/list/contains_test.py b/tests/expr_and_series/list/contains_test.py index 6e56846d6e..6be2d53a7a 100644 --- a/tests/expr_and_series/list/contains_test.py +++ b/tests/expr_and_series/list/contains_test.py @@ -8,8 +8,7 @@ from tests.utils import assert_equal_data if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager - + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [[2, 2, 3, None, None], None, []]} expected = {"a": [True, None, False]} diff --git a/tests/expr_and_series/list/get_test.py b/tests/expr_and_series/list/get_test.py index 52ca3386ba..55f9071a49 100644 --- a/tests/expr_and_series/list/get_test.py +++ b/tests/expr_and_series/list/get_test.py @@ -1,12 +1,15 @@ from __future__ import annotations import re -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [[1, 2], [None, 3], [None], None]} @@ -45,9 +48,8 @@ def test_get_series( pytest.skip() pytest.importorskip("pyarrow") - if ( - constructor_eager.__name__.startswith("pandas") - and "pyarrow" not in constructor_eager.__name__ + if str(constructor_eager).startswith("pandas") and "pyarrow" not in str( + constructor_eager ): df = nw.from_native(constructor_eager(data), eager_only=True) msg = re.escape("Series must be of PyArrow List type to support list namespace.") diff --git a/tests/expr_and_series/list/len_test.py b/tests/expr_and_series/list/len_test.py index 918fda5831..257db68327 100644 --- a/tests/expr_and_series/list/len_test.py +++ b/tests/expr_and_series/list/len_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [[1, 2], [3, 4, None], None, [], [None]]} expected = {"a": [2, 3, None, 0, 1]} diff --git a/tests/expr_and_series/list/max_test.py b/tests/expr_and_series/list/max_test.py index 646bff2244..baf9e8eaec 100644 --- a/tests/expr_and_series/list/max_test.py +++ b/tests/expr_and_series/list/max_test.py @@ -8,7 +8,7 @@ from tests.utils import PANDAS_VERSION, assert_equal_data if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [[3, None, 2, 2, 4, None], [-1], None, [None, None, None], []]} expected = [4, -1, None, None, None] diff --git a/tests/expr_and_series/list/mean_test.py b/tests/expr_and_series/list/mean_test.py index 9175d5b248..bb0e8848a3 100644 --- a/tests/expr_and_series/list/mean_test.py +++ b/tests/expr_and_series/list/mean_test.py @@ -8,7 +8,7 @@ from tests.utils import PANDAS_VERSION, assert_equal_data if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [[3, None, 2, 2, 4, None], [-1], None, [None, None, None], []]} expected = [2.75, -1, None, None, None] diff --git a/tests/expr_and_series/list/median_test.py b/tests/expr_and_series/list/median_test.py index f0d3c615ff..f39e896552 100644 --- a/tests/expr_and_series/list/median_test.py +++ b/tests/expr_and_series/list/median_test.py @@ -9,7 +9,7 @@ from tests.utils import PANDAS_VERSION, POLARS_VERSION, assert_equal_data, is_windows if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [[3, None, 2, 2, 4, None], [-1], None, [None, None, None], [], [3, 4, None]]} expected = [2.5, -1, None, None, None, 3.5] diff --git a/tests/expr_and_series/list/min_test.py b/tests/expr_and_series/list/min_test.py index 2039f7de56..32ad33e381 100644 --- a/tests/expr_and_series/list/min_test.py +++ b/tests/expr_and_series/list/min_test.py @@ -8,7 +8,7 @@ from tests.utils import PANDAS_VERSION, assert_equal_data if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [[3, None, 2, 2, 4, None], [-1], None, [None, None, None], []]} expected = [2, -1, None, None, None] diff --git a/tests/expr_and_series/list/sort_test.py b/tests/expr_and_series/list/sort_test.py index 1866934a73..872c95e00b 100644 --- a/tests/expr_and_series/list/sort_test.py +++ b/tests/expr_and_series/list/sort_test.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from typing import Any - from tests.utils import Constructor, ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [[3, 2, 2, 4, -10, None, None], [-1], None, [None, None, None], []]} diff --git a/tests/expr_and_series/list/sum_test.py b/tests/expr_and_series/list/sum_test.py index d66266e016..dd8290abd2 100644 --- a/tests/expr_and_series/list/sum_test.py +++ b/tests/expr_and_series/list/sum_test.py @@ -8,8 +8,7 @@ from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, assert_equal_data if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager - + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [[3, None, 2, 2, 4, None], [-1], None, [None, None, None], []]} expected = [11, -1, None, 0, 0] diff --git a/tests/expr_and_series/list/unique_test.py b/tests/expr_and_series/list/unique_test.py index 3d7c9dd039..809e1ed82c 100644 --- a/tests/expr_and_series/list/unique_test.py +++ b/tests/expr_and_series/list/unique_test.py @@ -8,7 +8,7 @@ from tests.utils import DUCKDB_VERSION if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [[2, 2, 3, None, None], None, [], [None]]} expected = {2, 3, None} diff --git a/tests/expr_and_series/lit_test.py b/tests/expr_and_series/lit_test.py index 188292c0d1..d3187b2d2b 100644 --- a/tests/expr_and_series/lit_test.py +++ b/tests/expr_and_series/lit_test.py @@ -12,12 +12,12 @@ PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, - Constructor, assert_equal_data, ) if TYPE_CHECKING: from narwhals.dtypes import DType + from narwhals.testing.typing import Constructor from narwhals.typing import IntoDType, PythonLiteral diff --git a/tests/expr_and_series/log_test.py b/tests/expr_and_series/log_test.py index 4157b5a4a6..dc3372c443 100644 --- a/tests/expr_and_series/log_test.py +++ b/tests/expr_and_series/log_test.py @@ -1,11 +1,15 @@ from __future__ import annotations import math +from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [-1, 0, 1, 2, 4]} diff --git a/tests/expr_and_series/map_batches_test.py b/tests/expr_and_series/map_batches_test.py index d6f8cc8b32..bac083b1f2 100644 --- a/tests/expr_and_series/map_batches_test.py +++ b/tests/expr_and_series/map_batches_test.py @@ -7,15 +7,12 @@ pytest.importorskip("numpy") import narwhals as nw -from tests.utils import ( - PANDAS_VERSION, - POLARS_VERSION, - ConstructorEager, - assert_equal_data, -) +from tests.utils import PANDAS_VERSION, POLARS_VERSION, assert_equal_data if TYPE_CHECKING: from narwhals.dtypes import DType + from narwhals.testing.typing import ConstructorEager + data = {"a": [1, 2, 3], "b": [4, 5, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/expr_and_series/max_horizontal_test.py b/tests/expr_and_series/max_horizontal_test.py index cc0bddfb1a..788d176b18 100644 --- a/tests/expr_and_series/max_horizontal_test.py +++ b/tests/expr_and_series/max_horizontal_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = {"a": [1, 3, None, None], "b": [4, None, 6, None], "z": [3, 1, None, None]} expected_values = [4, 3, 6, None] diff --git a/tests/expr_and_series/max_test.py b/tests/expr_and_series/max_test.py index eb359121c6..47906bfc19 100644 --- a/tests/expr_and_series/max_test.py +++ b/tests/expr_and_series/max_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/expr_and_series/mean_horizontal_test.py b/tests/expr_and_series/mean_horizontal_test.py index bc5bc12fa6..7754d3b854 100644 --- a/tests/expr_and_series/mean_horizontal_test.py +++ b/tests/expr_and_series/mean_horizontal_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_meanh(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/mean_test.py b/tests/expr_and_series/mean_test.py index 39a97bd53a..b2983a0b51 100644 --- a/tests/expr_and_series/mean_test.py +++ b/tests/expr_and_series/mean_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 7], "z": [7.0, 8.0, 9.0]} diff --git a/tests/expr_and_series/median_test.py b/tests/expr_and_series/median_test.py index b01350586a..b485bc6002 100644 --- a/tests/expr_and_series/median_test.py +++ b/tests/expr_and_series/median_test.py @@ -1,12 +1,16 @@ from __future__ import annotations import re +from typing import TYPE_CHECKING import pytest import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = { "a": [3, 8, 2, None], diff --git a/tests/expr_and_series/min_horizontal_test.py b/tests/expr_and_series/min_horizontal_test.py index df9ff31feb..575abf2d27 100644 --- a/tests/expr_and_series/min_horizontal_test.py +++ b/tests/expr_and_series/min_horizontal_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = {"a": [1, 3, None, None], "b": [4, None, 6, None], "z": [3, 1, None, None]} expected_values = [1, 1, 6, None] diff --git a/tests/expr_and_series/min_test.py b/tests/expr_and_series/min_test.py index 842e70c166..f166baf165 100644 --- a/tests/expr_and_series/min_test.py +++ b/tests/expr_and_series/min_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/expr_and_series/mode_test.py b/tests/expr_and_series/mode_test.py index a17de7bd17..c50d5d3bf7 100644 --- a/tests/expr_and_series/mode_test.py +++ b/tests/expr_and_series/mode_test.py @@ -2,13 +2,16 @@ import re from contextlib import nullcontext as does_not_raise -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw from narwhals.exceptions import ShapeError -from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 1, 2, 2, 3], "b": [1, 2, 3, 3, 4]} data_group = { diff --git a/tests/expr_and_series/n_unique_test.py b/tests/expr_and_series/n_unique_test.py index e9af1f0154..206777ccf0 100644 --- a/tests/expr_and_series/n_unique_test.py +++ b/tests/expr_and_series/n_unique_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1.0, None, None, 3.0], "b": [1.0, None, 4.0, 5.0]} diff --git a/tests/expr_and_series/name/keep_test.py b/tests/expr_and_series/name/keep_test.py index e4a3ba17f6..40b7e38f9b 100644 --- a/tests/expr_and_series/name/keep_test.py +++ b/tests/expr_and_series/name/keep_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} diff --git a/tests/expr_and_series/name/map_test.py b/tests/expr_and_series/name/map_test.py index eb4b2bc22b..22f8609733 100644 --- a/tests/expr_and_series/name/map_test.py +++ b/tests/expr_and_series/name/map_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, Constructor, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} diff --git a/tests/expr_and_series/name/prefix_test.py b/tests/expr_and_series/name/prefix_test.py index a639455823..f4447f5cde 100644 --- a/tests/expr_and_series/name/prefix_test.py +++ b/tests/expr_and_series/name/prefix_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, Constructor, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} prefix = "with_prefix_" diff --git a/tests/expr_and_series/name/suffix_test.py b/tests/expr_and_series/name/suffix_test.py index 437d93e05a..780dfd4c16 100644 --- a/tests/expr_and_series/name/suffix_test.py +++ b/tests/expr_and_series/name/suffix_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, Constructor, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} suffix = "_with_suffix" diff --git a/tests/expr_and_series/name/to_lowercase_test.py b/tests/expr_and_series/name/to_lowercase_test.py index 41a88a4f9c..43170dc311 100644 --- a/tests/expr_and_series/name/to_lowercase_test.py +++ b/tests/expr_and_series/name/to_lowercase_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, Constructor, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} diff --git a/tests/expr_and_series/name/to_uppercase_test.py b/tests/expr_and_series/name/to_uppercase_test.py index 9204ac86b5..1080cff82d 100644 --- a/tests/expr_and_series/name/to_uppercase_test.py +++ b/tests/expr_and_series/name/to_uppercase_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, Constructor, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} diff --git a/tests/expr_and_series/nth_test.py b/tests/expr_and_series/nth_test.py index 1249f7f2e2..d3e035faff 100644 --- a/tests/expr_and_series/nth_test.py +++ b/tests/expr_and_series/nth_test.py @@ -6,12 +6,12 @@ import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, Constructor, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data if TYPE_CHECKING: - from collections.abc import Mapping + from narwhals.testing.typing import Constructor -data: Mapping[str, Any] = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8.0, 9.0]} +data: dict[str, Any] = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8.0, 9.0]} @pytest.mark.parametrize( diff --git a/tests/expr_and_series/null_count_test.py b/tests/expr_and_series/null_count_test.py index 6efef0ada1..0c7e6b511a 100644 --- a/tests/expr_and_series/null_count_test.py +++ b/tests/expr_and_series/null_count_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1.0, None, None, 3.0], "b": [1.0, None, 4.0, 5.0]} diff --git a/tests/expr_and_series/operators_test.py b/tests/expr_and_series/operators_test.py index f505c8f972..add29a19c3 100644 --- a/tests/expr_and_series/operators_test.py +++ b/tests/expr_and_series/operators_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import DASK_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import DASK_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager @pytest.mark.parametrize( diff --git a/tests/expr_and_series/order_dependent_lazy_test.py b/tests/expr_and_series/order_dependent_lazy_test.py index 70bd234828..b88df38b53 100644 --- a/tests/expr_and_series/order_dependent_lazy_test.py +++ b/tests/expr_and_series/order_dependent_lazy_test.py @@ -8,7 +8,7 @@ from narwhals.exceptions import InvalidOperationError if TYPE_CHECKING: - from tests.utils import Constructor + from narwhals.testing.typing import Constructor def test_order_dependent_raises_in_lazy(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/over_pushdown_test.py b/tests/expr_and_series/over_pushdown_test.py index 8a8c9795d0..8dfceedf6a 100644 --- a/tests/expr_and_series/over_pushdown_test.py +++ b/tests/expr_and_series/over_pushdown_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, Constructor, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_over_pushdown(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/over_test.py b/tests/expr_and_series/over_test.py index 7fbb8f0f9f..3273bb1dfc 100644 --- a/tests/expr_and_series/over_test.py +++ b/tests/expr_and_series/over_test.py @@ -1,19 +1,16 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise +from typing import TYPE_CHECKING import pytest import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import ( - DUCKDB_VERSION, - PANDAS_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = { "a": ["a", "a", "b", "b", "b"], diff --git a/tests/expr_and_series/pipe_test.py b/tests/expr_and_series/pipe_test.py index d32743e1ea..1bd7e8ca39 100644 --- a/tests/expr_and_series/pipe_test.py +++ b/tests/expr_and_series/pipe_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager input_list = {"a": [2, 4, 6, 8]} expected = [4, 16, 36, 64] diff --git a/tests/expr_and_series/quantile_test.py b/tests/expr_and_series/quantile_test.py index 39489db89d..fd0a2a8f89 100644 --- a/tests/expr_and_series/quantile_test.py +++ b/tests/expr_and_series/quantile_test.py @@ -2,12 +2,15 @@ import re from contextlib import nullcontext as does_not_raise -from typing import Literal +from typing import TYPE_CHECKING, Literal import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager @pytest.mark.parametrize( @@ -43,7 +46,7 @@ def test_quantile_expr( ) context = ( pytest.raises(NotImplementedError, match=msg) - if "dask_lazy_p2" in str(constructor) + if "dask_lazy" in str(constructor) else does_not_raise() ) diff --git a/tests/expr_and_series/rank_test.py b/tests/expr_and_series/rank_test.py index fb50fdeb59..9fc1d0dd0e 100644 --- a/tests/expr_and_series/rank_test.py +++ b/tests/expr_and_series/rank_test.py @@ -1,7 +1,7 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise -from typing import Literal +from typing import TYPE_CHECKING, Literal import pytest @@ -10,12 +10,13 @@ DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, - Constructor, - ConstructorEager, assert_equal_data, is_windows, ) +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager + rank_methods = ["average", "min", "max", "dense", "ordinal"] data_int = {"a": [3, 6, 1, 1, None, 6], "b": [1, 1, 2, 1, 2, 2], "i": [1, 2, 3, 4, 5, 6]} diff --git a/tests/expr_and_series/reduction_test.py b/tests/expr_and_series/reduction_test.py index cba75767ea..5ead8edda9 100644 --- a/tests/expr_and_series/reduction_test.py +++ b/tests/expr_and_series/reduction_test.py @@ -1,12 +1,15 @@ from __future__ import annotations from itertools import chain -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager @pytest.mark.parametrize( diff --git a/tests/expr_and_series/replace_strict_test.py b/tests/expr_and_series/replace_strict_test.py index e0fbcecd06..96c29f49dc 100644 --- a/tests/expr_and_series/replace_strict_test.py +++ b/tests/expr_and_series/replace_strict_test.py @@ -6,18 +6,14 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import ( - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, - xfail_if_pyspark_connect, -) +from tests.utils import POLARS_VERSION, assert_equal_data, xfail_if_pyspark_connect if TYPE_CHECKING: from collections.abc import Mapping, Sequence from narwhals.dtypes import DType + from narwhals.testing.typing import Constructor, ConstructorEager + polars_lt_v1 = POLARS_VERSION < (1, 0, 0) skip_reason = "replace_strict only available after 1.0" diff --git a/tests/expr_and_series/rolling_mean_test.py b/tests/expr_and_series/rolling_mean_test.py index b4c0b9e656..c9768680c1 100644 --- a/tests/expr_and_series/rolling_mean_test.py +++ b/tests/expr_and_series/rolling_mean_test.py @@ -1,20 +1,17 @@ from __future__ import annotations import random -from typing import Any +from typing import TYPE_CHECKING, Any import hypothesis.strategies as st import pytest from hypothesis import given import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [None, 1, 2, None, 4, 6, 11]} diff --git a/tests/expr_and_series/rolling_std_test.py b/tests/expr_and_series/rolling_std_test.py index f027b450d9..4daf1adbde 100644 --- a/tests/expr_and_series/rolling_std_test.py +++ b/tests/expr_and_series/rolling_std_test.py @@ -1,19 +1,15 @@ from __future__ import annotations from math import sqrt -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - PANDAS_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1.0, 2.0, 1.0, 3.0, 1.0, 4.0, 1.0]} diff --git a/tests/expr_and_series/rolling_sum_test.py b/tests/expr_and_series/rolling_sum_test.py index df7b481826..fae3d9ebff 100644 --- a/tests/expr_and_series/rolling_sum_test.py +++ b/tests/expr_and_series/rolling_sum_test.py @@ -1,7 +1,7 @@ from __future__ import annotations import random -from typing import Any +from typing import TYPE_CHECKING, Any import hypothesis.strategies as st import pytest @@ -9,14 +9,10 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import ( - DUCKDB_VERSION, - PANDAS_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [None, 1, 2, None, 4, 6, 11]} diff --git a/tests/expr_and_series/rolling_var_test.py b/tests/expr_and_series/rolling_var_test.py index b38ba5f077..84c97b6fa8 100644 --- a/tests/expr_and_series/rolling_var_test.py +++ b/tests/expr_and_series/rolling_var_test.py @@ -1,25 +1,21 @@ from __future__ import annotations import random -from typing import Any +from typing import TYPE_CHECKING, Any import hypothesis.strategies as st import pytest from hypothesis import HealthCheck, given, settings import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - PANDAS_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data pytest.importorskip("pandas") import pandas as pd +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager + data = {"a": [1.0, 2.0, 1.0, 3.0, 1.0, 4.0, 1.0]} kwargs_and_expected = ( diff --git a/tests/expr_and_series/round_test.py b/tests/expr_and_series/round_test.py index 0709ad28b7..061a3e0765 100644 --- a/tests/expr_and_series/round_test.py +++ b/tests/expr_and_series/round_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager @pytest.mark.parametrize("decimals", [0, 1, 2]) diff --git a/tests/expr_and_series/sample_test.py b/tests/expr_and_series/sample_test.py index 4ec44a8712..23ad3bac30 100644 --- a/tests/expr_and_series/sample_test.py +++ b/tests/expr_and_series/sample_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_sample_fraction(constructor_eager: ConstructorEager) -> None: diff --git a/tests/expr_and_series/shift_test.py b/tests/expr_and_series/shift_test.py index a06ff4a872..26f134b3a3 100644 --- a/tests/expr_and_series/shift_test.py +++ b/tests/expr_and_series/shift_test.py @@ -1,18 +1,15 @@ from __future__ import annotations from contextlib import nullcontext -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = { "i": [0, 1, 2, 3, 4], diff --git a/tests/expr_and_series/sin_test.py b/tests/expr_and_series/sin_test.py index b1fed2f501..13ac55794b 100644 --- a/tests/expr_and_series/sin_test.py +++ b/tests/expr_and_series/sin_test.py @@ -6,15 +6,10 @@ import pytest import narwhals as nw -from tests.utils import ( - PANDAS_VERSION, - PYARROW_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import PANDAS_VERSION, PYARROW_VERSION, assert_equal_data if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import DTypeBackend data = {"a": [-pi, -pi / 2, 0.0, pi / 2, pi]} diff --git a/tests/expr_and_series/skew_test.py b/tests/expr_and_series/skew_test.py index 9be2056dac..7788d2be94 100644 --- a/tests/expr_and_series/skew_test.py +++ b/tests/expr_and_series/skew_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager @pytest.mark.parametrize( diff --git a/tests/expr_and_series/sqrt_test.py b/tests/expr_and_series/sqrt_test.py index 057401d594..a890ea7491 100644 --- a/tests/expr_and_series/sqrt_test.py +++ b/tests/expr_and_series/sqrt_test.py @@ -5,15 +5,10 @@ import pytest import narwhals as nw -from tests.utils import ( - PANDAS_VERSION, - PYARROW_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import PANDAS_VERSION, PYARROW_VERSION, assert_equal_data if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import DTypeBackend data = {"a": [-1, 0, 1, 2, 4]} diff --git a/tests/expr_and_series/std_test.py b/tests/expr_and_series/std_test.py index d2b3cb14ef..f5dbbfbe5b 100644 --- a/tests/expr_and_series/std_test.py +++ b/tests/expr_and_series/std_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} data_with_nulls = {"a": [1, 3, 2, None], "b": [4, 4, 6, None], "z": [7.0, 8.0, 9.0, None]} diff --git a/tests/expr_and_series/str/contains_test.py b/tests/expr_and_series/str/contains_test.py index 7935b6c92f..737e3e92fb 100644 --- a/tests/expr_and_series/str/contains_test.py +++ b/tests/expr_and_series/str/contains_test.py @@ -1,11 +1,14 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"pets": ["cat", "dog", "rabbit and parrot", "dove", "Parrot|dove", None]} """Test data for string literal pattern tests""" diff --git a/tests/expr_and_series/str/head_test.py b/tests/expr_and_series/str/head_test.py index d6d09b978e..960229de51 100644 --- a/tests/expr_and_series/str/head_test.py +++ b/tests/expr_and_series/str/head_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": ["foo", "bars"]} diff --git a/tests/expr_and_series/str/len_chars_test.py b/tests/expr_and_series/str/len_chars_test.py index 32a726ab26..4de51443fb 100644 --- a/tests/expr_and_series/str/len_chars_test.py +++ b/tests/expr_and_series/str/len_chars_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": ["foo", "foobar", "Café", "345", "東京"]} diff --git a/tests/expr_and_series/str/pad_test.py b/tests/expr_and_series/str/pad_test.py index 763fd41043..f51ecf4d28 100644 --- a/tests/expr_and_series/str/pad_test.py +++ b/tests/expr_and_series/str/pad_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_str_pad_start_series(constructor_eager: ConstructorEager) -> None: diff --git a/tests/expr_and_series/str/replace_test.py b/tests/expr_and_series/str/replace_test.py index e6f49f409b..0be107110d 100644 --- a/tests/expr_and_series/str/replace_test.py +++ b/tests/expr_and_series/str/replace_test.py @@ -1,12 +1,15 @@ from __future__ import annotations from contextlib import nullcontext -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager replace_data = [ ({"a": ["123abc", "abc456"]}, r"abc\b", "ABC", 1, False, {"a": ["123ABC", "abc456"]}), diff --git a/tests/expr_and_series/str/slice_test.py b/tests/expr_and_series/str/slice_test.py index 87486e6927..8f30717f9a 100644 --- a/tests/expr_and_series/str/slice_test.py +++ b/tests/expr_and_series/str/slice_test.py @@ -1,11 +1,14 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": ["fdas", "edfas"]} diff --git a/tests/expr_and_series/str/split_test.py b/tests/expr_and_series/str/split_test.py index b6b25cd024..2f4ce5fdf1 100644 --- a/tests/expr_and_series/str/split_test.py +++ b/tests/expr_and_series/str/split_test.py @@ -1,12 +1,15 @@ from __future__ import annotations import re -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"s": ["foo bar", "foo_bar", "foo_bar_baz", "foo,bar"]} @@ -20,8 +23,7 @@ ) def test_str_split(constructor: Constructor, by: str, expected: Any) -> None: if "cudf" not in str(constructor) and ( - constructor.__name__.startswith("pandas") - and "pyarrow" not in constructor.__name__ + str(constructor).startswith("pandas") and "pyarrow" not in str(constructor) ): df = nw.from_native(constructor(data)) msg = re.escape("This operation requires a pyarrow-backed series. ") @@ -44,8 +46,8 @@ def test_str_split_series( constructor_eager: ConstructorEager, by: str, expected: Any ) -> None: if "cudf" not in str(constructor_eager) and ( - constructor_eager.__name__.startswith("pandas") - and "pyarrow" not in constructor_eager.__name__ + str(constructor_eager).startswith("pandas") + and "pyarrow" not in str(constructor_eager) ): df = nw.from_native(constructor_eager(data), eager_only=True) msg = re.escape("This operation requires a pyarrow-backed series. ") diff --git a/tests/expr_and_series/str/starts_with_ends_with_test.py b/tests/expr_and_series/str/starts_with_ends_with_test.py index 73f60a355d..e3d159a67a 100644 --- a/tests/expr_and_series/str/starts_with_ends_with_test.py +++ b/tests/expr_and_series/str/starts_with_ends_with_test.py @@ -1,10 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw +from tests.utils import assert_equal_data -# Don't move this into typechecking block, for coverage -# purposes -from tests.utils import Constructor, ConstructorEager, assert_equal_data +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": ["fdas", "edfas"]} diff --git a/tests/expr_and_series/str/strip_chars_test.py b/tests/expr_and_series/str/strip_chars_test.py index 8944bce4de..1736dba8b7 100644 --- a/tests/expr_and_series/str/strip_chars_test.py +++ b/tests/expr_and_series/str/strip_chars_test.py @@ -1,11 +1,14 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": ["foobar", "bar\n", " baz"]} diff --git a/tests/expr_and_series/str/tail_test.py b/tests/expr_and_series/str/tail_test.py index 8aa9d66c02..a7bad4d13b 100644 --- a/tests/expr_and_series/str/tail_test.py +++ b/tests/expr_and_series/str/tail_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": ["foo", "bars"]} diff --git a/tests/expr_and_series/str/to_date_test.py b/tests/expr_and_series/str/to_date_test.py index d1f7558c9f..2d073564b9 100644 --- a/tests/expr_and_series/str/to_date_test.py +++ b/tests/expr_and_series/str/to_date_test.py @@ -9,7 +9,7 @@ from tests.utils import assert_equal_data, uses_pyarrow_backend if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": ["2020-01-01", "2020-01-02", None]} expected = {"a": [date(2020, 1, 1), date(2020, 1, 2), None]} diff --git a/tests/expr_and_series/str/to_datetime_test.py b/tests/expr_and_series/str/to_datetime_test.py index b98755a0cc..c6f8d1a838 100644 --- a/tests/expr_and_series/str/to_datetime_test.py +++ b/tests/expr_and_series/str/to_datetime_test.py @@ -18,7 +18,7 @@ ) if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": ["2020-01-01T12:34:56"]} diff --git a/tests/expr_and_series/str/to_titlecase_test.py b/tests/expr_and_series/str/to_titlecase_test.py index ae61c3ecd2..1e0a09bc79 100644 --- a/tests/expr_and_series/str/to_titlecase_test.py +++ b/tests/expr_and_series/str/to_titlecase_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = { "a": [ diff --git a/tests/expr_and_series/str/to_uppercase_to_lowercase_test.py b/tests/expr_and_series/str/to_uppercase_to_lowercase_test.py index 7c5c39a481..c3b2237ea4 100644 --- a/tests/expr_and_series/str/to_uppercase_to_lowercase_test.py +++ b/tests/expr_and_series/str/to_uppercase_to_lowercase_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager @pytest.mark.parametrize( diff --git a/tests/expr_and_series/str/zfill_test.py b/tests/expr_and_series/str/zfill_test.py index 78f59cecb1..86293ecdfb 100644 --- a/tests/expr_and_series/str/zfill_test.py +++ b/tests/expr_and_series/str/zfill_test.py @@ -1,17 +1,20 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw from tests.utils import ( PANDAS_VERSION, POLARS_VERSION, - Constructor, - ConstructorEager, assert_equal_data, uses_pyarrow_backend, ) +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager + data = {"a": ["-1", "+1", "1", "12", "123", "99999", "+9999", None]} expected = {"a": ["-01", "+01", "001", "012", "123", "99999", "+9999", None]} diff --git a/tests/expr_and_series/struct_/field_test.py b/tests/expr_and_series/struct_/field_test.py index a351c31500..2f50ff35af 100644 --- a/tests/expr_and_series/struct_/field_test.py +++ b/tests/expr_and_series/struct_/field_test.py @@ -1,11 +1,14 @@ from __future__ import annotations -from typing import cast +from typing import TYPE_CHECKING, cast import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_get_field_expr(request: pytest.FixtureRequest, constructor: Constructor) -> None: diff --git a/tests/expr_and_series/struct_test.py b/tests/expr_and_series/struct_test.py index b7f1eae1ca..80c2dfa65a 100644 --- a/tests/expr_and_series/struct_test.py +++ b/tests/expr_and_series/struct_test.py @@ -1,16 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - PANDAS_VERSION, - PYARROW_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, PYARROW_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 2, 3], "b": ["dogs", "cats", None], "c": ["play", "swim", "walk"]} diff --git a/tests/expr_and_series/sum_horizontal_test.py b/tests/expr_and_series/sum_horizontal_test.py index 94cc32d4b0..a80f6e0367 100644 --- a/tests/expr_and_series/sum_horizontal_test.py +++ b/tests/expr_and_series/sum_horizontal_test.py @@ -1,11 +1,14 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, Constructor, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_sumh(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/sum_test.py b/tests/expr_and_series/sum_test.py index 6501869a49..00da2e1620 100644 --- a/tests/expr_and_series/sum_test.py +++ b/tests/expr_and_series/sum_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/expr_and_series/unary_test.py b/tests/expr_and_series/unary_test.py index 038136a902..b2c1e59332 100644 --- a/tests/expr_and_series/unary_test.py +++ b/tests/expr_and_series/unary_test.py @@ -1,11 +1,15 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise +from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_unary(constructor: Constructor, request: pytest.FixtureRequest) -> None: diff --git a/tests/expr_and_series/unique_test.py b/tests/expr_and_series/unique_test.py index c54a0356c7..3c7bc7d20d 100644 --- a/tests/expr_and_series/unique_test.py +++ b/tests/expr_and_series/unique_test.py @@ -1,12 +1,16 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise +from typing import TYPE_CHECKING import pytest import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 1, None, 2]} data_str = {"a": ["x", "x", "y", None]} diff --git a/tests/expr_and_series/var_test.py b/tests/expr_and_series/var_test.py index f927dd1ac0..c33b7f6917 100644 --- a/tests/expr_and_series/var_test.py +++ b/tests/expr_and_series/var_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} data_with_nulls = {"a": [1, 3, 2, None], "b": [4, 4, 6, None], "z": [7.0, 8.0, 9.0, None]} diff --git a/tests/expr_and_series/when_test.py b/tests/expr_and_series/when_test.py index aefac8492c..b0deb0714a 100644 --- a/tests/expr_and_series/when_test.py +++ b/tests/expr_and_series/when_test.py @@ -6,15 +6,10 @@ import narwhals as nw from narwhals.exceptions import MultiOutputExpressionError -from tests.utils import ( - DUCKDB_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, - uses_pyarrow_backend, -) +from tests.utils import DUCKDB_VERSION, assert_equal_data, uses_pyarrow_backend if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import _1DArray data = { diff --git a/tests/expression_parsing_test.py b/tests/expression_parsing_test.py index 138063eb06..17d3b80104 100644 --- a/tests/expression_parsing_test.py +++ b/tests/expression_parsing_test.py @@ -1,10 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import DUCKDB_VERSION, POLARS_VERSION, Constructor, assert_equal_data +from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor @pytest.mark.parametrize( diff --git a/tests/frame/add_test.py b/tests/frame/add_test.py index 166be83fd6..082bb0076d 100644 --- a/tests/frame/add_test.py +++ b/tests/frame/add_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, Constructor, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_add(constructor: Constructor) -> None: diff --git a/tests/frame/array_dunder_test.py b/tests/frame/array_dunder_test.py index d5db653c09..f0ec7b90c8 100644 --- a/tests/frame/array_dunder_test.py +++ b/tests/frame/array_dunder_test.py @@ -3,16 +3,15 @@ import pytest pytest.importorskip("numpy") +from typing import TYPE_CHECKING + import numpy as np import narwhals as nw -from tests.utils import ( - PANDAS_VERSION, - POLARS_VERSION, - PYARROW_VERSION, - ConstructorEager, - assert_equal_data, -) +from tests.utils import PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_array_dunder( diff --git a/tests/frame/clone_test.py b/tests/frame/clone_test.py index 66b9771835..06a56bff64 100644 --- a/tests/frame/clone_test.py +++ b/tests/frame/clone_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_clone(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/collect_test.py b/tests/frame/collect_test.py index 66ea38e979..09093cbeae 100644 --- a/tests/frame/collect_test.py +++ b/tests/frame/collect_test.py @@ -7,10 +7,11 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.dependencies import get_cudf, get_modin, get_polars -from tests.utils import POLARS_VERSION, Constructor, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data if TYPE_CHECKING: from narwhals._typing import Arrow, Dask, IntoBackend, Modin, Pandas, Polars + from narwhals.testing.typing import Constructor data = {"a": [1, 2], "b": [3, 4]} diff --git a/tests/frame/columns_test.py b/tests/frame/columns_test.py index 3fda986219..beea95c36f 100644 --- a/tests/frame/columns_test.py +++ b/tests/frame/columns_test.py @@ -8,7 +8,7 @@ from tests.utils import assert_equal_data if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/frame/concat_test.py b/tests/frame/concat_test.py index 00beb2ebc1..5a5f51cab4 100644 --- a/tests/frame/concat_test.py +++ b/tests/frame/concat_test.py @@ -9,11 +9,13 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.exceptions import InvalidOperationError, NarwhalsError -from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data if TYPE_CHECKING: from collections.abc import Iterator + from narwhals.testing.typing import Constructor, ConstructorEager + def test_concat_horizontal(constructor_eager: ConstructorEager) -> None: data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/frame/double_test.py b/tests/frame/double_test.py index 6a8f5d6333..ebcbb1fc77 100644 --- a/tests/frame/double_test.py +++ b/tests/frame/double_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_double(constructor: Constructor) -> None: diff --git a/tests/frame/drop_nulls_test.py b/tests/frame/drop_nulls_test.py index 34d433979d..561abadd9b 100644 --- a/tests/frame/drop_nulls_test.py +++ b/tests/frame/drop_nulls_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = {"alpha": [1.0, 2.0, None, 4.0], "beta gamma": [None, 3.0, None, 5.0]} diff --git a/tests/frame/drop_test.py b/tests/frame/drop_test.py index 233ab3003b..22887c0ebc 100644 --- a/tests/frame/drop_test.py +++ b/tests/frame/drop_test.py @@ -10,7 +10,7 @@ from tests.utils import POLARS_VERSION if TYPE_CHECKING: - from tests.utils import Constructor + from narwhals.testing.typing import Constructor @pytest.mark.parametrize( diff --git a/tests/frame/eq_test.py b/tests/frame/eq_test.py index c100469451..fbe4786961 100644 --- a/tests/frame/eq_test.py +++ b/tests/frame/eq_test.py @@ -7,7 +7,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import Constructor + from narwhals.testing.typing import Constructor def test_eq_neq_raise(constructor: Constructor) -> None: diff --git a/tests/frame/estimated_size_test.py b/tests/frame/estimated_size_test.py index 78c1cc86f7..cef3c60216 100644 --- a/tests/frame/estimated_size_test.py +++ b/tests/frame/estimated_size_test.py @@ -8,7 +8,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager data = {"a": list(range(100))} diff --git a/tests/frame/explode_test.py b/tests/frame/explode_test.py index 596eaa82a9..269ab4056f 100644 --- a/tests/frame/explode_test.py +++ b/tests/frame/explode_test.py @@ -6,11 +6,13 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError, ShapeError -from tests.utils import PANDAS_VERSION, POLARS_VERSION, Constructor, assert_equal_data +from tests.utils import PANDAS_VERSION, POLARS_VERSION, assert_equal_data if TYPE_CHECKING: from collections.abc import Sequence + from narwhals.testing.typing import Constructor + # For context, polars allows to explode multiple columns only if the columns # have matching element counts, therefore, l1 and l2 but not l1 and l3 together. data = { diff --git a/tests/frame/filter_test.py b/tests/frame/filter_test.py index f8048d8e4c..f34adeb54e 100644 --- a/tests/frame/filter_test.py +++ b/tests/frame/filter_test.py @@ -1,12 +1,15 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw from narwhals.exceptions import ColumnNotFoundError, InvalidOperationError -from tests.utils import Constructor, ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/frame/from_dict_test.py b/tests/frame/from_dict_test.py index 4378d9820f..280bee9d2e 100644 --- a/tests/frame/from_dict_test.py +++ b/tests/frame/from_dict_test.py @@ -6,10 +6,11 @@ import narwhals as nw from narwhals._utils import Implementation -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data if TYPE_CHECKING: from narwhals._typing import EagerAllowed, Polars + from narwhals.testing.typing import Constructor def test_from_dict(eager_backend: EagerAllowed) -> None: diff --git a/tests/frame/get_column_test.py b/tests/frame/get_column_test.py index 254dceffc8..3ec34968e5 100644 --- a/tests/frame/get_column_test.py +++ b/tests/frame/get_column_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_get_column(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/getitem_test.py b/tests/frame/getitem_test.py index c079d154bb..0b6f4c4fa4 100644 --- a/tests/frame/getitem_test.py +++ b/tests/frame/getitem_test.py @@ -6,9 +6,10 @@ import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, ConstructorEager, assert_equal_data +from tests.utils import PANDAS_VERSION, assert_equal_data if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager from narwhals.typing import _1DArray data: dict[str, Any] = { diff --git a/tests/frame/group_by_test.py b/tests/frame/group_by_test.py index 57aacae09b..f4903d63bf 100644 --- a/tests/frame/group_by_test.py +++ b/tests/frame/group_by_test.py @@ -15,14 +15,13 @@ PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, - Constructor, - ConstructorEager, assert_equal_data, ) if TYPE_CHECKING: from collections.abc import Mapping, Sequence + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import NonNestedLiteral diff --git a/tests/frame/head_test.py b/tests/frame/head_test.py index 1c9aaf4123..9d4ba41351 100644 --- a/tests/frame/head_test.py +++ b/tests/frame/head_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_head(constructor: Constructor) -> None: diff --git a/tests/frame/invalid_test.py b/tests/frame/invalid_test.py index d59b67adb1..2c0661d4cf 100644 --- a/tests/frame/invalid_test.py +++ b/tests/frame/invalid_test.py @@ -6,7 +6,10 @@ import narwhals as nw from narwhals.exceptions import MultiOutputExpressionError -from tests.utils import NUMPY_VERSION, POLARS_VERSION, Constructor +from tests.utils import NUMPY_VERSION, POLARS_VERSION + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor T = TypeVar("T") diff --git a/tests/frame/is_duplicated_test.py b/tests/frame/is_duplicated_test.py index 0703d2bd6c..0aaf09bdfc 100644 --- a/tests/frame/is_duplicated_test.py +++ b/tests/frame/is_duplicated_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_is_duplicated(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/is_empty_test.py b/tests/frame/is_empty_test.py index 5464d0e678..7f4125275f 100644 --- a/tests/frame/is_empty_test.py +++ b/tests/frame/is_empty_test.py @@ -7,7 +7,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager @pytest.mark.parametrize(("threshold", "expected"), [(0, False), (10, True)]) diff --git a/tests/frame/is_unique_test.py b/tests/frame/is_unique_test.py index 10e9af910d..d2769bc064 100644 --- a/tests/frame/is_unique_test.py +++ b/tests/frame/is_unique_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_is_unique(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/item_test.py b/tests/frame/item_test.py index 997b91d847..92b904f856 100644 --- a/tests/frame/item_test.py +++ b/tests/frame/item_test.py @@ -1,12 +1,15 @@ from __future__ import annotations import re -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager @pytest.mark.parametrize(("row", "column", "expected"), [(0, 2, 7), (1, "z", 8)]) diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py index 42d52adafc..bf06a62cab 100644 --- a/tests/frame/join_test.py +++ b/tests/frame/join_test.py @@ -7,15 +7,10 @@ import pytest import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - PANDAS_VERSION, - POLARS_VERSION, - Constructor, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data if TYPE_CHECKING: + from narwhals.testing.typing import Constructor from narwhals.typing import IntoDataFrame, IntoLazyFrameT, JoinStrategy diff --git a/tests/frame/lazy_test.py b/tests/frame/lazy_test.py index 9e671c68d2..258bacd73a 100644 --- a/tests/frame/lazy_test.py +++ b/tests/frame/lazy_test.py @@ -9,17 +9,12 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.dependencies import get_cudf, get_modin -from tests.utils import ( - DUCKDB_VERSION, - PANDAS_VERSION, - assert_equal_data, - pyspark_session, - sqlframe_session, -) +from narwhals.testing.constructors import pyspark_session, sqlframe_session +from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, assert_equal_data if TYPE_CHECKING: from narwhals._typing import LazyAllowed, SparkLike - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager data = {"a": [1, 2, 3], "b": ["x", "y", "z"]} diff --git a/tests/frame/len_test.py b/tests/frame/len_test.py index 3e709701c5..c403dd3da8 100644 --- a/tests/frame/len_test.py +++ b/tests/frame/len_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager data = {"a": [1.0, 2.0, None, 4.0], "b": [None, 3.0, None, 5.0]} diff --git a/tests/frame/null_count_test.py b/tests/frame/null_count_test.py index eec6475c92..181f106fc9 100644 --- a/tests/frame/null_count_test.py +++ b/tests/frame/null_count_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_null_count(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/pipe_test.py b/tests/frame/pipe_test.py index 0a2eac992d..24e2fd0e83 100644 --- a/tests/frame/pipe_test.py +++ b/tests/frame/pipe_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = {"a": ["foo", "bars"], "ab": ["foo", "bars"]} diff --git a/tests/frame/pivot_test.py b/tests/frame/pivot_test.py index 260006555e..1e1314e5c5 100644 --- a/tests/frame/pivot_test.py +++ b/tests/frame/pivot_test.py @@ -1,13 +1,16 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw from narwhals.exceptions import NarwhalsError -from tests.utils import POLARS_VERSION, ConstructorEager, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = { "ix": [1, 2, 1, 1, 2, 2], diff --git a/tests/frame/rename_test.py b/tests/frame/rename_test.py index 11ec77185c..9353d4f0d0 100644 --- a/tests/frame/rename_test.py +++ b/tests/frame/rename_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_rename(constructor: Constructor) -> None: diff --git a/tests/frame/row_test.py b/tests/frame/row_test.py index 186fbf3478..614f04d7c5 100644 --- a/tests/frame/row_test.py +++ b/tests/frame/row_test.py @@ -7,7 +7,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager def test_row_column(request: Any, constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/rows_test.py b/tests/frame/rows_test.py index 95fd2d8094..ac5dae41ed 100644 --- a/tests/frame/rows_test.py +++ b/tests/frame/rows_test.py @@ -8,7 +8,7 @@ from tests.utils import is_pd_na if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} data_na = {"a": [None, 3, 2], "b": [4, 4, 6], "z": [7.0, None, 9]} diff --git a/tests/frame/sample_test.py b/tests/frame/sample_test.py index b86ddaee1d..7bd11b46a0 100644 --- a/tests/frame/sample_test.py +++ b/tests/frame/sample_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager def test_sample_n(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/schema_test.py b/tests/frame/schema_test.py index d90916b029..b7147686cf 100644 --- a/tests/frame/schema_test.py +++ b/tests/frame/schema_test.py @@ -17,13 +17,13 @@ import polars as pl from typing_extensions import TypeAlias + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import ( DTypeBackend, IntoArrowSchema, IntoPandasSchema, IntoPolarsSchema, ) - from tests.utils import Constructor, ConstructorEager TimeUnit: TypeAlias = Literal["ns", "us"] @@ -588,8 +588,8 @@ def origin_pandas_like_pyarrow( if PANDAS_VERSION < (1, 5): pytest.skip(reason="pandas too old for `pyarrow`") name_pandas_like = {"pandas_pyarrow_constructor", "modin_pyarrow_constructor"} - if constructor_pandas_like.__name__ not in name_pandas_like: - pytest.skip(f"{constructor_pandas_like.__name__!r} is not pandas_like_pyarrow") + if str(constructor_pandas_like) not in name_pandas_like: + pytest.skip(f"{str(constructor_pandas_like)!r} is not pandas_like_pyarrow") data = { "a": [2, 1], "b": ["hello", "hi"], diff --git a/tests/frame/select_test.py b/tests/frame/select_test.py index ad8bce44f8..2a58de9a01 100644 --- a/tests/frame/select_test.py +++ b/tests/frame/select_test.py @@ -1,19 +1,15 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw from narwhals.exceptions import ColumnNotFoundError, InvalidIntoExprError, NarwhalsError -from tests.utils import ( - DASK_VERSION, - DUCKDB_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, - maybe_collect, -) +from tests.utils import DASK_VERSION, DUCKDB_VERSION, assert_equal_data, maybe_collect + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager class Foo: ... diff --git a/tests/frame/shape_test.py b/tests/frame/shape_test.py index 4a4cf710b9..884776ba43 100644 --- a/tests/frame/shape_test.py +++ b/tests/frame/shape_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager def test_shape(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/sink_parquet_test.py b/tests/frame/sink_parquet_test.py index 360828bded..fd8da6cd01 100644 --- a/tests/frame/sink_parquet_test.py +++ b/tests/frame/sink_parquet_test.py @@ -8,7 +8,7 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: - from tests.utils import Constructor + from narwhals.testing.typing import Constructor pytest.importorskip("pyarrow") diff --git a/tests/frame/sort_test.py b/tests/frame/sort_test.py index 4539d90606..18ab40a6b3 100644 --- a/tests/frame/sort_test.py +++ b/tests/frame/sort_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_sort(constructor: Constructor) -> None: diff --git a/tests/frame/tail_test.py b/tests/frame/tail_test.py index 4fdf4da8ef..461c52c614 100644 --- a/tests/frame/tail_test.py +++ b/tests/frame/tail_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_tail(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/to_arrow_test.py b/tests/frame/to_arrow_test.py index 3bb74a9dc7..a276851e2e 100644 --- a/tests/frame/to_arrow_test.py +++ b/tests/frame/to_arrow_test.py @@ -10,7 +10,7 @@ import pyarrow as pa if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager @pytest.mark.filterwarnings("ignore:.*is_sparse is deprecated:DeprecationWarning") diff --git a/tests/frame/to_dict_test.py b/tests/frame/to_dict_test.py index c382a0619f..5d54d45aa9 100644 --- a/tests/frame/to_dict_test.py +++ b/tests/frame/to_dict_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager @pytest.mark.filterwarnings( diff --git a/tests/frame/to_native_test.py b/tests/frame/to_native_test.py index 0ef0ae885a..ee48d2da23 100644 --- a/tests/frame/to_native_test.py +++ b/tests/frame/to_native_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import Constructor + from narwhals.testing.typing import Constructor def test_to_native(constructor: Constructor) -> None: diff --git a/tests/frame/to_numpy_test.py b/tests/frame/to_numpy_test.py index 9330a2c7b3..db892e90e1 100644 --- a/tests/frame/to_numpy_test.py +++ b/tests/frame/to_numpy_test.py @@ -12,7 +12,7 @@ from tests.utils import PANDAS_VERSION, is_windows if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager def test_to_numpy(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/to_pandas_test.py b/tests/frame/to_pandas_test.py index 473b685c19..1e37494dfb 100644 --- a/tests/frame/to_pandas_test.py +++ b/tests/frame/to_pandas_test.py @@ -11,7 +11,7 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager @pytest.mark.filterwarnings("ignore:.*Passing a BlockManager.*:DeprecationWarning") @@ -22,7 +22,7 @@ def test_convert_pandas(constructor_eager: ConstructorEager) -> None: df_raw = constructor_eager(data) result = nw.from_native(df_raw, eager_only=True).to_pandas() - if constructor_eager.__name__.startswith("pandas"): + if str(constructor_eager).startswith("pandas"): expected = cast("pd.DataFrame", constructor_eager(data)) elif "modin_pyarrow" in str(constructor_eager): expected = pd.DataFrame(data).convert_dtypes(dtype_backend="pyarrow") diff --git a/tests/frame/to_polars_test.py b/tests/frame/to_polars_test.py index 60ca653f32..11e44f344a 100644 --- a/tests/frame/to_polars_test.py +++ b/tests/frame/to_polars_test.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: from collections.abc import Mapping - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager pytest.importorskip("polars") import polars as pl diff --git a/tests/frame/top_k_test.py b/tests/frame/top_k_test.py index d0ba228df0..e6f3de79bc 100644 --- a/tests/frame/top_k_test.py +++ b/tests/frame/top_k_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, POLARS_VERSION, Constructor, assert_equal_data +from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_top_k(constructor: Constructor) -> None: diff --git a/tests/frame/unique_test.py b/tests/frame/unique_test.py index 691540d0a2..e4a44fff30 100644 --- a/tests/frame/unique_test.py +++ b/tests/frame/unique_test.py @@ -1,12 +1,15 @@ from __future__ import annotations -from typing import Literal +from typing import TYPE_CHECKING, Literal import pytest import narwhals as nw from narwhals.exceptions import ColumnNotFoundError, InvalidOperationError -from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import DUCKDB_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/frame/unpivot_test.py b/tests/frame/unpivot_test.py index 8d661ae1de..7ab51a1d49 100644 --- a/tests/frame/unpivot_test.py +++ b/tests/frame/unpivot_test.py @@ -6,10 +6,11 @@ import pytest import narwhals as nw -from tests.utils import PYARROW_VERSION, Constructor, assert_equal_data +from tests.utils import PYARROW_VERSION, assert_equal_data if TYPE_CHECKING: from narwhals.stable.v1.dtypes import DType + from narwhals.testing.typing import Constructor data = {"a": [7, 8, 9], "b": [1, 3, 5], "c": [2, 4, 6]} diff --git a/tests/frame/with_columns_sequence_test.py b/tests/frame/with_columns_sequence_test.py index 106f473886..274cf5a40b 100644 --- a/tests/frame/with_columns_sequence_test.py +++ b/tests/frame/with_columns_sequence_test.py @@ -3,10 +3,15 @@ import pytest pytest.importorskip("numpy") +from typing import TYPE_CHECKING + import numpy as np import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = {"a": ["foo", "bars"], "ab": ["foo", "bars"]} diff --git a/tests/frame/with_columns_test.py b/tests/frame/with_columns_test.py index e2325e9347..875dea0a01 100644 --- a/tests/frame/with_columns_test.py +++ b/tests/frame/with_columns_test.py @@ -1,16 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw from narwhals.exceptions import ColumnNotFoundError, ShapeError -from tests.utils import ( - PYARROW_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, - maybe_collect, -) +from tests.utils import PYARROW_VERSION, assert_equal_data, maybe_collect + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager def test_with_columns_int_col_name_pandas() -> None: diff --git a/tests/frame/with_row_index_test.py b/tests/frame/with_row_index_test.py index 10310ca7f6..b065e72080 100644 --- a/tests/frame/with_row_index_test.py +++ b/tests/frame/with_row_index_test.py @@ -5,18 +5,14 @@ import pytest import narwhals as nw -from tests.utils import ( - DUCKDB_VERSION, - PANDAS_VERSION, - POLARS_VERSION, - Constructor, - ConstructorEager, - assert_equal_data, -) +from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data if TYPE_CHECKING: from collections.abc import Sequence + from narwhals.testing.typing import Constructor, ConstructorEager + + data = {"abc": ["foo", "bars"], "xyz": [100, 200], "const": [42, 42]} diff --git a/tests/frame/write_csv_test.py b/tests/frame/write_csv_test.py index 5f907ac78c..9080e0319c 100644 --- a/tests/frame/write_csv_test.py +++ b/tests/frame/write_csv_test.py @@ -3,11 +3,13 @@ from typing import TYPE_CHECKING import narwhals as nw -from tests.utils import ConstructorEager, is_windows +from tests.utils import is_windows if TYPE_CHECKING: import pytest + from narwhals.testing.typing import ConstructorEager + def test_write_csv( constructor_eager: ConstructorEager, tmpdir: pytest.TempdirFactory diff --git a/tests/frame/write_parquet_test.py b/tests/frame/write_parquet_test.py index cf86f1be00..47b277f7b9 100644 --- a/tests/frame/write_parquet_test.py +++ b/tests/frame/write_parquet_test.py @@ -8,7 +8,7 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager pytest.importorskip("pyarrow") data = {"a": [1, 2, 3]} diff --git a/tests/from_dict_test.py b/tests/from_dict_test.py index 93ea5b2880..06d3764987 100644 --- a/tests/from_dict_test.py +++ b/tests/from_dict_test.py @@ -7,10 +7,11 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError from narwhals.utils import Implementation -from tests.utils import PYARROW_VERSION, Constructor, assert_equal_data +from tests.utils import PYARROW_VERSION, assert_equal_data if TYPE_CHECKING: from narwhals._typing import EagerAllowed, Polars + from narwhals.testing.typing import Constructor def test_from_dict(eager_backend: EagerAllowed) -> None: diff --git a/tests/from_numpy_test.py b/tests/from_numpy_test.py index e8ebcbc6f0..834114107c 100644 --- a/tests/from_numpy_test.py +++ b/tests/from_numpy_test.py @@ -8,9 +8,10 @@ import numpy as np import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager from narwhals.typing import _2DArray data = {"a": [1, 2, 3], "b": [4, 5, 6]} diff --git a/tests/hypothesis/getitem_test.py b/tests/hypothesis/getitem_test.py index 759a292f97..638922567f 100644 --- a/tests/hypothesis/getitem_test.py +++ b/tests/hypothesis/getitem_test.py @@ -7,7 +7,7 @@ from hypothesis import assume, given import narwhals as nw -from tests.conftest import pandas_constructor, pyarrow_table_constructor +from narwhals.testing.constructors import ConstructorName from tests.utils import assert_equal_data if TYPE_CHECKING: @@ -19,8 +19,10 @@ pytest.importorskip("polars") import polars as pl +params = [ConstructorName.PANDAS.constructor, ConstructorName.PYARROW.constructor] -@pytest.fixture(params=[pandas_constructor, pyarrow_table_constructor], scope="module") + +@pytest.fixture(params=params, scope="module") def pandas_or_pyarrow_constructor( request: pytest.FixtureRequest, ) -> Callable[[Any], IntoDataFrame]: @@ -125,7 +127,7 @@ def test_getitem(pandas_or_pyarrow_constructor: Any, selector: Any) -> None: # NotImplementedError: Slicing with step is not supported on PyArrow tables assume( not ( - pandas_or_pyarrow_constructor is pyarrow_table_constructor + pandas_or_pyarrow_constructor.name.is_pyarrow and isinstance(selector, slice) and selector.step is not None ) @@ -134,7 +136,7 @@ def test_getitem(pandas_or_pyarrow_constructor: Any, selector: Any) -> None: # NotImplementedError: Slicing with step is not supported on PyArrow tables assume( not ( - pandas_or_pyarrow_constructor is pyarrow_table_constructor + pandas_or_pyarrow_constructor.name.is_pyarrow and isinstance(selector, tuple) and ( (isinstance(selector[0], slice) and selector[0].step is not None) diff --git a/tests/ibis_test.py b/tests/ibis_test.py index 14a93c8ef8..cb9900e7d5 100644 --- a/tests/ibis_test.py +++ b/tests/ibis_test.py @@ -10,7 +10,7 @@ import ibis import polars as pl - from tests.utils import Constructor + from narwhals.testing.typing import Constructor else: ibis = pytest.importorskip("ibis") pl = pytest.importorskip("polars") diff --git a/tests/joblib_test.py b/tests/joblib_test.py index 72b9440544..288562928e 100644 --- a/tests/joblib_test.py +++ b/tests/joblib_test.py @@ -1,16 +1,19 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data pytest.importorskip("joblib") from joblib import Parallel, delayed +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager + def test_parallelisability(constructor_eager: ConstructorEager) -> None: # https://github.com/narwhals-dev/narwhals/issues/2450 diff --git a/tests/modern_polars/ewm_mean_test.py b/tests/modern_polars/ewm_mean_test.py index 0b3ea06923..b34f5650c9 100644 --- a/tests/modern_polars/ewm_mean_test.py +++ b/tests/modern_polars/ewm_mean_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, ConstructorEager, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_ew_mean( diff --git a/tests/modern_polars/filter_test.py b/tests/modern_polars/filter_test.py index a6a0664cf8..bd51934573 100644 --- a/tests/modern_polars/filter_test.py +++ b/tests/modern_polars/filter_test.py @@ -1,9 +1,13 @@ from __future__ import annotations from datetime import datetime +from typing import TYPE_CHECKING import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_filter(constructor: Constructor) -> None: diff --git a/tests/modern_polars/method_chaining_2_test.py b/tests/modern_polars/method_chaining_2_test.py index 58451e9f9a..4845728ef6 100644 --- a/tests/modern_polars/method_chaining_2_test.py +++ b/tests/modern_polars/method_chaining_2_test.py @@ -1,9 +1,13 @@ from __future__ import annotations from datetime import datetime +from typing import TYPE_CHECKING import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = { "flight_date": [ diff --git a/tests/modern_polars/method_chaining_test.py b/tests/modern_polars/method_chaining_test.py index 611f85973e..42798cf532 100644 --- a/tests/modern_polars/method_chaining_test.py +++ b/tests/modern_polars/method_chaining_test.py @@ -1,11 +1,15 @@ from __future__ import annotations import re +from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, Constructor, assert_equal_data +from tests.utils import PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = { "OriginCityName": [ @@ -38,10 +42,7 @@ def test_split_list_get(request: pytest.FixtureRequest, constructor: Constructor if PANDAS_VERSION < (2, 2): pytest.skip() pytest.importorskip("pyarrow") - if ( - constructor.__name__.startswith("pandas") - and "pyarrow" not in constructor.__name__ - ): + if str(constructor).startswith("pandas") and "pyarrow" not in str(constructor): df = nw.from_native(constructor(data)) msg = re.escape("This operation requires a pyarrow-backed series. ") with pytest.raises(TypeError, match=msg): diff --git a/tests/modern_polars/performance_test.py b/tests/modern_polars/performance_test.py index b765055cbd..03462bcd7f 100644 --- a/tests/modern_polars/performance_test.py +++ b/tests/modern_polars/performance_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor data = {"weight": ["89kg", "83", "79kg", "68kg", "78kg", "73", "86kg"]} diff --git a/tests/modern_polars/pivot_test.py b/tests/modern_polars/pivot_test.py index b6e6b16dde..41accde4ab 100644 --- a/tests/modern_polars/pivot_test.py +++ b/tests/modern_polars/pivot_test.py @@ -1,11 +1,15 @@ from __future__ import annotations from datetime import datetime +from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, ConstructorEager, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_pivot( diff --git a/tests/modern_polars/unpivot_test.py b/tests/modern_polars/unpivot_test.py index c5bff87dc6..b5ae2b192e 100644 --- a/tests/modern_polars/unpivot_test.py +++ b/tests/modern_polars/unpivot_test.py @@ -1,9 +1,13 @@ from __future__ import annotations from datetime import datetime +from typing import TYPE_CHECKING import narwhals as nw -from tests.utils import Constructor, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor def test_unpivot(constructor: Constructor) -> None: diff --git a/tests/namespace_test.py b/tests/namespace_test.py index e94a6690c1..46623b266c 100644 --- a/tests/namespace_test.py +++ b/tests/namespace_test.py @@ -21,8 +21,8 @@ from narwhals._pandas_like.namespace import PandasLikeNamespace # noqa: F401 from narwhals._polars.namespace import PolarsNamespace # noqa: F401 from narwhals._typing import BackendName, _EagerAllowed + from narwhals.testing.typing import Constructor from narwhals.typing import _2DArray - from tests.utils import Constructor ExprT = TypeVar("ExprT", bound="CompliantExprAny") diff --git a/tests/new_series_test.py b/tests/new_series_test.py index 9360ef6fdc..a311694bf7 100644 --- a/tests/new_series_test.py +++ b/tests/new_series_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_new_series(constructor_eager: ConstructorEager) -> None: diff --git a/tests/read_scan_test.py b/tests/read_scan_test.py index 4548f76a87..487132382b 100644 --- a/tests/read_scan_test.py +++ b/tests/read_scan_test.py @@ -6,13 +6,8 @@ import pytest import narwhals as nw -from tests.utils import ( - PANDAS_VERSION, - Constructor, - assert_equal_data, - pyspark_session, - sqlframe_session, -) +from narwhals.testing.constructors import pyspark_session, sqlframe_session +from tests.utils import PANDAS_VERSION, assert_equal_data pytest.importorskip("polars") pytest.importorskip("pyarrow") @@ -26,6 +21,7 @@ from typing_extensions import TypeAlias from narwhals._typing import EagerAllowed, _LazyOnly, _SparkLike + from narwhals.testing.typing import Constructor from narwhals.typing import FileSource Factory: TypeAlias = pytest.TempPathFactory diff --git a/tests/selectors_test.py b/tests/selectors_test.py index 63c6b387e5..d1a8c562cf 100644 --- a/tests/selectors_test.py +++ b/tests/selectors_test.py @@ -2,7 +2,7 @@ import re from datetime import datetime, timezone -from typing import Literal +from typing import TYPE_CHECKING, Literal import pytest @@ -12,11 +12,13 @@ PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, - Constructor, assert_equal_data, is_windows, ) +if TYPE_CHECKING: + from narwhals.testing.typing import Constructor + data = { "a": [1, 1, 2], "b": ["a", "b", "c"], diff --git a/tests/series_only/__contains___test.py b/tests/series_only/__contains___test.py index 13bc0eb4e0..c220e6570e 100644 --- a/tests/series_only/__contains___test.py +++ b/tests/series_only/__contains___test.py @@ -8,7 +8,7 @@ from narwhals.exceptions import InvalidOperationError if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager data = [100, 200, None] diff --git a/tests/series_only/__iter___test.py b/tests/series_only/__iter___test.py index 5ed9e083f9..201e04a677 100644 --- a/tests/series_only/__iter___test.py +++ b/tests/series_only/__iter___test.py @@ -9,7 +9,7 @@ from tests.utils import assert_equal_data if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager data = [1, 2, 3] diff --git a/tests/series_only/alias_rename_test.py b/tests/series_only/alias_rename_test.py index e8420e8089..a609cee024 100644 --- a/tests/series_only/alias_rename_test.py +++ b/tests/series_only/alias_rename_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_alias_rename(constructor_eager: ConstructorEager) -> None: diff --git a/tests/series_only/arg_max_test.py b/tests/series_only/arg_max_test.py index c6460b7183..9e43034b24 100644 --- a/tests/series_only/arg_max_test.py +++ b/tests/series_only/arg_max_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0], "i": [3, 1, 5]} diff --git a/tests/series_only/arg_min_test.py b/tests/series_only/arg_min_test.py index 34a38a57f9..1edf98889d 100644 --- a/tests/series_only/arg_min_test.py +++ b/tests/series_only/arg_min_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/series_only/arg_true_test.py b/tests/series_only/arg_true_test.py index ccc25a83e7..3942d1331b 100644 --- a/tests/series_only/arg_true_test.py +++ b/tests/series_only/arg_true_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_arg_true_series(constructor_eager: ConstructorEager) -> None: diff --git a/tests/series_only/array_dunder_test.py b/tests/series_only/array_dunder_test.py index b50d033aa2..4a7a6a3a14 100644 --- a/tests/series_only/array_dunder_test.py +++ b/tests/series_only/array_dunder_test.py @@ -3,15 +3,15 @@ import pytest pytest.importorskip("numpy") +from typing import TYPE_CHECKING + import numpy as np import narwhals as nw -from tests.utils import ( - PANDAS_VERSION, - PYARROW_VERSION, - ConstructorEager, - assert_equal_data, -) +from tests.utils import PANDAS_VERSION, PYARROW_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_array_dunder( diff --git a/tests/series_only/cast_test.py b/tests/series_only/cast_test.py index b646f38299..622f5da8ca 100644 --- a/tests/series_only/cast_test.py +++ b/tests/series_only/cast_test.py @@ -9,7 +9,7 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager def test_cast_253( diff --git a/tests/series_only/gather_every_test.py b/tests/series_only/gather_every_test.py index e7d55fb03c..5c8d2b238d 100644 --- a/tests/series_only/gather_every_test.py +++ b/tests/series_only/gather_every_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = {"a": list(range(10))} diff --git a/tests/series_only/getitem_test.py b/tests/series_only/getitem_test.py index aa63ac8414..ce8a7eb549 100644 --- a/tests/series_only/getitem_test.py +++ b/tests/series_only/getitem_test.py @@ -8,7 +8,7 @@ from tests.utils import assert_equal_data, assert_equal_series if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager def test_by_slice(constructor_eager: ConstructorEager) -> None: diff --git a/tests/series_only/head_test.py b/tests/series_only/head_test.py index 5697bc59e5..ae09fcc6df 100644 --- a/tests/series_only/head_test.py +++ b/tests/series_only/head_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager @pytest.mark.parametrize("n", [2, -1]) diff --git a/tests/series_only/hist_test.py b/tests/series_only/hist_test.py index 183c0a13ff..34f4d2523a 100644 --- a/tests/series_only/hist_test.py +++ b/tests/series_only/hist_test.py @@ -11,11 +11,13 @@ import narwhals as nw from narwhals.exceptions import ComputeError -from tests.utils import POLARS_VERSION, ConstructorEager, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data if TYPE_CHECKING: from collections.abc import Sequence + from narwhals.testing.typing import ConstructorEager + rnd = Random(0) # noqa: S311 data: dict[str, Any] = { diff --git a/tests/series_only/is_empty_test.py b/tests/series_only/is_empty_test.py index cfdc8bd15d..43c885f645 100644 --- a/tests/series_only/is_empty_test.py +++ b/tests/series_only/is_empty_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager def test_is_empty(constructor_eager: ConstructorEager) -> None: diff --git a/tests/series_only/is_ordered_categorical_test.py b/tests/series_only/is_ordered_categorical_test.py index 8833957b1c..55f1057688 100644 --- a/tests/series_only/is_ordered_categorical_test.py +++ b/tests/series_only/is_ordered_categorical_test.py @@ -9,7 +9,7 @@ from tests.utils import POLARS_VERSION if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager class MockCompliantSeries: diff --git a/tests/series_only/is_sorted_test.py b/tests/series_only/is_sorted_test.py index 046669aac0..799807e685 100644 --- a/tests/series_only/is_sorted_test.py +++ b/tests/series_only/is_sorted_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = [1, 3, 2] data_dups = [4, 4, 6] diff --git a/tests/series_only/item_test.py b/tests/series_only/item_test.py index 521ce5af55..de267acb64 100644 --- a/tests/series_only/item_test.py +++ b/tests/series_only/item_test.py @@ -1,11 +1,15 @@ from __future__ import annotations import re +from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = [1, 3, 2] diff --git a/tests/series_only/scatter_test.py b/tests/series_only/scatter_test.py index d608f78f1a..5046df0cdc 100644 --- a/tests/series_only/scatter_test.py +++ b/tests/series_only/scatter_test.py @@ -6,11 +6,13 @@ import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data, assert_equal_series +from tests.utils import assert_equal_data, assert_equal_series if TYPE_CHECKING: from collections.abc import Collection + from narwhals.testing.typing import ConstructorEager + def series(frame: ConstructorEager, name: str, values: Collection[Any]) -> nw.Series[Any]: return nw.from_native(frame({name: values})).get_column(name) diff --git a/tests/series_only/shape_test.py b/tests/series_only/shape_test.py index 8f8ba638e5..efad8917bc 100644 --- a/tests/series_only/shape_test.py +++ b/tests/series_only/shape_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager def test_shape(constructor_eager: ConstructorEager) -> None: diff --git a/tests/series_only/sort_test.py b/tests/series_only/sort_test.py index 8b74c6623b..def1013670 100644 --- a/tests/series_only/sort_test.py +++ b/tests/series_only/sort_test.py @@ -8,7 +8,7 @@ from tests.utils import assert_equal_data if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager @pytest.mark.parametrize( diff --git a/tests/series_only/tail_test.py b/tests/series_only/tail_test.py index 43f9866094..6b6e2573d2 100644 --- a/tests/series_only/tail_test.py +++ b/tests/series_only/tail_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager @pytest.mark.parametrize("n", [2, -1]) diff --git a/tests/series_only/to_arrow_test.py b/tests/series_only/to_arrow_test.py index 6675ea2e31..a3c7c54b84 100644 --- a/tests/series_only/to_arrow_test.py +++ b/tests/series_only/to_arrow_test.py @@ -11,7 +11,7 @@ import pyarrow.compute as pc if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager def test_to_arrow(constructor_eager: ConstructorEager) -> None: diff --git a/tests/series_only/to_dummy_test.py b/tests/series_only/to_dummy_test.py index df301a6978..f819e5d447 100644 --- a/tests/series_only/to_dummy_test.py +++ b/tests/series_only/to_dummy_test.py @@ -1,9 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = ["x", "y", "z"] data_na = ["x", "y", None] diff --git a/tests/series_only/to_frame_test.py b/tests/series_only/to_frame_test.py index 8f56f7f6fa..10a10ee732 100644 --- a/tests/series_only/to_frame_test.py +++ b/tests/series_only/to_frame_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = [1, 2, 3] diff --git a/tests/series_only/to_list_test.py b/tests/series_only/to_list_test.py index e532f54799..859a6e92f4 100644 --- a/tests/series_only/to_list_test.py +++ b/tests/series_only/to_list_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = [1, 2, 3] diff --git a/tests/series_only/to_native_test.py b/tests/series_only/to_native_test.py index 350d81764d..6780c1c73a 100644 --- a/tests/series_only/to_native_test.py +++ b/tests/series_only/to_native_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager data = [4, 4, 4, 1, 6, 6, 4, 4, 1, 1] diff --git a/tests/series_only/to_numpy_test.py b/tests/series_only/to_numpy_test.py index 21873760cb..c08945be88 100644 --- a/tests/series_only/to_numpy_test.py +++ b/tests/series_only/to_numpy_test.py @@ -13,7 +13,7 @@ from tests.utils import PANDAS_VERSION, is_windows if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager def test_to_numpy( diff --git a/tests/series_only/to_pandas_test.py b/tests/series_only/to_pandas_test.py index 95cd969f14..7e616f971c 100644 --- a/tests/series_only/to_pandas_test.py +++ b/tests/series_only/to_pandas_test.py @@ -12,7 +12,7 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager data = [1, 3, 2] diff --git a/tests/series_only/to_polars_test.py b/tests/series_only/to_polars_test.py index c99d6e889e..f3bfe0a223 100644 --- a/tests/series_only/to_polars_test.py +++ b/tests/series_only/to_polars_test.py @@ -7,7 +7,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager pytest.importorskip("polars") import polars as pl diff --git a/tests/series_only/value_counts_test.py b/tests/series_only/value_counts_test.py index 77a7656cf2..41dae97228 100644 --- a/tests/series_only/value_counts_test.py +++ b/tests/series_only/value_counts_test.py @@ -1,11 +1,14 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, ConstructorEager, assert_equal_data +from tests.utils import PANDAS_VERSION, assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager data = [4, 4, 4, 1, 6, 6, 4, 4, 1, 1] diff --git a/tests/series_only/zip_with_test.py b/tests/series_only/zip_with_test.py index b40ac19fd6..d77055689c 100644 --- a/tests/series_only/zip_with_test.py +++ b/tests/series_only/zip_with_test.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import narwhals as nw -from tests.utils import ConstructorEager, assert_equal_data +from tests.utils import assert_equal_data + +if TYPE_CHECKING: + from narwhals.testing.typing import ConstructorEager def test_zip_with(constructor_eager: ConstructorEager) -> None: diff --git a/tests/testing/assert_frame_equal_test.py b/tests/testing/assert_frame_equal_test.py index c1b3b4e357..58365297f8 100644 --- a/tests/testing/assert_frame_equal_test.py +++ b/tests/testing/assert_frame_equal_test.py @@ -12,9 +12,9 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import IntoSchema from tests.conftest import Data - from tests.utils import Constructor, ConstructorEager def _assertion_error(detail: str) -> pytest.RaisesExc: diff --git a/tests/testing/assert_series_equal_test.py b/tests/testing/assert_series_equal_test.py index c4826c695e..5ab9f2a515 100644 --- a/tests/testing/assert_series_equal_test.py +++ b/tests/testing/assert_series_equal_test.py @@ -13,9 +13,9 @@ if TYPE_CHECKING: from typing_extensions import TypeAlias + from narwhals.testing.typing import ConstructorEager from narwhals.typing import IntoSchema, IntoSeriesT from tests.conftest import Data - from tests.utils import ConstructorEager SetupFn: TypeAlias = Callable[[nw.Series[Any]], tuple[nw.Series[Any], nw.Series[Any]]] diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py new file mode 100644 index 0000000000..473ab22f80 --- /dev/null +++ b/tests/testing/constructors_test.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import narwhals as nw +from narwhals.testing.constructors import ConstructorName +from narwhals.testing.constructors._classes import DaskConstructor + +if TYPE_CHECKING: + import pytest + +pytest_plugins = ["pytester"] + + +def test_dask_npartitions_distinct() -> None: + dp1, dp2 = DaskConstructor(npartitions=1), DaskConstructor(npartitions=2) + assert dp1 != dp2 + assert hash(dp1) != hash(dp2) + + +def test_dask_repr() -> None: + assert repr(DaskConstructor(npartitions=3)) == "DaskConstructor(npartitions=3)" + + +def test_eager_returns_eager_frame() -> None: + data = {"x": [1, 2, 3]} + constructor = ConstructorName.PANDAS.constructor + df = nw.from_native(constructor(data)) + assert isinstance(df, nw.DataFrame) + + +def test_lazy_returns_lazy_frame() -> None: + data = {"x": [1, 2, 3]} + constructor = ConstructorName.POLARS_LAZY.constructor + lf = nw.from_native(constructor(data)) + assert isinstance(lf, nw.LazyFrame) + + +def test_constructor_eager_fixture_runs_for_each_backend( + pytester: pytest.Pytester, +) -> None: + pytester.makeconftest("") + pytester.makepyfile(""" + import narwhals as nw + from narwhals.testing.typing import ConstructorEager + + def test_shape(constructor_eager: ConstructorEager) -> None: + df = nw.from_native(constructor_eager({"x": [1, 2, 3]}), eager_only=True) + assert df.shape == (3, 1) + """) + result = pytester.runpytest_subprocess( + "-v", "-p", "no:randomly", "--constructors=pandas,polars[eager],pyarrow" + ) + result.assert_outcomes(passed=3) + result.stdout.fnmatch_lines( + [ + "*test_shape?pandas?*", + "*test_shape?polars[[]eager[]]?*", + "*test_shape?pyarrow?*", + ] + ) + + +def test_constructor_fixture_includes_lazy_backends(pytester: pytest.Pytester) -> None: + pytester.makeconftest("") + pytester.makepyfile(""" + import narwhals as nw + from narwhals.testing.typing import Constructor + + def test_columns(constructor: Constructor) -> None: + df = nw.from_native(constructor({"x": [1, 2, 3]})) + assert df.collect_schema().names() == ["x"] + """) + result = pytester.runpytest_subprocess( + "-v", "--constructors=pandas,polars[lazy],duckdb" + ) + result.assert_outcomes(passed=3) + + +def test_external_constructor_disables_parametrisation(pytester: pytest.Pytester) -> None: + pytester.makeconftest("") + pytester.makepyfile(""" + from narwhals.testing.typing import ConstructorEager + + def test_unparam(constructor_eager: ConstructorEager) -> None: + pass + """) + result = pytester.runpytest_subprocess("--use-external-constructor") + # Without external parametrisation in place, the fixture is missing. + result.assert_outcomes(errors=1) diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 8d076699c0..33da5bc8e4 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -30,8 +30,8 @@ import narwhals as nw from narwhals._utils import Version -from tests.conftest import sqlframe_pyspark_lazy_constructor -from tests.utils import Constructor, maybe_get_modin_df +from narwhals.testing.constructors import ConstructorName +from tests.utils import maybe_get_modin_df if TYPE_CHECKING: from collections.abc import Iterable, Iterator @@ -39,6 +39,8 @@ from _pytest.mark import ParameterSet from typing_extensions import assert_type + from narwhals.testing.typing import Constructor + class MockDataFrame: def __init__(self, version: Version) -> None: @@ -294,7 +296,8 @@ def test_eager_only_lazy_dask(eager_only: Any, context: Any) -> None: def test_series_only_sqlframe() -> None: # pragma: no cover pytest.importorskip("sqlframe") - df = sqlframe_pyspark_lazy_constructor(data) + constructor = ConstructorName.SQLFRAME.constructor + df = constructor(data) with pytest.raises(TypeError, match="Cannot only use `series_only`"): nw.from_native(df, series_only=True) # pyright: ignore[reportArgumentType, reportCallIssue] @@ -315,7 +318,8 @@ def test_series_only_sqlframe() -> None: # pragma: no cover ) def test_eager_only_sqlframe(eager_only: Any, context: Any) -> None: # pragma: no cover pytest.importorskip("sqlframe") - df = sqlframe_pyspark_lazy_constructor(data) + constructor = ConstructorName.SQLFRAME.constructor + df = constructor(data) with context: res = nw.from_native(df, eager_only=eager_only) diff --git a/tests/translate/get_native_namespace_test.py b/tests/translate/get_native_namespace_test.py index 821443ea64..9f3541584b 100644 --- a/tests/translate/get_native_namespace_test.py +++ b/tests/translate/get_native_namespace_test.py @@ -7,7 +7,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import Constructor, ConstructorEager + from narwhals.testing.typing import Constructor, ConstructorEager data = {"a": [1, 2, 3]} @@ -76,7 +76,7 @@ def test_native_namespace_frame(constructor: Constructor) -> None: def test_native_namespace_series(constructor_eager: ConstructorEager) -> None: - constructor_name = constructor_eager.__name__ + constructor_name = str(constructor_eager) expected_namespace = _get_expected_namespace(constructor_name=constructor_name) diff --git a/tests/translate/to_native_test.py b/tests/translate/to_native_test.py index f84cf80dd6..594c95f08c 100644 --- a/tests/translate/to_native_test.py +++ b/tests/translate/to_native_test.py @@ -8,7 +8,7 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import ConstructorEager + from narwhals.testing.typing import ConstructorEager @pytest.mark.parametrize( diff --git a/tests/utils.py b/tests/utils.py index 4d01223b2a..1b470bd71e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,7 +6,7 @@ import warnings from datetime import date, datetime from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import TYPE_CHECKING, Any, Callable import pytest @@ -19,12 +19,10 @@ from collections.abc import Mapping, Sequence import pandas as pd - from pyspark.sql import SparkSession - from sqlframe.duckdb import DuckDBSession from typing_extensions import TypeAlias - from narwhals._native import NativeLazyFrame - from narwhals.typing import Frame, IntoDataFrame, TimeUnit + from narwhals.testing.typing import Constructor, ConstructorEager + from narwhals.typing import Frame, TimeUnit def get_module_version_as_tuple(module_name: str) -> tuple[int, ...]: @@ -44,9 +42,6 @@ def get_module_version_as_tuple(module_name: str) -> tuple[int, ...]: PYSPARK_VERSION: tuple[int, ...] = get_module_version_as_tuple("pyspark") CUDF_VERSION: tuple[int, ...] = get_module_version_as_tuple("cudf") -Constructor: TypeAlias = Callable[[Any], "NativeLazyFrame | IntoDataFrame"] -ConstructorEager: TypeAlias = Callable[[Any], "IntoDataFrame"] -ConstructorLazy: TypeAlias = Callable[[Any], "NativeLazyFrame"] ConstructorPandasLike: TypeAlias = Callable[[Any], "pd.DataFrame"] NestedOrEnumDType: TypeAlias = "nw.List | nw.Array | nw.Struct | nw.Enum" @@ -174,33 +169,6 @@ def assert_equal_hash(left: Any, right: Any) -> None: ) -def sqlframe_session() -> DuckDBSession: - from sqlframe.duckdb import DuckDBSession - - # NOTE: `__new__` override inferred by `pyright` only - # https://github.com/eakmanrq/sqlframe/blob/772b3a6bfe5a1ffd569b7749d84bea2f3a314510/sqlframe/base/session.py#L181-L184 - return cast("DuckDBSession", DuckDBSession()) # type: ignore[redundant-cast] - - -def pyspark_session() -> SparkSession: # pragma: no cover - if is_spark_connect := os.environ.get("SPARK_CONNECT", None): - from pyspark.sql.connect.session import SparkSession - else: - from pyspark.sql import SparkSession - builder = cast("SparkSession.Builder", SparkSession.builder).appName("unit-tests") - builder = ( - builder.remote(f"sc://localhost:{os.environ.get('SPARK_PORT', '15002')}") - if is_spark_connect - else builder.master("local[1]").config("spark.ui.enabled", "false") - ) - return ( - builder.config("spark.default.parallelism", "1") - .config("spark.sql.shuffle.partitions", "2") - .config("spark.sql.session.timeZone", "UTC") - .getOrCreate() - ) - - def maybe_get_modin_df(df_pandas: pd.DataFrame) -> Any: # pragma: no cover """Convert a pandas DataFrame to a Modin DataFrame if Modin is available.""" try: @@ -230,10 +198,7 @@ def is_pyarrow_windows_no_tzdata(constructor: Constructor, /) -> bool: def uses_pyarrow_backend(constructor: Constructor | ConstructorEager) -> bool: """Checks if the pandas-like constructor uses pyarrow backend.""" - return constructor.__name__ in { - "pandas_pyarrow_constructor", - "modin_pyarrow_constructor", - } + return str(constructor) in {"pandas_pyarrow_constructor", "modin_pyarrow_constructor"} def maybe_collect(df: Frame) -> Frame: diff --git a/tests/v1_test.py b/tests/v1_test.py index 9882c4ed15..a71daa087f 100644 --- a/tests/v1_test.py +++ b/tests/v1_test.py @@ -36,8 +36,6 @@ PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, - Constructor, - ConstructorEager, assert_equal_data, assert_equal_hash, assert_equal_series, @@ -51,8 +49,8 @@ from narwhals._typing import EagerAllowed from narwhals.dtypes import DType from narwhals.stable.v1.typing import IntoDataFrameT + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import IntoDType, _1DArray, _2DArray - from tests.utils import Constructor, ConstructorEager def test_toplevel() -> None: diff --git a/tests/v2_test.py b/tests/v2_test.py index 7a1903425c..94576d9bb3 100644 --- a/tests/v2_test.py +++ b/tests/v2_test.py @@ -18,8 +18,6 @@ PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, - Constructor, - ConstructorEager, assert_equal_data, assert_equal_series, ) @@ -31,6 +29,7 @@ from narwhals._typing import EagerAllowed from narwhals.stable.v2.typing import IntoDataFrameT + from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import IntoDType, _1DArray, _2DArray From a6361f97430e1ba68209c1265d73e150d18501cf Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 00:44:56 +0200 Subject: [PATCH 05/71] Adjust TPCH --- tpch/tests/conftest.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tpch/tests/conftest.py b/tpch/tests/conftest.py index d98c4b401a..499571a567 100644 --- a/tpch/tests/conftest.py +++ b/tpch/tests/conftest.py @@ -36,13 +36,6 @@ def pytest_configure(config: pytest.Config) -> None: def pytest_addoption(parser: pytest.Parser) -> None: - from tests.conftest import DEFAULT_CONSTRUCTORS - - parser.addoption( - "--constructors", - default=DEFAULT_CONSTRUCTORS, - help="", - ) parser.addoption( "--scale-factor", default=constants.SCALE_FACTOR_DEFAULT, From 8c0f27ffcb0bbc402be54464ba7699e676b7b3a8 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 00:47:31 +0200 Subject: [PATCH 06/71] split testing plugin and constructors into multiple test files --- tests/testing/constructors_test.py | 61 ----------------------------- tests/testing/plugin_test.py | 63 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 61 deletions(-) create mode 100644 tests/testing/plugin_test.py diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index 473ab22f80..0821273ca9 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -1,16 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw from narwhals.testing.constructors import ConstructorName from narwhals.testing.constructors._classes import DaskConstructor -if TYPE_CHECKING: - import pytest - -pytest_plugins = ["pytester"] - def test_dask_npartitions_distinct() -> None: dp1, dp2 = DaskConstructor(npartitions=1), DaskConstructor(npartitions=2) @@ -34,57 +27,3 @@ def test_lazy_returns_lazy_frame() -> None: constructor = ConstructorName.POLARS_LAZY.constructor lf = nw.from_native(constructor(data)) assert isinstance(lf, nw.LazyFrame) - - -def test_constructor_eager_fixture_runs_for_each_backend( - pytester: pytest.Pytester, -) -> None: - pytester.makeconftest("") - pytester.makepyfile(""" - import narwhals as nw - from narwhals.testing.typing import ConstructorEager - - def test_shape(constructor_eager: ConstructorEager) -> None: - df = nw.from_native(constructor_eager({"x": [1, 2, 3]}), eager_only=True) - assert df.shape == (3, 1) - """) - result = pytester.runpytest_subprocess( - "-v", "-p", "no:randomly", "--constructors=pandas,polars[eager],pyarrow" - ) - result.assert_outcomes(passed=3) - result.stdout.fnmatch_lines( - [ - "*test_shape?pandas?*", - "*test_shape?polars[[]eager[]]?*", - "*test_shape?pyarrow?*", - ] - ) - - -def test_constructor_fixture_includes_lazy_backends(pytester: pytest.Pytester) -> None: - pytester.makeconftest("") - pytester.makepyfile(""" - import narwhals as nw - from narwhals.testing.typing import Constructor - - def test_columns(constructor: Constructor) -> None: - df = nw.from_native(constructor({"x": [1, 2, 3]})) - assert df.collect_schema().names() == ["x"] - """) - result = pytester.runpytest_subprocess( - "-v", "--constructors=pandas,polars[lazy],duckdb" - ) - result.assert_outcomes(passed=3) - - -def test_external_constructor_disables_parametrisation(pytester: pytest.Pytester) -> None: - pytester.makeconftest("") - pytester.makepyfile(""" - from narwhals.testing.typing import ConstructorEager - - def test_unparam(constructor_eager: ConstructorEager) -> None: - pass - """) - result = pytester.runpytest_subprocess("--use-external-constructor") - # Without external parametrisation in place, the fixture is missing. - result.assert_outcomes(errors=1) diff --git a/tests/testing/plugin_test.py b/tests/testing/plugin_test.py new file mode 100644 index 0000000000..c3eaf8c2fc --- /dev/null +++ b/tests/testing/plugin_test.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import pytest + + +pytest_plugins = ["pytester"] + + +def test_constructor_eager_fixture_runs_for_each_backend( + pytester: pytest.Pytester, +) -> None: + pytester.makeconftest("") + pytester.makepyfile(""" + import narwhals as nw + from narwhals.testing.typing import ConstructorEager + + def test_shape(constructor_eager: ConstructorEager) -> None: + df = nw.from_native(constructor_eager({"x": [1, 2, 3]}), eager_only=True) + assert df.shape == (3, 1) + """) + result = pytester.runpytest_subprocess( + "-v", "-p", "no:randomly", "--constructors=pandas,polars[eager],pyarrow" + ) + result.assert_outcomes(passed=3) + result.stdout.fnmatch_lines( + [ + "*test_shape?pandas?*", + "*test_shape?polars[[]eager[]]?*", + "*test_shape?pyarrow?*", + ] + ) + + +def test_constructor_fixture_includes_lazy_backends(pytester: pytest.Pytester) -> None: + pytester.makeconftest("") + pytester.makepyfile(""" + import narwhals as nw + from narwhals.testing.typing import Constructor + + def test_columns(constructor: Constructor) -> None: + df = nw.from_native(constructor({"x": [1, 2, 3]})) + assert df.collect_schema().names() == ["x"] + """) + result = pytester.runpytest_subprocess( + "-v", "--constructors=pandas,polars[lazy],duckdb" + ) + result.assert_outcomes(passed=3) + + +def test_external_constructor_disables_parametrisation(pytester: pytest.Pytester) -> None: + pytester.makeconftest("") + pytester.makepyfile(""" + from narwhals.testing.typing import ConstructorEager + + def test_unparam(constructor_eager: ConstructorEager) -> None: + pass + """) + result = pytester.runpytest_subprocess("--use-external-constructor") + # Without external parametrisation in place, the fixture is missing. + result.assert_outcomes(errors=1) From 24743cebad15d62dc4dbebafa482104103def1d2 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 10:54:32 +0200 Subject: [PATCH 07/71] mix with Dan implementation --- narwhals/testing/constructors/__init__.py | 40 +++ narwhals/testing/constructors/_classes.py | 283 +++++++++++++++------- narwhals/testing/constructors/_name.py | 68 ++++-- tests/expr_and_series/corr_test.py | 8 +- 4 files changed, 285 insertions(+), 114 deletions(-) diff --git a/narwhals/testing/constructors/__init__.py b/narwhals/testing/constructors/__init__.py index 9b57ff7230..fb9d60754a 100644 --- a/narwhals/testing/constructors/__init__.py +++ b/narwhals/testing/constructors/__init__.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from narwhals.testing.constructors._classes import ( ConstructorBase, ConstructorEagerBase, @@ -8,6 +10,9 @@ ) from narwhals.testing.constructors._name import ConstructorName +if TYPE_CHECKING: + from collections.abc import Iterable + __all__ = ( "ALL_CONSTRUCTORS", "ALL_CPU_CONSTRUCTORS", @@ -17,6 +22,7 @@ "ConstructorName", "available_constructors", "get_constructor", + "prepare_constructors", "pyspark_session", "sqlframe_session", ) @@ -44,6 +50,7 @@ ALL_CPU_CONSTRUCTORS: frozenset[ConstructorName] = frozenset( name for name in ConstructorName if not name.needs_gpu ) +"""All constructors that do not require GPU hardware.""" def available_constructors() -> frozenset[ConstructorName]: @@ -79,3 +86,36 @@ def get_constructor(name: ConstructorName | str) -> ConstructorBase: msg = f"Unknown constructor {name!r}. Expected one of: {valid}." raise ValueError(msg) from exc return ALL_CONSTRUCTORS[key] + + +def prepare_constructors( + *, + include: Iterable[ConstructorName] | None = None, + exclude: Iterable[ConstructorName] | None = None, +) -> list[ConstructorBase]: + """Return available constructors, optionally filtered. + + Arguments: + include: If given, only return constructors whose name is in this set. + exclude: If given, remove constructors whose name is in this set. + + Examples: + >>> from narwhals.testing.constructors import ( + ... ConstructorName, + ... prepare_constructors, + ... ) + >>> constructors = prepare_constructors( + ... include=[ConstructorName.PANDAS, ConstructorName.POLARS_EAGER] + ... ) + """ + available = available_constructors() + candidates: list[ConstructorBase] = [ + ConstructorBase._registry[n] for n in ConstructorBase._registry if n in available + ] + if include is not None: + inc = frozenset(include) + candidates = [c for c in candidates if c.name in inc] + if exclude is not None: + exc = frozenset(exclude) + candidates = [c for c in candidates if c.name not in exc] + return sorted(candidates, key=lambda c: c.name.value) diff --git a/narwhals/testing/constructors/_classes.py b/narwhals/testing/constructors/_classes.py index e541aa19fb..5e4d24207c 100644 --- a/narwhals/testing/constructors/_classes.py +++ b/narwhals/testing/constructors/_classes.py @@ -1,3 +1,42 @@ +"""Concrete constructor classes and auto-registration machinery. + +Each constructor wraps one backend library (pandas, Polars, DuckDB, ...) +and knows how to turn a column-oriented `dict` into a native frame. + +## Adding a new constructor + +1. Choose the right base class: + + * `ConstructorEagerBase`: if the backend returns an eager dataframe. + * `ConstructorLazyBase`: if the backend returns a lazy frame. + +2. Add a member to `ConstructorName` in `_name.py` and register the corresponding + `Implementation` mapping in `_NAME_TO_IMPL`. + +3. Define the class in this module. Specify: + + * `requirements`: the packages that `importlib.util.find_spec` should check + * `legacy_name` (if relevant): the old `str(constructor)` value used in existing + tests as keyword arguments in the class header: + + ```py + class MyBackendConstructor( + ConstructorLazyBase, + requirements=("my_backend",), + legacy_name="my_backend_lazy_constructor", + ): + name = ConstructorName.MY_BACKEND + + def __call__(self, obj: Data, /, **kwds: Any) -> ...: + import my_backend + + return my_backend.from_dict(obj) + ``` + +That is all. `__init_subclass__` on `ConstructorBase` will automatically register +a default singleton into `_registry`, record the *requirements*, and store the *legacy_name*. +""" + from __future__ import annotations import os @@ -6,7 +45,7 @@ from abc import ABC, abstractmethod from copy import deepcopy from functools import lru_cache -from typing import TYPE_CHECKING, ClassVar, cast +from typing import TYPE_CHECKING, Any, ClassVar, cast from narwhals._utils import generate_temporary_column_name from narwhals.testing.constructors._name import ConstructorName @@ -83,18 +122,53 @@ class ConstructorBase(ABC): A constructor is a callable that turns a column-oriented `dict` (typed as [`Data`][narwhals.testing.typing.Data]) into a native dataframe / lazy frame, plus a typed [`ConstructorName`][] that identifies the backend. + + Subclasses are automatically registered when they set ``name`` as a + class variable and pass ``requirements`` / ``legacy_name`` as + keyword arguments in the class definition. """ + _registry: ClassVar[dict[ConstructorName, ConstructorBase]] = {} + _requirements: ClassVar[dict[ConstructorName, tuple[str, ...]]] = {} + _legacy_names: ClassVar[dict[ConstructorName, str]] = {} + name: ClassVar[ConstructorName] + def __init_subclass__( + cls, *, requirements: tuple[str, ...] = (), legacy_name: str = "", **kwargs: Any + ) -> None: + """Register concrete subclasses automatically. + + Arguments: + requirements: Package names that must be importable for + this constructor to be available (checked via + ``importlib.util.find_spec``). + legacy_name: Value returned by ``str(constructor)`` for + backward compatibility with test assertions that + match on the old naming scheme. + **kwargs: Forwarded to ``super().__init_subclass__``. + """ + super().__init_subclass__(**kwargs) + instance = cls() + ConstructorBase._registry[cls.name] = instance + ConstructorBase._requirements[cls.name] = requirements + if legacy_name: + ConstructorBase._legacy_names[cls.name] = legacy_name + @abstractmethod - def __call__(self, obj: Data) -> IntoFrame: + def __call__(self, obj: Data, /, **kwds: Any) -> IntoFrame: """Build a native frame from `obj`.""" + @property + def identifier(self) -> str: + """Instance-level string identifier for test IDs.""" + return str(self.name) + def __str__(self) -> str: # NOTE: This is a temporary hack - # TODO(Unassigned): Remove once all the `"backend" in str(constructor)` - # statements in the test suite are properly replaced + # TODO(Unassigned): Remove once all the + # `"backend" in str(constructor)` statements in the + # test suite are properly replaced return _LEGACY_NAME[self.name] def __repr__(self) -> str: @@ -113,69 +187,101 @@ class ConstructorEagerBase(ConstructorBase): """A constructor that returns an *eager* native dataframe.""" @abstractmethod - def __call__(self, obj: Data) -> IntoDataFrame: ... + def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: ... class ConstructorLazyBase(ConstructorBase): """A constructor that returns a *lazy* native frame.""" @abstractmethod - def __call__(self, obj: Data) -> IntoLazyFrame: ... + def __call__(self, obj: Data, /, **kwds: Any) -> IntoLazyFrame: ... # Eager constructors -class PandasConstructor(ConstructorEagerBase): +class PandasConstructor( + ConstructorEagerBase, requirements=("pandas",), legacy_name="pandas_constructor" +): + """Constructor backed by ``pandas.DataFrame`` with default NumPy dtypes.""" + name = ConstructorName.PANDAS - def __call__(self, obj: Data) -> pd.DataFrame: + def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: import pandas as pd return pd.DataFrame(obj) -class PandasNullableConstructor(ConstructorEagerBase): +class PandasNullableConstructor( + ConstructorEagerBase, + requirements=("pandas",), + legacy_name="pandas_nullable_constructor", +): + """Constructor backed by ``pandas.DataFrame`` with ``numpy_nullable`` dtypes.""" + name = ConstructorName.PANDAS_NULLABLE - def __call__(self, obj: Data) -> pd.DataFrame: + def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: import pandas as pd return pd.DataFrame(obj).convert_dtypes(dtype_backend="numpy_nullable") -class PandasPyArrowConstructor(ConstructorEagerBase): +class PandasPyArrowConstructor( + ConstructorEagerBase, + requirements=("pandas", "pyarrow"), + legacy_name="pandas_pyarrow_constructor", +): + """Constructor backed by ``pandas.DataFrame`` with ``pyarrow`` dtypes.""" + name = ConstructorName.PANDAS_PYARROW - def __call__(self, obj: Data) -> pd.DataFrame: + def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: import pandas as pd return pd.DataFrame(obj).convert_dtypes(dtype_backend="pyarrow") -class PyArrowConstructor(ConstructorEagerBase): +class PyArrowConstructor( + ConstructorEagerBase, + requirements=("pyarrow",), + legacy_name="pyarrow_table_constructor", +): + """Constructor backed by ``pyarrow.Table``.""" + name = ConstructorName.PYARROW - def __call__(self, obj: Data) -> pa.Table: + def __call__(self, obj: Data, /, **kwds: Any) -> pa.Table: import pyarrow as pa return pa.table(obj) # type:ignore[arg-type] -class ModinConstructor(ConstructorEagerBase): # pragma: no cover +class ModinConstructor( + ConstructorEagerBase, requirements=("modin",), legacy_name="modin_constructor" +): # pragma: no cover + """Constructor backed by ``modin.pandas.DataFrame``.""" + name = ConstructorName.MODIN - def __call__(self, obj: Data) -> IntoDataFrame: + def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: import modin.pandas as mpd import pandas as pd return cast("IntoDataFrame", mpd.DataFrame(pd.DataFrame(obj))) -class ModinPyArrowConstructor(ConstructorEagerBase): +class ModinPyArrowConstructor( + ConstructorEagerBase, + requirements=("modin", "pyarrow"), + legacy_name="modin_pyarrow_constructor", +): + """Constructor backed by ``modin.pandas.DataFrame`` with ``pyarrow`` dtypes.""" + name = ConstructorName.MODIN_PYARROW - def __call__(self, obj: Data) -> IntoDataFrame: + def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: import modin.pandas as mpd import pandas as pd @@ -183,19 +289,27 @@ def __call__(self, obj: Data) -> IntoDataFrame: return cast("IntoDataFrame", df) -class CudfConstructor(ConstructorEagerBase): # pragma: no cover +class CudfConstructor( + ConstructorEagerBase, requirements=("cudf",), legacy_name="cudf_constructor" +): # pragma: no cover + """Constructor backed by ``cudf.DataFrame`` (requires GPU).""" + name = ConstructorName.CUDF - def __call__(self, obj: Data) -> IntoDataFrame: + def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: import cudf return cast("IntoDataFrame", cudf.DataFrame(obj)) -class PolarsEagerConstructor(ConstructorEagerBase): +class PolarsEagerConstructor( + ConstructorEagerBase, requirements=("polars",), legacy_name="polars_eager_constructor" +): + """Constructor backed by ``polars.DataFrame``.""" + name = ConstructorName.POLARS_EAGER - def __call__(self, obj: Data) -> pl.DataFrame: + def __call__(self, obj: Data, /, **kwds: Any) -> pl.DataFrame: import polars as pl return pl.DataFrame(obj) @@ -204,26 +318,43 @@ def __call__(self, obj: Data) -> pl.DataFrame: # Lazy constructors -class PolarsLazyConstructor(ConstructorLazyBase): +class PolarsLazyConstructor( + ConstructorLazyBase, requirements=("polars",), legacy_name="polars_lazy_constructor" +): + """Constructor backed by ``polars.LazyFrame``.""" + name = ConstructorName.POLARS_LAZY - def __call__(self, obj: Data) -> pl.LazyFrame: + def __call__(self, obj: Data, /, **kwds: Any) -> pl.LazyFrame: import polars as pl return pl.LazyFrame(obj) -class DaskConstructor(ConstructorLazyBase): # pragma: no cover +class DaskConstructor( + ConstructorLazyBase, requirements=("dask",), legacy_name="dask_lazy_p1_constructor" +): # pragma: no cover + """Constructor backed by ``dask.dataframe``. + + Arguments: + npartitions: Number of Dask partitions (default ``1``). + """ + name = ConstructorName.DASK def __init__(self, npartitions: int = 1) -> None: self.npartitions = npartitions - def __call__(self, obj: Data) -> NativeDask: + def __call__(self, obj: Data, /, **kwds: Any) -> NativeDask: import dask.dataframe as dd return cast("NativeDask", dd.from_dict(obj, npartitions=self.npartitions)) + @property + def identifier(self) -> str: + """Identifier that encodes the number of partitions.""" + return f"dask[p{self.npartitions}]" + def __repr__(self) -> str: return f"{type(self).__name__}(npartitions={self.npartitions})" @@ -237,10 +368,16 @@ def __eq__(self, other: object) -> bool: ) -class DuckDBConstructor(ConstructorLazyBase): +class DuckDBConstructor( + ConstructorLazyBase, + requirements=("duckdb", "pyarrow"), + legacy_name="duckdb_lazy_constructor", +): + """Constructor backed by DuckDB (via ``duckdb.sql``).""" + name = ConstructorName.DUCKDB - def __call__(self, obj: Data) -> NativeDuckDB: + def __call__(self, obj: Data, /, **kwds: Any) -> NativeDuckDB: import duckdb import pyarrow as pa @@ -249,10 +386,14 @@ def __call__(self, obj: Data) -> NativeDuckDB: return duckdb.sql("select * from _df") -class PySparkConstructor(ConstructorLazyBase): # pragma: no cover +class PySparkConstructor( + ConstructorLazyBase, requirements=("pyspark",), legacy_name="pyspark_lazy_constructor" +): # pragma: no cover + """Constructor backed by ``pyspark.sql.DataFrame``.""" + name = ConstructorName.PYSPARK - def __call__(self, obj: Data) -> NativePySpark: + def __call__(self, obj: Data, /, **kwds: Any) -> NativePySpark: session = _pyspark_session_lazy() _obj = deepcopy(obj) index_col_name = generate_temporary_column_name(n_bytes=8, columns=list(_obj)) @@ -266,22 +407,38 @@ def __call__(self, obj: Data) -> NativePySpark: return cast("NativePySpark", result) -class PySparkConnectConstructor(PySparkConstructor): # pragma: no cover +class PySparkConnectConstructor( + PySparkConstructor, requirements=("pyspark",), legacy_name="pyspark_lazy_constructor" +): # pragma: no cover + """Constructor backed by PySpark Connect (Spark Connect protocol).""" + name = ConstructorName.PYSPARK_CONNECT -class SQLFrameConstructor(ConstructorLazyBase): +class SQLFrameConstructor( + ConstructorLazyBase, + requirements=("sqlframe", "duckdb"), + legacy_name="sqlframe_pyspark_lazy_constructor", +): + """Constructor backed by ``sqlframe`` (DuckDB session).""" + name = ConstructorName.SQLFRAME - def __call__(self, obj: Data) -> NativeSQLFrame: + def __call__(self, obj: Data, /, **kwds: Any) -> NativeSQLFrame: session = sqlframe_session() return session.createDataFrame([*zip(*obj.values())], schema=[*obj.keys()]) -class IbisConstructor(ConstructorLazyBase): +class IbisConstructor( + ConstructorLazyBase, + requirements=("ibis", "duckdb", "pyarrow"), + legacy_name="ibis_lazy_constructor", +): + """Constructor backed by ``ibis`` (DuckDB backend).""" + name = ConstructorName.IBIS - def __call__(self, obj: Data) -> ibis.Table: + def __call__(self, obj: Data, /, **kwds: Any) -> ibis.Table: import pyarrow as pa table = pa.table(obj) # type:ignore[arg-type] @@ -289,58 +446,6 @@ def __call__(self, obj: Data) -> ibis.Table: return _ibis_backend().create_table(table_name, table) -_NAME_TO_CONSTRUCTOR: dict[ConstructorName, ConstructorBase] = { - ConstructorName.PANDAS: PandasConstructor(), - ConstructorName.PANDAS_NULLABLE: PandasNullableConstructor(), - ConstructorName.PANDAS_PYARROW: PandasPyArrowConstructor(), - ConstructorName.PYARROW: PyArrowConstructor(), - ConstructorName.MODIN: ModinConstructor(), - ConstructorName.MODIN_PYARROW: ModinPyArrowConstructor(), - ConstructorName.CUDF: CudfConstructor(), - ConstructorName.POLARS_EAGER: PolarsEagerConstructor(), - ConstructorName.POLARS_LAZY: PolarsLazyConstructor(), - ConstructorName.DASK: DaskConstructor(), - ConstructorName.DUCKDB: DuckDBConstructor(), - ConstructorName.PYSPARK: PySparkConstructor(), - ConstructorName.PYSPARK_CONNECT: PySparkConnectConstructor(), - ConstructorName.SQLFRAME: SQLFrameConstructor(), - ConstructorName.IBIS: IbisConstructor(), -} - -_BACKEND_REQUIREMENTS: dict[ConstructorName, tuple[str, ...]] = { - ConstructorName.PANDAS: ("pandas",), - ConstructorName.PANDAS_NULLABLE: ("pandas",), - ConstructorName.PANDAS_PYARROW: ("pandas", "pyarrow"), - ConstructorName.PYARROW: ("pyarrow",), - ConstructorName.MODIN: ("modin",), - ConstructorName.MODIN_PYARROW: ("modin", "pyarrow"), - ConstructorName.CUDF: ("cudf",), - ConstructorName.POLARS_EAGER: ("polars",), - ConstructorName.POLARS_LAZY: ("polars",), - ConstructorName.DASK: ("dask",), - ConstructorName.DUCKDB: ("duckdb", "pyarrow"), - ConstructorName.PYSPARK: ("pyspark",), - ConstructorName.PYSPARK_CONNECT: ("pyspark",), - ConstructorName.SQLFRAME: ("sqlframe", "duckdb"), - ConstructorName.IBIS: ("ibis", "duckdb", "pyarrow"), -} - # TODO(Unassigned): Remove once all the `"backend" in str(constructor)` -# statements in the test suite are properly replaced -_LEGACY_NAME: dict[ConstructorName, str] = { - ConstructorName.PANDAS: "pandas_constructor", - ConstructorName.PANDAS_NULLABLE: "pandas_nullable_constructor", - ConstructorName.PANDAS_PYARROW: "pandas_pyarrow_constructor", - ConstructorName.PYARROW: "pyarrow_table_constructor", - ConstructorName.MODIN: "modin_constructor", - ConstructorName.MODIN_PYARROW: "modin_pyarrow_constructor", - ConstructorName.CUDF: "cudf_constructor", - ConstructorName.POLARS_EAGER: "polars_eager_constructor", - ConstructorName.POLARS_LAZY: "polars_lazy_constructor", - ConstructorName.DASK: "dask_lazy_p1_constructor", - ConstructorName.DUCKDB: "duckdb_lazy_constructor", - ConstructorName.PYSPARK: "pyspark_lazy_constructor", - ConstructorName.PYSPARK_CONNECT: "pyspark_lazy_constructor", - ConstructorName.SQLFRAME: "sqlframe_pyspark_lazy_constructor", - ConstructorName.IBIS: "ibis_lazy_constructor", -} +# statements in the test suite are properly replaced. +_LEGACY_NAME = ConstructorBase._legacy_names diff --git a/narwhals/testing/constructors/_name.py b/narwhals/testing/constructors/_name.py index f71f184e62..1b98c05cec 100644 --- a/narwhals/testing/constructors/_name.py +++ b/narwhals/testing/constructors/_name.py @@ -4,6 +4,8 @@ from importlib.util import find_spec from typing import TYPE_CHECKING +from narwhals._utils import Implementation + if TYPE_CHECKING: import pytest @@ -59,69 +61,71 @@ class ConstructorName(str, Enum): def __str__(self) -> str: return str(self.value) + @property + def implementation(self) -> Implementation: + """The [`Implementation`][] that this constructor belongs to.""" + return _NAME_TO_IMPL[self] + @property def is_pandas(self) -> bool: """Whether this is one of the pandas constructors.""" - return self in { - ConstructorName.PANDAS, - ConstructorName.PANDAS_NULLABLE, - ConstructorName.PANDAS_PYARROW, - } + return self.implementation.is_pandas() @property def is_modin(self) -> bool: """Whether this is one of the modin constructors.""" - return self in {ConstructorName.MODIN, ConstructorName.MODIN_PYARROW} + return self.implementation.is_modin() @property def is_cudf(self) -> bool: """Whether this is the cudf constructor.""" - return self is ConstructorName.CUDF + return self.implementation.is_cudf() @property def is_pandas_like(self) -> bool: """Whether this constructor produces a pandas-like dataframe (pandas, modin, cudf).""" - return self.is_pandas or self.is_modin or self.is_cudf + return self.implementation.is_pandas_like() @property def is_polars(self) -> bool: """Whether this is one of the polars constructors.""" - return self in {ConstructorName.POLARS_EAGER, ConstructorName.POLARS_LAZY} + return self.implementation.is_polars() @property def is_pyarrow(self) -> bool: """Whether this is the pyarrow table constructor.""" - return self is ConstructorName.PYARROW + return self.implementation.is_pyarrow() @property def is_dask(self) -> bool: """Whether this is the dask constructor.""" - return self is ConstructorName.DASK + return self.implementation.is_dask() @property def is_duckdb(self) -> bool: """Whether this is the duckdb constructor.""" - return self is ConstructorName.DUCKDB + return self.implementation.is_duckdb() @property def is_pyspark(self) -> bool: """Whether this is one of the pyspark constructors.""" - return self in {ConstructorName.PYSPARK, ConstructorName.PYSPARK_CONNECT} + impl = self.implementation + return impl.is_pyspark() or impl.is_pyspark_connect() @property def is_sqlframe(self) -> bool: """Whether this is the sqlframe constructor.""" - return self is ConstructorName.SQLFRAME + return self.implementation.is_sqlframe() @property def is_ibis(self) -> bool: """Whether this is the ibis constructor.""" - return self is ConstructorName.IBIS + return self.implementation.is_ibis() @property def is_spark_like(self) -> bool: """Whether this constructor uses a spark-like backend (pyspark, sqlframe).""" - return self.is_pyspark or self.is_sqlframe + return self.implementation.is_spark_like() @property def is_eager(self) -> bool: @@ -155,6 +159,7 @@ def needs_pyarrow(self) -> bool: @property def is_non_nullable(self) -> bool: + """Whether this constructor uses a backend without native null support.""" return self in { ConstructorName.PANDAS, ConstructorName.MODIN, @@ -181,12 +186,33 @@ def from_pytest_request(cls, request: pytest.FixtureRequest) -> ConstructorName: @property def constructor(self) -> ConstructorBase: - from narwhals.testing.constructors._classes import _NAME_TO_CONSTRUCTOR + """Return the registered singleton constructor for this name.""" + from narwhals.testing.constructors._classes import ConstructorBase - return _NAME_TO_CONSTRUCTOR[self] + return ConstructorBase._registry[self] @property def is_available(self) -> bool: - from narwhals.testing.constructors._classes import _BACKEND_REQUIREMENTS - - return is_backend_available(*_BACKEND_REQUIREMENTS[self]) + """Whether every package required by this constructor is importable.""" + from narwhals.testing.constructors._classes import ConstructorBase + + return is_backend_available(*ConstructorBase._requirements[self]) + + +_NAME_TO_IMPL: dict[ConstructorName, Implementation] = { + ConstructorName.PANDAS: Implementation.PANDAS, + ConstructorName.PANDAS_NULLABLE: Implementation.PANDAS, + ConstructorName.PANDAS_PYARROW: Implementation.PANDAS, + ConstructorName.PYARROW: Implementation.PYARROW, + ConstructorName.MODIN: Implementation.MODIN, + ConstructorName.MODIN_PYARROW: Implementation.MODIN, + ConstructorName.CUDF: Implementation.CUDF, + ConstructorName.POLARS_EAGER: Implementation.POLARS, + ConstructorName.POLARS_LAZY: Implementation.POLARS, + ConstructorName.DASK: Implementation.DASK, + ConstructorName.DUCKDB: Implementation.DUCKDB, + ConstructorName.PYSPARK: Implementation.PYSPARK, + ConstructorName.PYSPARK_CONNECT: Implementation.PYSPARK_CONNECT, + ConstructorName.SQLFRAME: Implementation.SQLFRAME, + ConstructorName.IBIS: Implementation.IBIS, +} diff --git a/tests/expr_and_series/corr_test.py b/tests/expr_and_series/corr_test.py index 3c70b2623b..b37e61d3e1 100644 --- a/tests/expr_and_series/corr_test.py +++ b/tests/expr_and_series/corr_test.py @@ -29,7 +29,7 @@ def test_corr_expr( output_name: str, a: str | nw.Expr, b: str | nw.Expr, - expected_corr: float, + expected_corr: float | None, ) -> None: if "pyspark" in str(constructor) and expected_corr is None: request.applymarker( @@ -55,7 +55,7 @@ def test_corr_expr_spearman( output_name: str, a: str | nw.Expr, b: str | nw.Expr, - expected_corr: float, + expected_corr: float | None, ) -> None: context = ( does_not_raise() @@ -79,7 +79,7 @@ def test_corr_series( output_name: str, a: str, b: str, - expected_corr: float, + expected_corr: float | None, ) -> None: if "pyspark" in str(constructor_eager) and expected_corr is None: request.applymarker( @@ -101,7 +101,7 @@ def test_corr_series_spearman( output_name: str, a: str, b: str, - expected_corr: float, + expected_corr: float | None, ) -> None: if "pyspark" in str(constructor_eager) and expected_corr is None: request.applymarker( From 1e05a49ca062e5a83c33bbf399d3c6cfd93d931d Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 11:27:06 +0200 Subject: [PATCH 08/71] fixup and allow kwargs --- narwhals/testing/constructors/_classes.py | 38 +++++++++++++---------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/narwhals/testing/constructors/_classes.py b/narwhals/testing/constructors/_classes.py index 5e4d24207c..7c17239a3b 100644 --- a/narwhals/testing/constructors/_classes.py +++ b/narwhals/testing/constructors/_classes.py @@ -149,6 +149,8 @@ def __init_subclass__( **kwargs: Forwarded to ``super().__init_subclass__``. """ super().__init_subclass__(**kwargs) + if "name" not in cls.__dict__: + return instance = cls() ConstructorBase._registry[cls.name] = instance ConstructorBase._requirements[cls.name] = requirements @@ -210,7 +212,7 @@ class PandasConstructor( def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: import pandas as pd - return pd.DataFrame(obj) + return pd.DataFrame(obj, **kwds) class PandasNullableConstructor( @@ -225,7 +227,7 @@ class PandasNullableConstructor( def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: import pandas as pd - return pd.DataFrame(obj).convert_dtypes(dtype_backend="numpy_nullable") + return pd.DataFrame(obj, **kwds).convert_dtypes(dtype_backend="numpy_nullable") class PandasPyArrowConstructor( @@ -240,7 +242,7 @@ class PandasPyArrowConstructor( def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: import pandas as pd - return pd.DataFrame(obj).convert_dtypes(dtype_backend="pyarrow") + return pd.DataFrame(obj, **kwds).convert_dtypes(dtype_backend="pyarrow") class PyArrowConstructor( @@ -255,7 +257,7 @@ class PyArrowConstructor( def __call__(self, obj: Data, /, **kwds: Any) -> pa.Table: import pyarrow as pa - return pa.table(obj) # type:ignore[arg-type] + return pa.table(obj, **kwds) # type:ignore[arg-type] class ModinConstructor( @@ -269,7 +271,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: import modin.pandas as mpd import pandas as pd - return cast("IntoDataFrame", mpd.DataFrame(pd.DataFrame(obj))) + return cast("IntoDataFrame", mpd.DataFrame(pd.DataFrame(obj, **kwds))) class ModinPyArrowConstructor( @@ -285,7 +287,9 @@ def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: import modin.pandas as mpd import pandas as pd - df = mpd.DataFrame(pd.DataFrame(obj)).convert_dtypes(dtype_backend="pyarrow") + df = mpd.DataFrame(pd.DataFrame(obj, **kwds)).convert_dtypes( + dtype_backend="pyarrow" + ) return cast("IntoDataFrame", df) @@ -299,7 +303,7 @@ class CudfConstructor( def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: import cudf - return cast("IntoDataFrame", cudf.DataFrame(obj)) + return cast("IntoDataFrame", cudf.DataFrame(obj, **kwds)) class PolarsEagerConstructor( @@ -312,7 +316,7 @@ class PolarsEagerConstructor( def __call__(self, obj: Data, /, **kwds: Any) -> pl.DataFrame: import polars as pl - return pl.DataFrame(obj) + return pl.DataFrame(obj, **kwds) # Lazy constructors @@ -328,11 +332,11 @@ class PolarsLazyConstructor( def __call__(self, obj: Data, /, **kwds: Any) -> pl.LazyFrame: import polars as pl - return pl.LazyFrame(obj) + return pl.LazyFrame(obj, **kwds) class DaskConstructor( - ConstructorLazyBase, requirements=("dask",), legacy_name="dask_lazy_p1_constructor" + ConstructorLazyBase, requirements=("dask",), legacy_name="dask_lazy_p2_constructor" ): # pragma: no cover """Constructor backed by ``dask.dataframe``. @@ -342,13 +346,13 @@ class DaskConstructor( name = ConstructorName.DASK - def __init__(self, npartitions: int = 1) -> None: + def __init__(self, npartitions: int = 2) -> None: self.npartitions = npartitions def __call__(self, obj: Data, /, **kwds: Any) -> NativeDask: import dask.dataframe as dd - return cast("NativeDask", dd.from_dict(obj, npartitions=self.npartitions)) + return cast("NativeDask", dd.from_dict(obj, npartitions=self.npartitions, **kwds)) @property def identifier(self) -> str: @@ -382,7 +386,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> NativeDuckDB: import pyarrow as pa duckdb.sql("""set timezone = 'UTC'""") - _df = pa.table(obj) # type:ignore[arg-type] + _df = pa.table(obj, **kwds) # type:ignore[arg-type] return duckdb.sql("select * from _df") @@ -399,7 +403,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> NativePySpark: index_col_name = generate_temporary_column_name(n_bytes=8, columns=list(_obj)) _obj[index_col_name] = list(range(len(_obj[next(iter(_obj))]))) result = ( - session.createDataFrame([*zip(*_obj.values())], schema=[*_obj.keys()]) + session.createDataFrame([*zip(*_obj.values())], schema=[*_obj.keys()], **kwds) .repartition(2) .orderBy(index_col_name) .drop(index_col_name) @@ -426,7 +430,9 @@ class SQLFrameConstructor( def __call__(self, obj: Data, /, **kwds: Any) -> NativeSQLFrame: session = sqlframe_session() - return session.createDataFrame([*zip(*obj.values())], schema=[*obj.keys()]) + return session.createDataFrame( + [*zip(*obj.values())], schema=[*obj.keys()], **kwds + ) class IbisConstructor( @@ -443,7 +449,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ibis.Table: table = pa.table(obj) # type:ignore[arg-type] table_name = str(uuid.uuid4()) - return _ibis_backend().create_table(table_name, table) + return _ibis_backend().create_table(table_name, table, **kwds) # TODO(Unassigned): Remove once all the `"backend" in str(constructor)` From c55beaa7bc11d6f753448a5bc9f4deae9df4e2fa Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 11:41:28 +0200 Subject: [PATCH 09/71] simplify pytest_plugin --- narwhals/testing/pytest_plugin.py | 59 ++++++++++--------------------- 1 file changed, 18 insertions(+), 41 deletions(-) diff --git a/narwhals/testing/pytest_plugin.py b/narwhals/testing/pytest_plugin.py index 256b21d382..63a8fefd52 100644 --- a/narwhals/testing/pytest_plugin.py +++ b/narwhals/testing/pytest_plugin.py @@ -8,21 +8,19 @@ ALL_CPU_CONSTRUCTORS, DEFAULT_CONSTRUCTORS, ConstructorBase, - ConstructorEagerBase, ConstructorName, - available_constructors, - get_constructor, + prepare_constructors, ) if TYPE_CHECKING: - from collections.abc import Iterable - import pytest _MIN_PANDAS_NULLABLE_VERSION: tuple[int, ...] = (2, 0, 0) """`pandas.convert_dtypes(dtype_backend=...)` requires pandas >= 2.0.0.""" +_PANDAS_NULLABLES = {ConstructorName.PANDAS_NULLABLE, ConstructorName.PANDAS_PYARROW} + _ALL_CPU_EXCLUSIONS: frozenset[ConstructorName] = frozenset( {ConstructorName.MODIN, ConstructorName.PYSPARK_CONNECT} ) @@ -49,8 +47,7 @@ def _default_constructor_ids() -> list[str]: """ if env := os.environ.get("NARWHALS_DEFAULT_CONSTRUCTORS"): # pragma: no cover return env.split(",") - available = available_constructors() - return [name.value for name in DEFAULT_CONSTRUCTORS if name in available] + return [str(c.name) for c in prepare_constructors(include=DEFAULT_CONSTRUCTORS)] def pytest_addoption(parser: pytest.Parser) -> None: @@ -91,24 +88,18 @@ def pytest_addoption(parser: pytest.Parser) -> None: def _select_constructors( config: pytest.Config, -) -> list[ConstructorName]: # pragma: no cover +) -> list[ConstructorBase]: # pragma: no cover if config.getoption("all_cpu_constructors"): - names: Iterable[ConstructorName] = sorted( - ALL_CPU_CONSTRUCTORS - _ALL_CPU_EXCLUSIONS, key=lambda c: c.value + selected = prepare_constructors( + include=ALL_CPU_CONSTRUCTORS, exclude=_ALL_CPU_EXCLUSIONS ) else: opt = cast("str", config.getoption("constructors")) names = [ConstructorName(c) for c in opt.split(",") if c] + selected = prepare_constructors(include=names) - pandas_version = _pandas_version() - selected: list[ConstructorName] = [] - for name in names: - if ( - name in {ConstructorName.PANDAS_NULLABLE, ConstructorName.PANDAS_PYARROW} - and pandas_version < _MIN_PANDAS_NULLABLE_VERSION - ): - continue - selected.append(name) + if _pandas_version() < _MIN_PANDAS_NULLABLE_VERSION: + selected = [c for c in selected if c.name not in _PANDAS_NULLABLES] return selected @@ -122,27 +113,13 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: selected = _select_constructors(metafunc.config) - constructors: list[ConstructorBase] = [] - constructor_ids: list[str] = [] - eager: list[ConstructorEagerBase] = [] - eager_ids: list[str] = [] - pandas_like: list[ConstructorEagerBase] = [] - pandas_like_ids: list[str] = [] - - for name in selected: - constructor = get_constructor(name) - constructors.append(constructor) - constructor_ids.append(name.value) - if isinstance(constructor, ConstructorEagerBase): - eager.append(constructor) - eager_ids.append(name.value) - if name.is_pandas_like: - pandas_like.append(constructor) - pandas_like_ids.append(name.value) - if "constructor_eager" in fixturenames: - metafunc.parametrize("constructor_eager", eager, ids=eager_ids) + params = [c for c in selected if c.name.is_eager] + ids = [str(c.name) for c in params] + metafunc.parametrize("constructor_eager", params, ids=ids) elif "constructor" in fixturenames: - metafunc.parametrize("constructor", constructors, ids=constructor_ids) - elif "constructor_pandas_like" in metafunc.fixturenames: - metafunc.parametrize("constructor_pandas_like", pandas_like, ids=pandas_like_ids) + metafunc.parametrize("constructor", selected, ids=[str(c.name) for c in selected]) + elif "constructor_pandas_like" in fixturenames: + params = [c for c in selected if c.name.is_eager and c.name.is_pandas_like] + ids = [str(c.name) for c in params] + metafunc.parametrize("constructor_pandas_like", params, ids=ids) From 7e057340f5725e568f510caabd0b10b7b0c7906c Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 13:12:39 +0200 Subject: [PATCH 10/71] defer imports --- narwhals/testing/constructors/_name.py | 49 ++++++++++++--------- narwhals/testing/pytest_plugin.py | 59 ++++++++++++++++---------- pyproject.toml | 1 + 3 files changed, 67 insertions(+), 42 deletions(-) diff --git a/narwhals/testing/constructors/_name.py b/narwhals/testing/constructors/_name.py index 1b98c05cec..c3e33dbd31 100644 --- a/narwhals/testing/constructors/_name.py +++ b/narwhals/testing/constructors/_name.py @@ -1,14 +1,14 @@ from __future__ import annotations from enum import Enum +from functools import lru_cache from importlib.util import find_spec from typing import TYPE_CHECKING -from narwhals._utils import Implementation - if TYPE_CHECKING: import pytest + from narwhals._utils import Implementation from narwhals.testing.constructors._classes import ConstructorBase @@ -64,7 +64,7 @@ def __str__(self) -> str: @property def implementation(self) -> Implementation: """The [`Implementation`][] that this constructor belongs to.""" - return _NAME_TO_IMPL[self] + return _name_to_impl()[self] @property def is_pandas(self) -> bool: @@ -199,20 +199,29 @@ def is_available(self) -> bool: return is_backend_available(*ConstructorBase._requirements[self]) -_NAME_TO_IMPL: dict[ConstructorName, Implementation] = { - ConstructorName.PANDAS: Implementation.PANDAS, - ConstructorName.PANDAS_NULLABLE: Implementation.PANDAS, - ConstructorName.PANDAS_PYARROW: Implementation.PANDAS, - ConstructorName.PYARROW: Implementation.PYARROW, - ConstructorName.MODIN: Implementation.MODIN, - ConstructorName.MODIN_PYARROW: Implementation.MODIN, - ConstructorName.CUDF: Implementation.CUDF, - ConstructorName.POLARS_EAGER: Implementation.POLARS, - ConstructorName.POLARS_LAZY: Implementation.POLARS, - ConstructorName.DASK: Implementation.DASK, - ConstructorName.DUCKDB: Implementation.DUCKDB, - ConstructorName.PYSPARK: Implementation.PYSPARK, - ConstructorName.PYSPARK_CONNECT: Implementation.PYSPARK_CONNECT, - ConstructorName.SQLFRAME: Implementation.SQLFRAME, - ConstructorName.IBIS: Implementation.IBIS, -} +@lru_cache(maxsize=1) +def _name_to_impl() -> dict[ConstructorName, Implementation]: + """Lazily build the ConstructorName -> Implementation mapping. + + The import is deferred so that ``narwhals._utils`` is not loaded + at plugin-registration time (before coverage starts measuring). + """ + from narwhals._utils import Implementation + + return { + ConstructorName.PANDAS: Implementation.PANDAS, + ConstructorName.PANDAS_NULLABLE: Implementation.PANDAS, + ConstructorName.PANDAS_PYARROW: Implementation.PANDAS, + ConstructorName.PYARROW: Implementation.PYARROW, + ConstructorName.MODIN: Implementation.MODIN, + ConstructorName.MODIN_PYARROW: Implementation.MODIN, + ConstructorName.CUDF: Implementation.CUDF, + ConstructorName.POLARS_EAGER: Implementation.POLARS, + ConstructorName.POLARS_LAZY: Implementation.POLARS, + ConstructorName.DASK: Implementation.DASK, + ConstructorName.DUCKDB: Implementation.DUCKDB, + ConstructorName.PYSPARK: Implementation.PYSPARK, + ConstructorName.PYSPARK_CONNECT: Implementation.PYSPARK_CONNECT, + ConstructorName.SQLFRAME: Implementation.SQLFRAME, + ConstructorName.IBIS: Implementation.IBIS, + } diff --git a/narwhals/testing/pytest_plugin.py b/narwhals/testing/pytest_plugin.py index 63a8fefd52..78dc8e4e53 100644 --- a/narwhals/testing/pytest_plugin.py +++ b/narwhals/testing/pytest_plugin.py @@ -1,41 +1,38 @@ +"""Narwhals pytest plugin - auto-parametrises `constructor*` fixtures. + +NOTE: All imports from `narwhals.*` are deferred inside the hook functions so +that the entry-point module can be loaded by pytest without pulling in the +narwhals package tree. + +This is critical because entry-point plugins are loaded *before* `pytest-cov` +starts coverage measurement; any narwhals module imported at that stage would +have its module-level code (class definitions, constants, etc.) executed outside +the coverage tracer. +""" + from __future__ import annotations import os from typing import TYPE_CHECKING, cast -from narwhals._utils import parse_version -from narwhals.testing.constructors import ( - ALL_CPU_CONSTRUCTORS, - DEFAULT_CONSTRUCTORS, - ConstructorBase, - ConstructorName, - prepare_constructors, -) - if TYPE_CHECKING: import pytest + from narwhals.testing.constructors import ConstructorBase + _MIN_PANDAS_NULLABLE_VERSION: tuple[int, ...] = (2, 0, 0) """`pandas.convert_dtypes(dtype_backend=...)` requires pandas >= 2.0.0.""" -_PANDAS_NULLABLES = {ConstructorName.PANDAS_NULLABLE, ConstructorName.PANDAS_PYARROW} - -_ALL_CPU_EXCLUSIONS: frozenset[ConstructorName] = frozenset( - {ConstructorName.MODIN, ConstructorName.PYSPARK_CONNECT} -) -"""Backends excluded from `--all-cpu-constructors` even when installed: - -* modin is too slow for the full matrix -* pyspark[connect] needs a different local setup and can't run alongside pyspark -""" - def _pandas_version() -> tuple[int, ...]: try: import pandas as pd except ImportError: # pragma: no cover return (0, 0, 0) + + from narwhals._utils import parse_version + return parse_version(pd.__version__) @@ -47,10 +44,14 @@ def _default_constructor_ids() -> list[str]: """ if env := os.environ.get("NARWHALS_DEFAULT_CONSTRUCTORS"): # pragma: no cover return env.split(",") + from narwhals.testing.constructors import DEFAULT_CONSTRUCTORS, prepare_constructors + return [str(c.name) for c in prepare_constructors(include=DEFAULT_CONSTRUCTORS)] def pytest_addoption(parser: pytest.Parser) -> None: + from narwhals.testing.constructors import DEFAULT_CONSTRUCTORS + group = parser.getgroup("narwhals", "narwhals.testing") defaults = ", ".join(f"'{c.value}'" for c in sorted(DEFAULT_CONSTRUCTORS)) group.addoption( @@ -89,9 +90,19 @@ def pytest_addoption(parser: pytest.Parser) -> None: def _select_constructors( config: pytest.Config, ) -> list[ConstructorBase]: # pragma: no cover + from narwhals.testing.constructors import ( + ALL_CPU_CONSTRUCTORS, + ConstructorName, + prepare_constructors, + ) + + _all_cpu_exclusions = frozenset( + {ConstructorName.MODIN, ConstructorName.PYSPARK_CONNECT} + ) + if config.getoption("all_cpu_constructors"): selected = prepare_constructors( - include=ALL_CPU_CONSTRUCTORS, exclude=_ALL_CPU_EXCLUSIONS + include=ALL_CPU_CONSTRUCTORS, exclude=_all_cpu_exclusions ) else: opt = cast("str", config.getoption("constructors")) @@ -99,7 +110,11 @@ def _select_constructors( selected = prepare_constructors(include=names) if _pandas_version() < _MIN_PANDAS_NULLABLE_VERSION: - selected = [c for c in selected if c.name not in _PANDAS_NULLABLES] + _pandas_nullables = { + ConstructorName.PANDAS_NULLABLE, + ConstructorName.PANDAS_PYARROW, + } + selected = [c for c in selected if c.name not in _pandas_nullables] return selected diff --git a/pyproject.toml b/pyproject.toml index 8b688effda..024538f384 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -304,6 +304,7 @@ env = [ [tool.coverage.run] plugins = ["covdefaults"] +source = ["narwhals", "tests"] [tool.coverage.report] fail_under = 80 # This is just for local development, in CI we set it to 100 From db4784e208545dfcb5879f1c16a4cfa7bf56f830 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 13:14:10 +0200 Subject: [PATCH 11/71] adjust GHA accordingly --- .github/workflows/extremes.yml | 16 +++++++++++----- .github/workflows/pytest-pyspark.yml | 9 ++++++--- .github/workflows/pytest.yml | 19 ++++++++++++++----- .github/workflows/random_ci_pytest.yml | 4 ++-- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/.github/workflows/extremes.yml b/.github/workflows/extremes.yml index c3b20f64e4..52ffb6d8e1 100644 --- a/.github/workflows/extremes.yml +++ b/.github/workflows/extremes.yml @@ -44,7 +44,9 @@ jobs: echo "$DEPS" | grep 'scikit-learn==1.1.0' echo "$DEPS" | grep 'duckdb==1.1' - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb + run: | + coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb + coverage report pretty_old_versions: strategy: @@ -82,7 +84,9 @@ jobs: echo "$DEPS" | grep 'scikit-learn==1.1.0' echo "$DEPS" | grep 'duckdb==1.2' - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb + run: | + coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb + coverage report not_so_old_versions: strategy: @@ -119,7 +123,9 @@ jobs: echo "$DEPS" | grep 'dask==2024.10' echo "$DEPS" | grep 'duckdb==1.3' - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb + run: | + coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb + coverage report nightlies: strategy: @@ -175,5 +181,5 @@ jobs: echo "$DEPS" | grep 'dask.*@' - name: Run pytest run: | - pytest tests --cov=narwhals --cov=tests --cov-fail-under=50 --runslow \ - --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb + coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb + coverage report diff --git a/.github/workflows/pytest-pyspark.yml b/.github/workflows/pytest-pyspark.yml index 043c630abd..bfc332faf7 100644 --- a/.github/workflows/pytest-pyspark.yml +++ b/.github/workflows/pytest-pyspark.yml @@ -40,8 +40,9 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors pyspark - + run: | + coverage run -m pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors pyspark + coverage report pytest-pyspark-min-version-constructor: strategy: @@ -133,7 +134,9 @@ jobs: echo "Spark Connect server started" - name: Run pytest - run: pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors "pyspark[connect]" + run: | + coverage run -m pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors "pyspark[connect]" + coverage report - name: Stop Spark Connect server if: always() diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 9f6205d1cc..7848573e3b 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -30,7 +30,9 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] + run: | + coverage run -m pytest tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] + coverage report - name: install-test-plugin run: uv pip install -e test-plugin/. @@ -60,7 +62,8 @@ jobs: run: uv pip freeze - name: Run pytest run: | - pytest tests --cov=narwhals --cov=tests --runslow --cov-fail-under=95 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 + coverage run -m pytest tests --runslow --cov-fail-under=95 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 + coverage report pytest-full-coverage: strategy: @@ -91,7 +94,9 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=100 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 + run: | + coverage run -m pytest tests --cov-fail-under=100 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 + coverage report - name: Run doctests # reprs differ between versions, so we only run doctests on the latest Python if: matrix.python-version == '3.13' @@ -157,7 +162,9 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --cov-fail-under=50 + run: | + coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --cov-fail-under=50 + coverage report python-314t: strategy: @@ -183,4 +190,6 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow --cov-fail-under=50 + run: | + coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow --cov-fail-under=50 + coverage report diff --git a/.github/workflows/random_ci_pytest.yml b/.github/workflows/random_ci_pytest.yml index 2950989871..e39d8871f9 100644 --- a/.github/workflows/random_ci_pytest.yml +++ b/.github/workflows/random_ci_pytest.yml @@ -36,5 +36,5 @@ jobs: run: uv pip freeze - name: Run pytest run: | - pytest tests --cov=narwhals --cov=tests --cov-fail-under=75 \ - --constructors=pandas,pyarrow,polars[eager],polars[lazy] + coverage run -m pytest tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] + coverage report From 1ad6ffb0a1f80df6de4741f7dfd4a8f7fcfdb9d5 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 14:09:51 +0200 Subject: [PATCH 12/71] constructors module coverage --- narwhals/testing/constructors/_classes.py | 3 +- narwhals/testing/constructors/_name.py | 5 +- narwhals/testing/pytest_plugin.py | 2 + tests/testing/constructors_test.py | 115 +++++++++++++++++++++- 4 files changed, 120 insertions(+), 5 deletions(-) diff --git a/narwhals/testing/constructors/_classes.py b/narwhals/testing/constructors/_classes.py index 7c17239a3b..7039dec150 100644 --- a/narwhals/testing/constructors/_classes.py +++ b/narwhals/testing/constructors/_classes.py @@ -154,8 +154,7 @@ def __init_subclass__( instance = cls() ConstructorBase._registry[cls.name] = instance ConstructorBase._requirements[cls.name] = requirements - if legacy_name: - ConstructorBase._legacy_names[cls.name] = legacy_name + ConstructorBase._legacy_names[cls.name] = legacy_name @abstractmethod def __call__(self, obj: Data, /, **kwds: Any) -> IntoFrame: diff --git a/narwhals/testing/constructors/_name.py b/narwhals/testing/constructors/_name.py index c3e33dbd31..8fa2d080e0 100644 --- a/narwhals/testing/constructors/_name.py +++ b/narwhals/testing/constructors/_name.py @@ -171,8 +171,11 @@ def needs_gpu(self) -> bool: """Whether this constructor requires GPU hardware.""" return self is ConstructorName.CUDF + # TODO(Unassigned): remove 'no cover' flag once used in test suite @classmethod - def from_pytest_request(cls, request: pytest.FixtureRequest) -> ConstructorName: + def from_pytest_request( + cls, request: pytest.FixtureRequest + ) -> ConstructorName: # pragma: no cover """Resolve the [`ConstructorName`][] from the current parametrised pytest request. Examples: diff --git a/narwhals/testing/pytest_plugin.py b/narwhals/testing/pytest_plugin.py index 78dc8e4e53..936ce08e64 100644 --- a/narwhals/testing/pytest_plugin.py +++ b/narwhals/testing/pytest_plugin.py @@ -138,3 +138,5 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: params = [c for c in selected if c.name.is_eager and c.name.is_pandas_like] ids = [str(c.name) for c in params] metafunc.parametrize("constructor_pandas_like", params, ids=ids) + else: # pragma: no cover + ... diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index 0821273ca9..aea51160d8 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -1,8 +1,29 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +import pytest + import narwhals as nw -from narwhals.testing.constructors import ConstructorName -from narwhals.testing.constructors._classes import DaskConstructor +from narwhals._utils import Implementation +from narwhals.testing.constructors import ( + ConstructorName, + get_constructor, + prepare_constructors, +) +from narwhals.testing.constructors._classes import ( + ConstructorBase, + DaskConstructor, + PandasConstructor, + PolarsEagerConstructor as OriginalPolarsEagerConstructor, +) + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + + PropertyName: TypeAlias = str + ReturnTrue: TypeAlias = set[ConstructorName] + ReturnFalse: TypeAlias = set[ConstructorName] def test_dask_npartitions_distinct() -> None: @@ -27,3 +48,93 @@ def test_lazy_returns_lazy_frame() -> None: constructor = ConstructorName.POLARS_LAZY.constructor lf = nw.from_native(constructor(data)) assert isinstance(lf, nw.LazyFrame) + + +CN = ConstructorName + + +_IS_PROPERTY_CASES: list[tuple[PropertyName, ReturnTrue, ReturnFalse]] = [ + ("is_pandas", {CN.PANDAS, CN.PANDAS_NULLABLE, CN.PANDAS_PYARROW}, {CN.POLARS_EAGER}), + ("is_modin", {CN.MODIN, CN.MODIN_PYARROW}, {CN.PANDAS}), + ("is_cudf", {CN.CUDF}, {CN.PANDAS}), + ("is_pandas_like", {CN.PANDAS, CN.MODIN, CN.CUDF}, {CN.POLARS_EAGER}), + ("is_polars", {CN.POLARS_EAGER, CN.POLARS_LAZY}, {CN.PANDAS}), + ("is_pyarrow", {CN.PYARROW}, {CN.PANDAS}), + ("is_dask", {CN.DASK}, {CN.PANDAS}), + ("is_duckdb", {CN.DUCKDB}, {CN.PANDAS}), + ("is_pyspark", {CN.PYSPARK, CN.PYSPARK_CONNECT}, {CN.PANDAS}), + ("is_sqlframe", {CN.SQLFRAME}, {CN.PANDAS}), + ("is_ibis", {CN.IBIS}, {CN.PANDAS}), + ("is_spark_like", {CN.PYSPARK, CN.SQLFRAME, CN.PYSPARK_CONNECT}, {CN.PANDAS}), + ("is_lazy", {CN.POLARS_LAZY, CN.DASK, CN.DUCKDB}, {CN.PANDAS}), + ("needs_pyarrow", {CN.PYARROW, CN.DUCKDB, CN.IBIS}, {CN.PANDAS}), + ("is_non_nullable", {CN.PANDAS, CN.MODIN, CN.DASK}, {CN.POLARS_EAGER}), +] + + +@pytest.mark.parametrize(("prop", "true_names", "false_names"), _IS_PROPERTY_CASES) +def test_constructor_name_is_properties( + prop: str, true_names: set[ConstructorName], false_names: set[ConstructorName] +) -> None: + for name in true_names: + assert getattr(name, prop), f"{name}.{prop} should be True" + for name in false_names: + assert not getattr(name, prop), f"{name}.{prop} should be False" + + +def test_constructor_name_implementation() -> None: + assert CN.PANDAS.implementation is Implementation.PANDAS + assert CN.PANDAS_PYARROW.implementation is Implementation.PANDAS + assert CN.POLARS_EAGER.implementation is Implementation.POLARS + assert CN.PYSPARK_CONNECT.implementation is Implementation.PYSPARK_CONNECT + + +def test_constructor_dunder() -> None: + c1 = ConstructorName.PANDAS.constructor + c2 = PandasConstructor() + assert c1.identifier == "pandas" + assert c1 == c2 + assert hash(c1) == hash(c2) + assert c1 != OriginalPolarsEagerConstructor() + assert c1 != "not a constructor" + + +def test_init_subclass_no_legacy_name() -> None: + class _Dummy(ConstructorBase, requirements=("polars",)): + name = ConstructorName.POLARS_EAGER + + def __call__(self, obj: object, /, **kwds: object) -> None: # type: ignore[override] + ... # pragma: no cover + + # re-registered POLARS_EAGER (overwriting the real one), but without adding a legacy_name entry for it. + assert ConstructorBase._registry[ConstructorName.POLARS_EAGER] == _Dummy() + assert ConstructorBase._legacy_names[ConstructorName.POLARS_EAGER] == "" + + # Restore the original + legacy_name = "polars_eager_constructor" + + class PolarsEagerConstructor( + OriginalPolarsEagerConstructor, requirements=("polars",), legacy_name=legacy_name + ): + name = ConstructorName.POLARS_EAGER + + original = PolarsEagerConstructor() + + assert ConstructorBase._registry[ConstructorName.POLARS_EAGER] == original + assert ConstructorBase._legacy_names[ConstructorName.POLARS_EAGER] == legacy_name + + +def test_get_constructor() -> None: + expected = ConstructorName.PANDAS_PYARROW.constructor + assert get_constructor("pandas[pyarrow]") == expected + + +def test_get_constructor_invalid_name() -> None: + with pytest.raises(ValueError, match="Unknown constructor"): + get_constructor("not_a_backend") + + +def test_prepare_constructors_exclude_only() -> None: + result = prepare_constructors(exclude=[ConstructorName.PANDAS]) + names = {c.name for c in result} + assert ConstructorName.PANDAS not in names From c833b8704d8e189f01237c8f5d75b9f22c55a907 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 14:10:07 +0200 Subject: [PATCH 13/71] add makefile rule --- Makefile | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Makefile b/Makefile index ae4bfc244d..9d353c8ae1 100644 --- a/Makefile +++ b/Makefile @@ -38,3 +38,14 @@ docs-serve: # Build and serve the docs locally $(VENV_BIN)/uv run --no-sync utils/generate_backend_completeness.py $(VENV_BIN)/uv run --no-sync utils/generate_zen_content.py $(VENV_BIN)/uv run --no-sync zensical serve + +.PHONY: test +test: ## Run unittest + $(VENV_BIN)/uv pip install \ + --upgrade \ + --editable test-plugin/. \ + --editable .[ibis,modin,pyspark] \ + --group core \ + --group tests + $(VENV_BIN)/uv run --no-sync coverage run -m pytest tests --cov-fail-under=95 --all-cpu-constructors + $(VENV_BIN)/uv run --no-sync coverage report From 380b7be864710765286535dcf69c69297a06dc86 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 16:00:01 +0200 Subject: [PATCH 14/71] fix test deps --- tests/testing/constructors_test.py | 16 ++++++++++------ tests/testing/plugin_test.py | 14 +++++++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index aea51160d8..7c19e0a895 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -37,16 +37,20 @@ def test_dask_repr() -> None: def test_eager_returns_eager_frame() -> None: - data = {"x": [1, 2, 3]} - constructor = ConstructorName.PANDAS.constructor - df = nw.from_native(constructor(data)) + cn = ConstructorName.PANDAS + if not cn.is_available: + pytest.skip() + + df = nw.from_native(cn.constructor({"x": [1, 2, 3]})) assert isinstance(df, nw.DataFrame) def test_lazy_returns_lazy_frame() -> None: - data = {"x": [1, 2, 3]} - constructor = ConstructorName.POLARS_LAZY.constructor - lf = nw.from_native(constructor(data)) + cn = ConstructorName.POLARS_LAZY + if not cn.is_available: + pytest.skip() + + lf = nw.from_native(cn.constructor({"x": [1, 2, 3]})) assert isinstance(lf, nw.LazyFrame) diff --git a/tests/testing/plugin_test.py b/tests/testing/plugin_test.py index c3eaf8c2fc..7023ae4f41 100644 --- a/tests/testing/plugin_test.py +++ b/tests/testing/plugin_test.py @@ -1,10 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - import pytest - +import pytest pytest_plugins = ["pytester"] @@ -12,6 +8,10 @@ def test_constructor_eager_fixture_runs_for_each_backend( pytester: pytest.Pytester, ) -> None: + pytest.importorskip("pandas") + pytest.importorskip("polars") + pytest.importorskip("pyarrow") + pytester.makeconftest("") pytester.makepyfile(""" import narwhals as nw @@ -35,6 +35,10 @@ def test_shape(constructor_eager: ConstructorEager) -> None: def test_constructor_fixture_includes_lazy_backends(pytester: pytest.Pytester) -> None: + pytest.importorskip("pandas") + pytest.importorskip("polars") + pytest.importorskip("duckdb") + pytester.makeconftest("") pytester.makepyfile(""" import narwhals as nw From 612b470466c4f263ff182e745722019cd5f1b45e Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 16:00:29 +0200 Subject: [PATCH 15/71] try with coverage run ... && coverage report --- .github/workflows/extremes.yml | 16 ++++++++-------- .github/workflows/pytest-pyspark.yml | 8 ++++---- .github/workflows/pytest.yml | 20 ++++++++++---------- .github/workflows/random_ci_pytest.yml | 4 ++-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/extremes.yml b/.github/workflows/extremes.yml index 52ffb6d8e1..edbd0ddb2d 100644 --- a/.github/workflows/extremes.yml +++ b/.github/workflows/extremes.yml @@ -45,8 +45,8 @@ jobs: echo "$DEPS" | grep 'duckdb==1.1' - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb - coverage report + coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb \ + && coverage report pretty_old_versions: strategy: @@ -85,8 +85,8 @@ jobs: echo "$DEPS" | grep 'duckdb==1.2' - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb - coverage report + coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb \ + && coverage report not_so_old_versions: strategy: @@ -124,8 +124,8 @@ jobs: echo "$DEPS" | grep 'duckdb==1.3' - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb - coverage report + coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb \ + && coverage report nightlies: strategy: @@ -181,5 +181,5 @@ jobs: echo "$DEPS" | grep 'dask.*@' - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb - coverage report + coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb \ + && coverage report diff --git a/.github/workflows/pytest-pyspark.yml b/.github/workflows/pytest-pyspark.yml index bfc332faf7..341ff1eb50 100644 --- a/.github/workflows/pytest-pyspark.yml +++ b/.github/workflows/pytest-pyspark.yml @@ -41,8 +41,8 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors pyspark - coverage report + coverage run -m pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors pyspark \ + && coverage report pytest-pyspark-min-version-constructor: strategy: @@ -135,8 +135,8 @@ jobs: - name: Run pytest run: | - coverage run -m pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors "pyspark[connect]" - coverage report + coverage run -m pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors "pyspark[connect]" \ + && coverage report - name: Stop Spark Connect server if: always() diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 7848573e3b..d87b1874a1 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -31,8 +31,8 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] - coverage report + coverage run -m pytest tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] \ + && coverage report - name: install-test-plugin run: uv pip install -e test-plugin/. @@ -62,8 +62,8 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --cov-fail-under=95 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 - coverage report + coverage run -m pytest tests --runslow --cov-fail-under=95 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 \ + && coverage report pytest-full-coverage: strategy: @@ -95,8 +95,8 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=100 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 - coverage report + coverage run -m pytest tests --cov-fail-under=100 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 \ + && coverage report - name: Run doctests # reprs differ between versions, so we only run doctests on the latest Python if: matrix.python-version == '3.13' @@ -163,8 +163,8 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --cov-fail-under=50 - coverage report + coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --cov-fail-under=50 \ + && coverage report python-314t: strategy: @@ -191,5 +191,5 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow --cov-fail-under=50 - coverage report + coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow --cov-fail-under=50 \ + && coverage report diff --git a/.github/workflows/random_ci_pytest.yml b/.github/workflows/random_ci_pytest.yml index e39d8871f9..d8ecc212df 100644 --- a/.github/workflows/random_ci_pytest.yml +++ b/.github/workflows/random_ci_pytest.yml @@ -36,5 +36,5 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] - coverage report + coverage run -m pytest tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] \ + && coverage report From af9986180537bce603f7b78f35025076ab54d14a Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 16:27:12 +0200 Subject: [PATCH 16/71] fix(typing): WIP --- narwhals/testing/constructors/_classes.py | 9 +- narwhals/testing/typing.py | 2 +- tests/frame/group_by_test.py | 2 +- tests/ibis_test.py | 27 +--- tests/series_only/hist_test.py | 142 ++++++---------------- tests/series_only/is_sorted_test.py | 2 +- 6 files changed, 49 insertions(+), 135 deletions(-) diff --git a/narwhals/testing/constructors/_classes.py b/narwhals/testing/constructors/_classes.py index 7039dec150..fc8ccc2ba3 100644 --- a/narwhals/testing/constructors/_classes.py +++ b/narwhals/testing/constructors/_classes.py @@ -42,10 +42,9 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ...: import os import uuid import warnings -from abc import ABC, abstractmethod from copy import deepcopy from functools import lru_cache -from typing import TYPE_CHECKING, Any, ClassVar, cast +from typing import TYPE_CHECKING, Any, ClassVar, Protocol, cast from narwhals._utils import generate_temporary_column_name from narwhals.testing.constructors._name import ConstructorName @@ -116,7 +115,7 @@ def _pyspark_session_lazy() -> SparkSession: # pragma: no cover return session -class ConstructorBase(ABC): +class ConstructorBase(Protocol): """Abstract base for any constructor exposed by `narwhals.testing`. A constructor is a callable that turns a column-oriented `dict` (typed as @@ -156,9 +155,9 @@ def __init_subclass__( ConstructorBase._requirements[cls.name] = requirements ConstructorBase._legacy_names[cls.name] = legacy_name - @abstractmethod def __call__(self, obj: Data, /, **kwds: Any) -> IntoFrame: """Build a native frame from `obj`.""" + ... @property def identifier(self) -> str: @@ -187,14 +186,12 @@ def __eq__(self, other: object) -> bool: class ConstructorEagerBase(ConstructorBase): """A constructor that returns an *eager* native dataframe.""" - @abstractmethod def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: ... class ConstructorLazyBase(ConstructorBase): """A constructor that returns a *lazy* native frame.""" - @abstractmethod def __call__(self, obj: Data, /, **kwds: Any) -> IntoLazyFrame: ... diff --git a/narwhals/testing/typing.py b/narwhals/testing/typing.py index 3893b4d709..a006a5d4c2 100644 --- a/narwhals/testing/typing.py +++ b/narwhals/testing/typing.py @@ -11,7 +11,7 @@ ConstructorLazyBase, ) -Data: TypeAlias = dict[str, list[Any]] +Data: TypeAlias = dict[str, Any] # TODO(Unassined): This should have a better annotation """A column-oriented mapping used as input to a [`Constructor`][].""" Constructor: TypeAlias = "ConstructorBase" diff --git a/tests/frame/group_by_test.py b/tests/frame/group_by_test.py index f4903d63bf..7690be5124 100644 --- a/tests/frame/group_by_test.py +++ b/tests/frame/group_by_test.py @@ -25,7 +25,7 @@ from narwhals.typing import NonNestedLiteral -data: Mapping[str, Any] = {"a": [1, 1, 3], "b": [4, 4, 6], "c": [7.0, 8.0, 9.0]} +data: dict[str, list[Any]] = {"a": [1, 1, 3], "b": [4, 4, 6], "c": [7.0, 8.0, 9.0]} POLARS_COLLECT_STREAMING_ENGINE = os.environ.get("NARWHALS_POLARS_NEW_STREAMING", None) diff --git a/tests/ibis_test.py b/tests/ibis_test.py index cb9900e7d5..06926f821d 100644 --- a/tests/ibis_test.py +++ b/tests/ibis_test.py @@ -1,30 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any - import pytest import narwhals as nw - -if TYPE_CHECKING: - import ibis - import polars as pl - - from narwhals.testing.typing import Constructor -else: - ibis = pytest.importorskip("ibis") - pl = pytest.importorskip("polars") - - -@pytest.fixture -def ibis_constructor() -> Constructor: - def func(data: dict[str, Any]) -> ibis.Table: - df = pl.DataFrame(data) - return ibis.memtable(df) - - return func +from narwhals.testing.constructors import ConstructorName -def test_from_native(ibis_constructor: Constructor) -> None: - df = nw.from_native(ibis_constructor({"a": [1, 2, 3], "b": [4, 5, 6]})) +def test_from_native() -> None: + if not (name := ConstructorName.IBIS).is_available: + pytest.skip() + df = nw.from_native(name.constructor({"a": [1, 2, 3], "b": [4, 5, 6]})) assert df.columns == ["a", "b"] diff --git a/tests/series_only/hist_test.py b/tests/series_only/hist_test.py index 34f4d2523a..3e58e5b524 100644 --- a/tests/series_only/hist_test.py +++ b/tests/series_only/hist_test.py @@ -3,7 +3,7 @@ from __future__ import annotations from random import Random -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import hypothesis.strategies as st import pytest @@ -11,6 +11,7 @@ import narwhals as nw from narwhals.exceptions import ComputeError +from narwhals.testing.constructors import ConstructorName from tests.utils import POLARS_VERSION, assert_equal_data if TYPE_CHECKING: @@ -45,7 +46,18 @@ param_include_breakpoint = pytest.mark.parametrize( "include_breakpoint", [True, False], ids=["breakpoint-True", "breakpoint-False"] ) -param_library = pytest.mark.parametrize("library", ["pandas", "polars", "pyarrow"]) +param_name = pytest.mark.parametrize( + "name", + [ConstructorName.PANDAS, ConstructorName.POLARS_EAGER, ConstructorName.PYARROW], +) + + +def maybe_name_to_constructor(name: ConstructorName) -> ConstructorEager: + if name.is_available: + return cast("ConstructorEager", name.constructor) + + pytest.skip() + SHIFT_BINS_BY = 10 """shift bins property""" @@ -65,31 +77,15 @@ ], ids=str, ) -@param_library +@param_name def test_hist_bin( - library: str, + name: ConstructorName, bins: list[float], expected: Sequence[float], *, include_breakpoint: bool, ) -> None: - constructor_eager: ConstructorEager - pytest.importorskip(library) - if library == "pandas": - import pandas as pd - - constructor_eager = pd.DataFrame - elif library == "polars": - import polars as pl - - constructor_eager = pl.DataFrame - else: - import pyarrow as pa - - pytest.importorskip("numpy") - - constructor_eager = pa.table - + constructor_eager = maybe_name_to_constructor(name) df = nw.from_native(constructor_eager(data)).with_columns( float=nw.col("int").cast(nw.Float64) ) @@ -132,22 +128,11 @@ def test_hist_bin( @pytest.mark.parametrize("params", counts_and_expected) @param_include_breakpoint -@param_library +@param_name def test_hist_count( - library: str, *, params: dict[str, Any], include_breakpoint: bool + name: ConstructorName, *, params: dict[str, Any], include_breakpoint: bool ) -> None: - if library == "pandas": - pytest.importorskip("pandas") - import pandas as pd - - constructor_eager: Any = pd.DataFrame - elif library == "polars": - pl = pytest.importorskip("polars") - constructor_eager = pl.DataFrame - else: - pa = pytest.importorskip("pyarrow") - pytest.importorskip("numpy") - constructor_eager = pa.table + constructor_eager = maybe_name_to_constructor(name) df = nw.from_native(constructor_eager(data)).with_columns( float=nw.col("int").cast(nw.Float64) ) @@ -188,20 +173,9 @@ def test_hist_count( ) -@param_library -def test_hist_count_no_spread(library: str) -> None: - if library == "pandas": - pytest.importorskip("pandas") - import pandas as pd - - constructor_eager: Any = pd.DataFrame - elif library == "polars": - pl = pytest.importorskip("polars") - constructor_eager = pl.DataFrame - else: - pa = pytest.importorskip("pyarrow") - pytest.importorskip("numpy") - constructor_eager = pa.table +@param_name +def test_hist_count_no_spread(name: ConstructorName) -> None: + constructor_eager = maybe_name_to_constructor(name) data = {"all_zero": [0, 0, 0], "all_non_zero": [5, 5, 5]} df = nw.from_native(constructor_eager(data)) @@ -231,20 +205,9 @@ def test_hist_bin_and_bin_count() -> None: @param_include_breakpoint -@param_library -def test_hist_no_data(library: str, *, include_breakpoint: bool) -> None: - if library == "pandas": - pytest.importorskip("pandas") - import pandas as pd - - constructor_eager: Any = pd.DataFrame - elif library == "polars": - pl = pytest.importorskip("polars") - constructor_eager = pl.DataFrame - else: - pa = pytest.importorskip("pyarrow") - pytest.importorskip("numpy") - constructor_eager = pa.table +@param_name +def test_hist_no_data(name: ConstructorName, *, include_breakpoint: bool) -> None: + constructor_eager = maybe_name_to_constructor(name) s = nw.from_native(constructor_eager({"values": []})).select( nw.col("values").cast(nw.Float64) )["values"] @@ -264,20 +227,9 @@ def test_hist_no_data(library: str, *, include_breakpoint: bool) -> None: assert result["count"].sum() == 0 -@param_library -def test_hist_small_bins(library: str) -> None: - if library == "pandas": - pytest.importorskip("pandas") - import pandas as pd - - constructor_eager: Any = pd.DataFrame - elif library == "polars": - pl = pytest.importorskip("polars") - constructor_eager = pl.DataFrame - else: - pa = pytest.importorskip("pyarrow") - pytest.importorskip("numpy") - constructor_eager = pa.table +@param_name +def test_hist_small_bins(name: ConstructorName) -> None: + constructor_eager = maybe_name_to_constructor(name) s = nw.from_native(constructor_eager({"values": [1, 2, 3]})) result = s["values"].hist(bins=None, bin_count=None) assert len(result) == 10 @@ -325,24 +277,13 @@ def test_hist_non_monotonic(constructor_eager: ConstructorEager) -> None: POLARS_VERSION < (1, 27), reason="polars cannot be used for compatibility checks since narwhals aims to mimic polars>=1.27 behavior", ) -@param_library +@param_name @pytest.mark.filterwarnings("ignore:invalid value encountered in cast:RuntimeWarning") @pytest.mark.slow def test_hist_bin_hypotheis( - library: str, data: list[float], bin_deltas: list[float] + name: ConstructorName, data: list[float], bin_deltas: list[float] ) -> None: - if library == "pandas": - pytest.importorskip("pandas") - import pandas as pd - - constructor_eager: Any = pd.DataFrame - elif library == "polars": - pl = pytest.importorskip("polars") - constructor_eager = pl.DataFrame - else: - pa = pytest.importorskip("pyarrow") - pytest.importorskip("numpy") - constructor_eager = pa.table + constructor_eager = maybe_name_to_constructor(name) pytest.importorskip("polars") import polars as pl @@ -378,25 +319,18 @@ def test_hist_bin_hypotheis( reason="polars cannot be used for compatibility checks since narwhals aims to mimic polars>=1.27 behavior", ) @pytest.mark.filterwarnings("ignore:invalid value encountered in cast:RuntimeWarning") -@param_library +@param_name @pytest.mark.slow def test_hist_count_hypothesis( - library: str, data: list[float], bin_count: int, request: pytest.FixtureRequest + name: ConstructorName, + data: list[float], + bin_count: int, + request: pytest.FixtureRequest, ) -> None: pytest.importorskip("polars") import polars as pl - if library == "pandas": - pytest.importorskip("pandas") - import pandas as pd - - constructor_eager: Any = pd.DataFrame - elif library == "polars": - constructor_eager = pl.DataFrame - else: - pa = pytest.importorskip("pyarrow") - pytest.importorskip("numpy") - constructor_eager = pa.table + constructor_eager = maybe_name_to_constructor(name) df = nw.from_native(constructor_eager({"values": data})).select( nw.col("values").cast(nw.Float64) ) diff --git a/tests/series_only/is_sorted_test.py b/tests/series_only/is_sorted_test.py index 799807e685..716b382b25 100644 --- a/tests/series_only/is_sorted_test.py +++ b/tests/series_only/is_sorted_test.py @@ -21,7 +21,7 @@ ) def test_is_sorted( constructor_eager: ConstructorEager, - input_data: str, + input_data: list[int], descending: bool, # noqa: FBT001 expected: bool, # noqa: FBT001 ) -> None: From 6e01b9b92232d7b623260a1d21a8460519c03231 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 16:29:16 +0200 Subject: [PATCH 17/71] revert 7e2c50d8bf2205e27d04addb4199f64823af083f --- .../is_narwhals_dataframe_test.py | 2 +- .../is_narwhals_lazyframe_test.py | 4 +- tests/dependencies/is_narwhals_series_test.py | 2 +- tests/dtypes/dtypes_test.py | 12 ++++-- tests/expr_and_series/abs_test.py | 7 +-- tests/expr_and_series/all_horizontal_test.py | 7 +-- tests/expr_and_series/any_all_test.py | 7 +-- tests/expr_and_series/any_horizontal_test.py | 6 +-- tests/expr_and_series/any_value_test.py | 2 +- tests/expr_and_series/arithmetic_test.py | 14 +++--- tests/expr_and_series/binary_test.py | 7 +-- tests/expr_and_series/cast_test.py | 3 +- .../cat/get_categories_test.py | 7 +-- tests/expr_and_series/clip_test.py | 7 +-- tests/expr_and_series/coalesce_test.py | 7 +-- tests/expr_and_series/concat_str_test.py | 7 +-- tests/expr_and_series/corr_test.py | 6 +-- tests/expr_and_series/cos_test.py | 9 +++- tests/expr_and_series/count_test.py | 7 +-- tests/expr_and_series/cum_count_test.py | 13 +++--- tests/expr_and_series/cum_max_test.py | 14 +++--- tests/expr_and_series/cum_min_test.py | 7 +-- tests/expr_and_series/cum_prod_test.py | 14 +++--- tests/expr_and_series/cum_sum_test.py | 7 +-- tests/expr_and_series/diff_test.py | 13 +++--- .../expr_and_series/division_by_zero_test.py | 7 +-- tests/expr_and_series/double_selected_test.py | 7 +-- tests/expr_and_series/double_test.py | 7 +-- tests/expr_and_series/drop_nulls_test.py | 7 +-- .../dt/convert_time_zone_test.py | 13 ++++-- .../dt/datetime_attributes_test.py | 4 +- .../dt/datetime_duration_test.py | 7 +-- tests/expr_and_series/dt/offset_by_test.py | 13 +++--- .../dt/replace_time_zone_test.py | 12 ++++-- tests/expr_and_series/dt/timestamp_test.py | 4 +- tests/expr_and_series/dt/to_string_test.py | 7 +-- tests/expr_and_series/dt/truncate_test.py | 6 +-- tests/expr_and_series/ewm_test.py | 7 +-- tests/expr_and_series/exclude_test.py | 2 +- tests/expr_and_series/exp_test.py | 7 +-- tests/expr_and_series/fill_nan_test.py | 29 +++++++------ tests/expr_and_series/fill_null_test.py | 10 ++++- tests/expr_and_series/filter_test.py | 7 +-- tests/expr_and_series/first_last_test.py | 11 +++-- tests/expr_and_series/floor_ceil_test.py | 7 +-- tests/expr_and_series/format_test.py | 7 +-- .../horizontal_broadcasts_test.py | 7 +-- tests/expr_and_series/is_between_test.py | 7 +-- tests/expr_and_series/is_close_test.py | 37 ++++++++++------ tests/expr_and_series/is_duplicated_test.py | 7 +-- tests/expr_and_series/is_finite_test.py | 29 +++++++------ .../expr_and_series/is_first_distinct_test.py | 13 +++--- tests/expr_and_series/is_in_test.py | 6 +-- .../expr_and_series/is_last_distinct_test.py | 13 +++--- tests/expr_and_series/is_nan_test.py | 31 +++++++------ tests/expr_and_series/is_null_test.py | 7 +-- tests/expr_and_series/is_unique_test.py | 7 +-- tests/expr_and_series/kurtosis_test.py | 7 +-- tests/expr_and_series/len_test.py | 7 +-- tests/expr_and_series/list/contains_test.py | 3 +- tests/expr_and_series/list/get_test.py | 12 +++--- tests/expr_and_series/list/len_test.py | 7 +-- tests/expr_and_series/list/max_test.py | 2 +- tests/expr_and_series/list/mean_test.py | 2 +- tests/expr_and_series/list/median_test.py | 2 +- tests/expr_and_series/list/min_test.py | 2 +- tests/expr_and_series/list/sort_test.py | 2 +- tests/expr_and_series/list/sum_test.py | 3 +- tests/expr_and_series/list/unique_test.py | 2 +- tests/expr_and_series/lit_test.py | 2 +- tests/expr_and_series/log_test.py | 6 +-- tests/expr_and_series/map_batches_test.py | 9 ++-- tests/expr_and_series/max_horizontal_test.py | 7 +-- tests/expr_and_series/max_test.py | 7 +-- tests/expr_and_series/mean_horizontal_test.py | 7 +-- tests/expr_and_series/mean_test.py | 7 +-- tests/expr_and_series/median_test.py | 6 +-- tests/expr_and_series/min_horizontal_test.py | 7 +-- tests/expr_and_series/min_test.py | 7 +-- tests/expr_and_series/mode_test.py | 7 +-- tests/expr_and_series/n_unique_test.py | 7 +-- tests/expr_and_series/name/keep_test.py | 7 +-- tests/expr_and_series/name/map_test.py | 7 +-- tests/expr_and_series/name/prefix_test.py | 7 +-- tests/expr_and_series/name/suffix_test.py | 7 +-- .../expr_and_series/name/to_lowercase_test.py | 7 +-- .../expr_and_series/name/to_uppercase_test.py | 7 +-- tests/expr_and_series/nth_test.py | 6 +-- tests/expr_and_series/null_count_test.py | 7 +-- tests/expr_and_series/operators_test.py | 7 +-- .../order_dependent_lazy_test.py | 2 +- tests/expr_and_series/over_pushdown_test.py | 7 +-- tests/expr_and_series/over_test.py | 13 +++--- tests/expr_and_series/pipe_test.py | 7 +-- tests/expr_and_series/quantile_test.py | 9 ++-- tests/expr_and_series/rank_test.py | 7 ++- tests/expr_and_series/reduction_test.py | 7 +-- tests/expr_and_series/replace_strict_test.py | 10 +++-- tests/expr_and_series/rolling_mean_test.py | 13 +++--- tests/expr_and_series/rolling_std_test.py | 14 +++--- tests/expr_and_series/rolling_sum_test.py | 14 +++--- tests/expr_and_series/rolling_var_test.py | 14 +++--- tests/expr_and_series/round_test.py | 7 +-- tests/expr_and_series/sample_test.py | 7 +-- tests/expr_and_series/shift_test.py | 13 +++--- tests/expr_and_series/sin_test.py | 9 +++- tests/expr_and_series/skew_test.py | 7 +-- tests/expr_and_series/sqrt_test.py | 9 +++- tests/expr_and_series/std_test.py | 7 +-- tests/expr_and_series/str/contains_test.py | 7 +-- tests/expr_and_series/str/head_test.py | 7 +-- tests/expr_and_series/str/len_chars_test.py | 7 +-- tests/expr_and_series/str/pad_test.py | 7 +-- tests/expr_and_series/str/replace_test.py | 7 +-- tests/expr_and_series/str/slice_test.py | 7 +-- tests/expr_and_series/str/split_test.py | 14 +++--- .../str/starts_with_ends_with_test.py | 8 ++-- tests/expr_and_series/str/strip_chars_test.py | 7 +-- tests/expr_and_series/str/tail_test.py | 7 +-- tests/expr_and_series/str/to_date_test.py | 2 +- tests/expr_and_series/str/to_datetime_test.py | 2 +- .../expr_and_series/str/to_titlecase_test.py | 7 +-- .../str/to_uppercase_to_lowercase_test.py | 7 +-- tests/expr_and_series/str/zfill_test.py | 7 +-- tests/expr_and_series/struct_/field_test.py | 7 +-- tests/expr_and_series/struct_test.py | 14 +++--- tests/expr_and_series/sum_horizontal_test.py | 7 +-- tests/expr_and_series/sum_test.py | 7 +-- tests/expr_and_series/unary_test.py | 6 +-- tests/expr_and_series/unique_test.py | 6 +-- tests/expr_and_series/var_test.py | 7 +-- tests/expr_and_series/when_test.py | 9 +++- tests/expression_parsing_test.py | 7 +-- tests/frame/add_test.py | 7 +-- tests/frame/array_dunder_test.py | 13 +++--- tests/frame/clone_test.py | 7 +-- tests/frame/collect_test.py | 3 +- tests/frame/columns_test.py | 2 +- tests/frame/concat_test.py | 4 +- tests/frame/double_test.py | 7 +-- tests/frame/drop_nulls_test.py | 7 +-- tests/frame/drop_test.py | 2 +- tests/frame/eq_test.py | 2 +- tests/frame/estimated_size_test.py | 2 +- tests/frame/explode_test.py | 4 +- tests/frame/filter_test.py | 7 +-- tests/frame/from_dict_test.py | 3 +- tests/frame/get_column_test.py | 7 +-- tests/frame/getitem_test.py | 3 +- tests/frame/group_by_test.py | 3 +- tests/frame/head_test.py | 7 +-- tests/frame/invalid_test.py | 5 +-- tests/frame/is_duplicated_test.py | 7 +-- tests/frame/is_empty_test.py | 2 +- tests/frame/is_unique_test.py | 7 +-- tests/frame/item_test.py | 7 +-- tests/frame/join_test.py | 9 +++- tests/frame/lazy_test.py | 11 +++-- tests/frame/len_test.py | 2 +- tests/frame/null_count_test.py | 7 +-- tests/frame/pipe_test.py | 7 +-- tests/frame/pivot_test.py | 7 +-- tests/frame/rename_test.py | 7 +-- tests/frame/row_test.py | 2 +- tests/frame/rows_test.py | 2 +- tests/frame/sample_test.py | 2 +- tests/frame/schema_test.py | 6 +-- tests/frame/select_test.py | 14 +++--- tests/frame/shape_test.py | 2 +- tests/frame/sink_parquet_test.py | 2 +- tests/frame/sort_test.py | 7 +-- tests/frame/tail_test.py | 7 +-- tests/frame/to_arrow_test.py | 2 +- tests/frame/to_dict_test.py | 7 +-- tests/frame/to_native_test.py | 2 +- tests/frame/to_numpy_test.py | 2 +- tests/frame/to_pandas_test.py | 4 +- tests/frame/to_polars_test.py | 2 +- tests/frame/top_k_test.py | 7 +-- tests/frame/unique_test.py | 7 +-- tests/frame/unpivot_test.py | 3 +- tests/frame/with_columns_sequence_test.py | 7 +-- tests/frame/with_columns_test.py | 13 +++--- tests/frame/with_row_index_test.py | 12 ++++-- tests/frame/write_csv_test.py | 4 +- tests/frame/write_parquet_test.py | 2 +- tests/from_dict_test.py | 3 +- tests/from_numpy_test.py | 3 +- tests/hypothesis/getitem_test.py | 10 ++--- tests/joblib_test.py | 7 +-- tests/modern_polars/ewm_mean_test.py | 7 +-- tests/modern_polars/filter_test.py | 6 +-- tests/modern_polars/method_chaining_2_test.py | 6 +-- tests/modern_polars/method_chaining_test.py | 11 +++-- tests/modern_polars/performance_test.py | 7 +-- tests/modern_polars/pivot_test.py | 6 +-- tests/modern_polars/unpivot_test.py | 6 +-- tests/namespace_test.py | 2 +- tests/new_series_test.py | 7 +-- tests/read_scan_test.py | 10 +++-- tests/selectors_test.py | 6 +-- tests/series_only/__contains___test.py | 2 +- tests/series_only/__iter___test.py | 2 +- tests/series_only/alias_rename_test.py | 7 +-- tests/series_only/arg_max_test.py | 7 +-- tests/series_only/arg_min_test.py | 7 +-- tests/series_only/arg_true_test.py | 7 +-- tests/series_only/array_dunder_test.py | 12 +++--- tests/series_only/cast_test.py | 2 +- tests/series_only/gather_every_test.py | 7 +-- tests/series_only/getitem_test.py | 2 +- tests/series_only/head_test.py | 7 +-- tests/series_only/hist_test.py | 4 +- tests/series_only/is_empty_test.py | 2 +- .../is_ordered_categorical_test.py | 2 +- tests/series_only/is_sorted_test.py | 7 +-- tests/series_only/item_test.py | 6 +-- tests/series_only/scatter_test.py | 4 +- tests/series_only/shape_test.py | 2 +- tests/series_only/sort_test.py | 2 +- tests/series_only/tail_test.py | 7 +-- tests/series_only/to_arrow_test.py | 2 +- tests/series_only/to_dummy_test.py | 7 +-- tests/series_only/to_frame_test.py | 7 +-- tests/series_only/to_list_test.py | 7 +-- tests/series_only/to_native_test.py | 2 +- tests/series_only/to_numpy_test.py | 2 +- tests/series_only/to_pandas_test.py | 2 +- tests/series_only/to_polars_test.py | 2 +- tests/series_only/value_counts_test.py | 7 +-- tests/series_only/zip_with_test.py | 7 +-- tests/testing/assert_frame_equal_test.py | 2 +- tests/testing/assert_series_equal_test.py | 2 +- tests/translate/from_native_test.py | 12 ++---- tests/translate/get_native_namespace_test.py | 4 +- tests/translate/to_native_test.py | 2 +- tests/utils.py | 43 +++++++++++++++++-- tests/v1_test.py | 4 +- tests/v2_test.py | 3 +- 239 files changed, 635 insertions(+), 1040 deletions(-) diff --git a/tests/dependencies/is_narwhals_dataframe_test.py b/tests/dependencies/is_narwhals_dataframe_test.py index 5269f6899b..aeedf15981 100644 --- a/tests/dependencies/is_narwhals_dataframe_test.py +++ b/tests/dependencies/is_narwhals_dataframe_test.py @@ -6,7 +6,7 @@ from narwhals.stable.v1.dependencies import is_narwhals_dataframe if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager def test_is_narwhals_dataframe(constructor_eager: ConstructorEager) -> None: diff --git a/tests/dependencies/is_narwhals_lazyframe_test.py b/tests/dependencies/is_narwhals_lazyframe_test.py index 3d78fbad7e..0e4c6e1bd9 100644 --- a/tests/dependencies/is_narwhals_lazyframe_test.py +++ b/tests/dependencies/is_narwhals_lazyframe_test.py @@ -4,10 +4,10 @@ import narwhals as nw from narwhals.stable.v1.dependencies import is_narwhals_lazyframe -from narwhals.testing.typing import Constructor +from tests.utils import Constructor if TYPE_CHECKING: - from narwhals.testing.typing import Constructor + from tests.utils import Constructor def test_is_narwhals_lazyframe(constructor: Constructor) -> None: diff --git a/tests/dependencies/is_narwhals_series_test.py b/tests/dependencies/is_narwhals_series_test.py index 92678f03e4..659696d108 100644 --- a/tests/dependencies/is_narwhals_series_test.py +++ b/tests/dependencies/is_narwhals_series_test.py @@ -6,7 +6,7 @@ from narwhals.stable.v1.dependencies import is_narwhals_series if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager def test_is_narwhals_series(constructor_eager: ConstructorEager) -> None: diff --git a/tests/dtypes/dtypes_test.py b/tests/dtypes/dtypes_test.py index 64c4827043..33fa61ac08 100644 --- a/tests/dtypes/dtypes_test.py +++ b/tests/dtypes/dtypes_test.py @@ -9,15 +9,19 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError, PerformanceWarning -from narwhals.testing.constructors import pyspark_session -from tests.utils import PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, assert_equal_hash +from tests.utils import ( + PANDAS_VERSION, + POLARS_VERSION, + PYARROW_VERSION, + assert_equal_hash, + pyspark_session, +) if TYPE_CHECKING: from collections.abc import Iterable - from narwhals.testing.typing import Constructor from narwhals.typing import IntoFrame, IntoSeries, NonNestedDType - from tests.utils import ConstructorPandasLike, NestedOrEnumDType + from tests.utils import Constructor, ConstructorPandasLike, NestedOrEnumDType @pytest.mark.parametrize("time_unit", ["us", "ns", "ms"]) diff --git a/tests/expr_and_series/abs_test.py b/tests/expr_and_series/abs_test.py index 35a6c26c0e..547ff9d8dc 100644 --- a/tests/expr_and_series/abs_test.py +++ b/tests/expr_and_series/abs_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data def test_abs(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/all_horizontal_test.py b/tests/expr_and_series/all_horizontal_test.py index 1ac6e98cb7..d980b3def3 100644 --- a/tests/expr_and_series/all_horizontal_test.py +++ b/tests/expr_and_series/all_horizontal_test.py @@ -1,15 +1,12 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data def test_allh(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/any_all_test.py b/tests/expr_and_series/any_all_test.py index e08d03c3cb..6b6cc6e35f 100644 --- a/tests/expr_and_series/any_all_test.py +++ b/tests/expr_and_series/any_all_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data def test_any_all(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/any_horizontal_test.py b/tests/expr_and_series/any_horizontal_test.py index 886efe092d..04f0cba76c 100644 --- a/tests/expr_and_series/any_horizontal_test.py +++ b/tests/expr_and_series/any_horizontal_test.py @@ -1,15 +1,11 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise -from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data def test_anyh(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/any_value_test.py b/tests/expr_and_series/any_value_test.py index 4c286943f5..e665fbd03c 100644 --- a/tests/expr_and_series/any_value_test.py +++ b/tests/expr_and_series/any_value_test.py @@ -8,7 +8,7 @@ from tests.utils import DUCKDB_VERSION, PYARROW_VERSION, assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager data = { "a": [1, 1, 1, 2, 2, 3], diff --git a/tests/expr_and_series/arithmetic_test.py b/tests/expr_and_series/arithmetic_test.py index 47c2d6ad6b..af0c464e5b 100644 --- a/tests/expr_and_series/arithmetic_test.py +++ b/tests/expr_and_series/arithmetic_test.py @@ -1,16 +1,20 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import hypothesis.strategies as st import pytest from hypothesis import assume, given import narwhals as nw -from tests.utils import DASK_VERSION, DUCKDB_VERSION, PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DASK_VERSION, + DUCKDB_VERSION, + PANDAS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) @pytest.mark.parametrize( diff --git a/tests/expr_and_series/binary_test.py b/tests/expr_and_series/binary_test.py index cd6484b904..6140ead120 100644 --- a/tests/expr_and_series/binary_test.py +++ b/tests/expr_and_series/binary_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DASK_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import DASK_VERSION, Constructor, assert_equal_data def test_expr_binary(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/cast_test.py b/tests/expr_and_series/cast_test.py index ad3015c0cd..90282ea596 100644 --- a/tests/expr_and_series/cast_test.py +++ b/tests/expr_and_series/cast_test.py @@ -9,6 +9,8 @@ from tests.utils import ( PANDAS_VERSION, PYARROW_VERSION, + Constructor, + ConstructorEager, assert_equal_data, is_pyarrow_windows_no_tzdata, time_unit_compat, @@ -17,7 +19,6 @@ if TYPE_CHECKING: from collections.abc import Mapping - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import NonNestedDType DATA = { diff --git a/tests/expr_and_series/cat/get_categories_test.py b/tests/expr_and_series/cat/get_categories_test.py index ed5520a91b..6f984ff7ce 100644 --- a/tests/expr_and_series/cat/get_categories_test.py +++ b/tests/expr_and_series/cat/get_categories_test.py @@ -1,15 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import PYARROW_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import PYARROW_VERSION, ConstructorEager, assert_equal_data data = {"a": ["one", "two", "two"]} diff --git a/tests/expr_and_series/clip_test.py b/tests/expr_and_series/clip_test.py index f2f4494b42..382133f23c 100644 --- a/tests/expr_and_series/clip_test.py +++ b/tests/expr_and_series/clip_test.py @@ -1,15 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw from narwhals.exceptions import MultiOutputExpressionError -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data @pytest.mark.parametrize( diff --git a/tests/expr_and_series/coalesce_test.py b/tests/expr_and_series/coalesce_test.py index f9cd2205dc..5311b39c85 100644 --- a/tests/expr_and_series/coalesce_test.py +++ b/tests/expr_and_series/coalesce_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data def test_coalesce_numeric(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/concat_str_test.py b/tests/expr_and_series/concat_str_test.py index 5ea68e010b..91e359c8fb 100644 --- a/tests/expr_and_series/concat_str_test.py +++ b/tests/expr_and_series/concat_str_test.py @@ -1,18 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable +from typing import Any, Callable import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, POLARS_VERSION, assert_equal_data +from tests.utils import PANDAS_VERSION, POLARS_VERSION, Constructor, assert_equal_data pytest.importorskip("pyarrow") import pyarrow as pa -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor - data = {"a": [1, 2, 3], "b": ["dogs", "cats", None], "c": ["play", "swim", "walk"]} diff --git a/tests/expr_and_series/corr_test.py b/tests/expr_and_series/corr_test.py index b37e61d3e1..15e371d62d 100644 --- a/tests/expr_and_series/corr_test.py +++ b/tests/expr_and_series/corr_test.py @@ -1,15 +1,11 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise -from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": [1, 3, 3], "b": [1, 2, 3], "c": [1, None, 1]} diff --git a/tests/expr_and_series/cos_test.py b/tests/expr_and_series/cos_test.py index ba96a9a083..4aa936d9bf 100644 --- a/tests/expr_and_series/cos_test.py +++ b/tests/expr_and_series/cos_test.py @@ -6,10 +6,15 @@ import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, PYARROW_VERSION, assert_equal_data +from tests.utils import ( + PANDAS_VERSION, + PYARROW_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import DTypeBackend data = {"a": [-pi, -pi / 2, 0.0, pi / 2, pi]} diff --git a/tests/expr_and_series/count_test.py b/tests/expr_and_series/count_test.py index 8ba620a28f..c3f34ee132 100644 --- a/tests/expr_and_series/count_test.py +++ b/tests/expr_and_series/count_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data def test_count(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/cum_count_test.py b/tests/expr_and_series/cum_count_test.py index 16af5089be..f25c27f1c3 100644 --- a/tests/expr_and_series/cum_count_test.py +++ b/tests/expr_and_series/cum_count_test.py @@ -1,14 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) data = {"a": ["x", "y", None, "z"]} diff --git a/tests/expr_and_series/cum_max_test.py b/tests/expr_and_series/cum_max_test.py index 65ccb49392..82cf5ba179 100644 --- a/tests/expr_and_series/cum_max_test.py +++ b/tests/expr_and_series/cum_max_test.py @@ -1,14 +1,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + PANDAS_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) data = {"a": [1, 3, None, 2]} diff --git a/tests/expr_and_series/cum_min_test.py b/tests/expr_and_series/cum_min_test.py index 1a5194ed71..43fc1f5b81 100644 --- a/tests/expr_and_series/cum_min_test.py +++ b/tests/expr_and_series/cum_min_test.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw @@ -10,13 +8,12 @@ PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, + Constructor, + ConstructorEager, assert_equal_data, is_windows, ) -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager - data = {"a": [3, 1, None, 2]} expected = {"cum_min": [3, 1, None, 1], "reverse_cum_min": [1, 1, None, 2]} diff --git a/tests/expr_and_series/cum_prod_test.py b/tests/expr_and_series/cum_prod_test.py index 397cb7f7fb..778c48bc67 100644 --- a/tests/expr_and_series/cum_prod_test.py +++ b/tests/expr_and_series/cum_prod_test.py @@ -1,14 +1,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + PANDAS_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) data = {"a": [1, 2, None, 3]} diff --git a/tests/expr_and_series/cum_sum_test.py b/tests/expr_and_series/cum_sum_test.py index a68969a949..f3f0f780db 100644 --- a/tests/expr_and_series/cum_sum_test.py +++ b/tests/expr_and_series/cum_sum_test.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw @@ -9,13 +7,12 @@ DUCKDB_VERSION, POLARS_VERSION, PYARROW_VERSION, + Constructor, + ConstructorEager, assert_equal_data, is_windows, ) -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager - data = {"arg entina": [1, 2, None, 4]} expected = {"cum_sum": [1, 3, None, 7], "reverse_cum_sum": [7, 6, None, 4]} diff --git a/tests/expr_and_series/diff_test.py b/tests/expr_and_series/diff_test.py index 02e897b276..dc7440541e 100644 --- a/tests/expr_and_series/diff_test.py +++ b/tests/expr_and_series/diff_test.py @@ -1,14 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) data = {"i": [0, 1, 2, 3, 4], "b": [1, 2, 3, 5, 3], "c": [5, 4, 3, 2, 1]} diff --git a/tests/expr_and_series/division_by_zero_test.py b/tests/expr_and_series/division_by_zero_test.py index cd3d86c29e..6e8313b522 100644 --- a/tests/expr_and_series/division_by_zero_test.py +++ b/tests/expr_and_series/division_by_zero_test.py @@ -1,15 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable +from typing import Any, Callable import pytest import narwhals as nw from narwhals._utils import zip_strict -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data data: dict[str, list[float]] = { "int": [-2, 0, 2], diff --git a/tests/expr_and_series/double_selected_test.py b/tests/expr_and_series/double_selected_test.py index ba5e9851d3..48b76222ec 100644 --- a/tests/expr_and_series/double_selected_test.py +++ b/tests/expr_and_series/double_selected_test.py @@ -1,15 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw from narwhals.exceptions import MultiOutputExpressionError -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data def test_double_selected(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/double_test.py b/tests/expr_and_series/double_test.py index 7064b879c9..de6a1afb09 100644 --- a/tests/expr_and_series/double_test.py +++ b/tests/expr_and_series/double_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data def test_double(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/drop_nulls_test.py b/tests/expr_and_series/drop_nulls_test.py index 1115137a27..d067552eb6 100644 --- a/tests/expr_and_series/drop_nulls_test.py +++ b/tests/expr_and_series/drop_nulls_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data def test_drop_nulls(constructor_eager: ConstructorEager) -> None: diff --git a/tests/expr_and_series/dt/convert_time_zone_test.py b/tests/expr_and_series/dt/convert_time_zone_test.py index 5145ddc0ca..65d1a6e3b6 100644 --- a/tests/expr_and_series/dt/convert_time_zone_test.py +++ b/tests/expr_and_series/dt/convert_time_zone_test.py @@ -7,12 +7,17 @@ import pytest import narwhals as nw -from narwhals.testing.constructors import pyspark_session -from narwhals.testing.typing import ConstructorEager -from tests.utils import PANDAS_VERSION, POLARS_VERSION, assert_equal_data, is_windows +from tests.utils import ( + PANDAS_VERSION, + POLARS_VERSION, + Constructor, + assert_equal_data, + is_windows, + pyspark_session, +) if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import ConstructorEager def test_convert_time_zone( diff --git a/tests/expr_and_series/dt/datetime_attributes_test.py b/tests/expr_and_series/dt/datetime_attributes_test.py index a1f125f54a..c7bf55e7c0 100644 --- a/tests/expr_and_series/dt/datetime_attributes_test.py +++ b/tests/expr_and_series/dt/datetime_attributes_test.py @@ -6,13 +6,11 @@ import pytest import narwhals as nw -from tests.utils import assert_equal_data +from tests.utils import Constructor, ConstructorEager, assert_equal_data if TYPE_CHECKING: import dask.dataframe as dd - from narwhals.testing.typing import Constructor, ConstructorEager - data = { "a": [datetime(2021, 3, 1, 12, 34, 56, 49000), datetime(2020, 1, 2, 2, 4, 14, 715000)] } diff --git a/tests/expr_and_series/dt/datetime_duration_test.py b/tests/expr_and_series/dt/datetime_duration_test.py index 637a6a95fb..b84ecfa66e 100644 --- a/tests/expr_and_series/dt/datetime_duration_test.py +++ b/tests/expr_and_series/dt/datetime_duration_test.py @@ -1,15 +1,12 @@ from __future__ import annotations from datetime import timedelta -from typing import TYPE_CHECKING, Literal +from typing import Literal import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data data = { "a": [None, timedelta(minutes=1, seconds=1, milliseconds=1, microseconds=1)], diff --git a/tests/expr_and_series/dt/offset_by_test.py b/tests/expr_and_series/dt/offset_by_test.py index 495b0bab4d..9085edb185 100644 --- a/tests/expr_and_series/dt/offset_by_test.py +++ b/tests/expr_and_series/dt/offset_by_test.py @@ -1,15 +1,18 @@ from __future__ import annotations from datetime import date, datetime, timezone -from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, assert_equal_data, is_windows - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + PANDAS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, + is_windows, +) data = { "a": [datetime(2021, 3, 1, 12, 34, 56, 49012), datetime(2020, 1, 2, 2, 4, 14, 715123)] diff --git a/tests/expr_and_series/dt/replace_time_zone_test.py b/tests/expr_and_series/dt/replace_time_zone_test.py index 94f61b4efe..1c9dff7d59 100644 --- a/tests/expr_and_series/dt/replace_time_zone_test.py +++ b/tests/expr_and_series/dt/replace_time_zone_test.py @@ -7,12 +7,16 @@ import pytest import narwhals as nw -from narwhals.testing.constructors import pyspark_session -from narwhals.testing.typing import ConstructorEager -from tests.utils import PANDAS_VERSION, assert_equal_data, is_windows +from tests.utils import ( + PANDAS_VERSION, + Constructor, + assert_equal_data, + is_windows, + pyspark_session, +) if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import ConstructorEager def test_replace_time_zone( diff --git a/tests/expr_and_series/dt/timestamp_test.py b/tests/expr_and_series/dt/timestamp_test.py index b216989fcc..e199fcac76 100644 --- a/tests/expr_and_series/dt/timestamp_test.py +++ b/tests/expr_and_series/dt/timestamp_test.py @@ -11,16 +11,16 @@ from tests.utils import ( PANDAS_VERSION, POLARS_VERSION, + Constructor, + ConstructorEager, assert_equal_data, is_pyarrow_windows_no_tzdata, time_unit_compat, ) if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import IntoSeriesT - data = { "a": [datetime(2021, 3, 1, 12, 34, 56, 49000), datetime(2020, 1, 2, 2, 4, 14, 715000)] } diff --git a/tests/expr_and_series/dt/to_string_test.py b/tests/expr_and_series/dt/to_string_test.py index 6b0a45d9c0..4fd4c14c66 100644 --- a/tests/expr_and_series/dt/to_string_test.py +++ b/tests/expr_and_series/dt/to_string_test.py @@ -1,15 +1,12 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import assert_equal_data, is_windows - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data, is_windows data = { "a": [datetime(2021, 3, 1, 12, 34, 56, 49000), datetime(2020, 1, 2, 2, 4, 14, 715000)] diff --git a/tests/expr_and_series/dt/truncate_test.py b/tests/expr_and_series/dt/truncate_test.py index 71bcd083d4..40ce18d428 100644 --- a/tests/expr_and_series/dt/truncate_test.py +++ b/tests/expr_and_series/dt/truncate_test.py @@ -1,16 +1,12 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING from zoneinfo import ZoneInfo import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data data = { "a": [datetime(2021, 3, 1, 12, 34, 56, 49012), datetime(2020, 1, 2, 2, 4, 14, 715123)] diff --git a/tests/expr_and_series/ewm_test.py b/tests/expr_and_series/ewm_test.py index 30f9511f5e..a3afae154b 100644 --- a/tests/expr_and_series/ewm_test.py +++ b/tests/expr_and_series/ewm_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data data = {"a": [1, 1, 2], "b": [1, 2, 3]} diff --git a/tests/expr_and_series/exclude_test.py b/tests/expr_and_series/exclude_test.py index 805f0218e2..4aa39478d2 100644 --- a/tests/expr_and_series/exclude_test.py +++ b/tests/expr_and_series/exclude_test.py @@ -8,7 +8,7 @@ from tests.utils import assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import Constructor + from tests.utils import Constructor @pytest.mark.parametrize( diff --git a/tests/expr_and_series/exp_test.py b/tests/expr_and_series/exp_test.py index 63df1f6923..7bfcd5c404 100644 --- a/tests/expr_and_series/exp_test.py +++ b/tests/expr_and_series/exp_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data data = {"a": [-1, 0, 1, 2, 4]} diff --git a/tests/expr_and_series/fill_nan_test.py b/tests/expr_and_series/fill_nan_test.py index a00e25eea2..132b553c50 100644 --- a/tests/expr_and_series/fill_nan_test.py +++ b/tests/expr_and_series/fill_nan_test.py @@ -1,21 +1,22 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from narwhals.testing.constructors import ConstructorName -from tests.utils import PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.conftest import ( + dask_lazy_p1_constructor, + dask_lazy_p2_constructor, + modin_constructor, + pandas_constructor, +) +from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data -NON_NULLABLE_CONSTRUCTORS = { - ConstructorName.MODIN, - ConstructorName.PANDAS, - ConstructorName.DASK, -} +NON_NULLABLE_CONSTRUCTORS = [ + pandas_constructor, + dask_lazy_p1_constructor, + dask_lazy_p2_constructor, + modin_constructor, +] def test_fill_nan(request: pytest.FixtureRequest, constructor: Constructor) -> None: @@ -35,7 +36,7 @@ def test_fill_nan(request: pytest.FixtureRequest, constructor: Constructor) -> N assert_equal_data(result, expected) assert result.lazy().collect()["float_na"].null_count() == 2 result = df.select(nw.all().fill_nan(3.0)) - if constructor.name in NON_NULLABLE_CONSTRUCTORS: + if any(constructor is c for c in NON_NULLABLE_CONSTRUCTORS): # no nan vs null distinction expected = {"float": [-1.0, 1.0, 3.0], "float_na": [3.0, 1.0, 3.0]} assert result.lazy().collect()["float_na"].null_count() == 0 @@ -54,7 +55,7 @@ def test_fill_nan_series(constructor_eager: ConstructorEager) -> None: "float_na" ] result = s.fill_nan(999) - if constructor_eager.name in NON_NULLABLE_CONSTRUCTORS: + if any(constructor_eager is c for c in NON_NULLABLE_CONSTRUCTORS): # no nan vs null distinction assert_equal_data({"a": result}, {"a": [999.0, 1.0, 999.0]}) elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): diff --git a/tests/expr_and_series/fill_null_test.py b/tests/expr_and_series/fill_null_test.py index a2f61223df..213b695176 100644 --- a/tests/expr_and_series/fill_null_test.py +++ b/tests/expr_and_series/fill_null_test.py @@ -7,10 +7,16 @@ import pytest import narwhals as nw -from tests.utils import DASK_VERSION, DUCKDB_VERSION, POLARS_VERSION, assert_equal_data +from tests.utils import ( + DASK_VERSION, + DUCKDB_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import FillNullStrategy diff --git a/tests/expr_and_series/filter_test.py b/tests/expr_and_series/filter_test.py index 4e420fcb73..5f300ed4b8 100644 --- a/tests/expr_and_series/filter_test.py +++ b/tests/expr_and_series/filter_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data data = { "i": [0, 1, 2, 3, 4], diff --git a/tests/expr_and_series/first_last_test.py b/tests/expr_and_series/first_last_test.py index 19570c8691..0dab322cd1 100644 --- a/tests/expr_and_series/first_last_test.py +++ b/tests/expr_and_series/first_last_test.py @@ -7,12 +7,17 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import DUCKDB_VERSION, POLARS_VERSION, PYARROW_VERSION, assert_equal_data +from tests.utils import ( + DUCKDB_VERSION, + POLARS_VERSION, + PYARROW_VERSION, + Constructor, + assert_equal_data, +) if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import PythonLiteral - + from tests.utils import ConstructorEager data: dict[str, list[PythonLiteral]] = { "a": [8, 2, 1, None], diff --git a/tests/expr_and_series/floor_ceil_test.py b/tests/expr_and_series/floor_ceil_test.py index a8cedb974f..ae0df72712 100644 --- a/tests/expr_and_series/floor_ceil_test.py +++ b/tests/expr_and_series/floor_ceil_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data def test_floor_expr(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/format_test.py b/tests/expr_and_series/format_test.py index c5231bfc68..5bd4ba75a9 100644 --- a/tests/expr_and_series/format_test.py +++ b/tests/expr_and_series/format_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data def test_format(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/horizontal_broadcasts_test.py b/tests/expr_and_series/horizontal_broadcasts_test.py index a689be6a8b..6c6bb8966d 100644 --- a/tests/expr_and_series/horizontal_broadcasts_test.py +++ b/tests/expr_and_series/horizontal_broadcasts_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import DUCKDB_VERSION, Constructor, assert_equal_data def test_sumh_broadcasting(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/is_between_test.py b/tests/expr_and_series/is_between_test.py index 5d97af51fa..6cec08ad64 100644 --- a/tests/expr_and_series/is_between_test.py +++ b/tests/expr_and_series/is_between_test.py @@ -1,16 +1,13 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING, Literal +from typing import Literal import pytest import narwhals as nw from narwhals.exceptions import MultiOutputExpressionError -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data @pytest.mark.parametrize( diff --git a/tests/expr_and_series/is_close_test.py b/tests/expr_and_series/is_close_test.py index e1d8762a18..16c59536ca 100644 --- a/tests/expr_and_series/is_close_test.py +++ b/tests/expr_and_series/is_close_test.py @@ -12,20 +12,29 @@ import narwhals as nw from narwhals.exceptions import ComputeError, InvalidOperationError -from narwhals.testing.constructors import ConstructorName -from tests.utils import PANDAS_VERSION, PYARROW_VERSION, assert_equal_data +from tests.conftest import ( + dask_lazy_p1_constructor, + dask_lazy_p2_constructor, + modin_constructor, + pandas_constructor, +) +from tests.utils import ( + PANDAS_VERSION, + PYARROW_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import NumericLiteral - -NON_NULLABLE_CONSTRUCTORS = { - ConstructorName.MODIN, - ConstructorName.PANDAS, - ConstructorName.DASK, -} - +NON_NULLABLE_CONSTRUCTORS = ( + pandas_constructor, + dask_lazy_p1_constructor, + dask_lazy_p2_constructor, + modin_constructor, +) NULL_PLACEHOLDER, NAN_PLACEHOLDER = 9999.0, -1.0 INF_POS, INF_NEG = float("inf"), float("-inf") @@ -117,7 +126,7 @@ def test_is_close_series_with_series( y = y.zip_with(y != NAN_PLACEHOLDER, y**0.5).zip_with(y != NULL_PLACEHOLDER, nulls) result = x.is_close(y, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal) - if constructor_eager.name in NON_NULLABLE_CONSTRUCTORS: + if constructor_eager in NON_NULLABLE_CONSTRUCTORS: expected = [v if v is not None else nans_equal for v in expected] elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): expected = [ @@ -145,7 +154,7 @@ def test_is_close_series_with_scalar( y = y.zip_with(y != NAN_PLACEHOLDER, y**0.5).zip_with(y != NULL_PLACEHOLDER, nulls) result = y.is_close(other, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal) - if constructor_eager.name in NON_NULLABLE_CONSTRUCTORS: + if constructor_eager in NON_NULLABLE_CONSTRUCTORS: expected = [v if v is not None else False for v in expected] elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): expected = [ @@ -190,7 +199,7 @@ def test_is_close_expr_with_expr( ) .sort("idx") ) - if constructor.name in NON_NULLABLE_CONSTRUCTORS: + if constructor in NON_NULLABLE_CONSTRUCTORS: expected = [v if v is not None else nans_equal for v in expected] elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): expected = [ @@ -231,7 +240,7 @@ def test_is_close_expr_with_scalar( ) .sort("idx") ) - if constructor.name in NON_NULLABLE_CONSTRUCTORS: + if constructor in NON_NULLABLE_CONSTRUCTORS: expected = [v if v is not None else False for v in expected] elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): expected = [ diff --git a/tests/expr_and_series/is_duplicated_test.py b/tests/expr_and_series/is_duplicated_test.py index 405143292f..42f07a91e5 100644 --- a/tests/expr_and_series/is_duplicated_test.py +++ b/tests/expr_and_series/is_duplicated_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data def test_is_duplicated_expr(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/is_finite_test.py b/tests/expr_and_series/is_finite_test.py index 8b46b20ee6..eb07b2a41e 100644 --- a/tests/expr_and_series/is_finite_test.py +++ b/tests/expr_and_series/is_finite_test.py @@ -1,21 +1,24 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from narwhals.testing.constructors import ConstructorName -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager - -NON_NULLABLE_CONSTRUCTORS = { - ConstructorName.MODIN, - ConstructorName.PANDAS, - ConstructorName.DASK, -} +from tests.conftest import ( + dask_lazy_p1_constructor, + dask_lazy_p2_constructor, + modin_constructor, + pandas_constructor, +) +from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data + +NON_NULLABLE_CONSTRUCTORS = [ + pandas_constructor, + dask_lazy_p1_constructor, + dask_lazy_p2_constructor, + modin_constructor, +] data = {"a": [float("nan"), float("inf"), 2.0, None]} @@ -74,7 +77,7 @@ def test_is_finite_column_with_null(constructor: Constructor, data: list[float]) result = df.select(nw.col("a").is_finite()) expected: dict[str, list[Any]] - if constructor.name in NON_NULLABLE_CONSTRUCTORS: + if any(constructor is c for c in NON_NULLABLE_CONSTRUCTORS): # Null values are coerced to NaN for non-nullable datatypes expected = {"a": [True, True, False]} else: diff --git a/tests/expr_and_series/is_first_distinct_test.py b/tests/expr_and_series/is_first_distinct_test.py index 90cfdc97e4..6f0501b03c 100644 --- a/tests/expr_and_series/is_first_distinct_test.py +++ b/tests/expr_and_series/is_first_distinct_test.py @@ -1,14 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) data = {"a": [1, 1, 2, 3, 2], "b": [1, 2, 3, 2, 1]} diff --git a/tests/expr_and_series/is_in_test.py b/tests/expr_and_series/is_in_test.py index aeec14b862..2ae6cabea5 100644 --- a/tests/expr_and_series/is_in_test.py +++ b/tests/expr_and_series/is_in_test.py @@ -1,15 +1,11 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": [1, 4, 2, 5]} diff --git a/tests/expr_and_series/is_last_distinct_test.py b/tests/expr_and_series/is_last_distinct_test.py index cb0860af51..b4a96b5b9f 100644 --- a/tests/expr_and_series/is_last_distinct_test.py +++ b/tests/expr_and_series/is_last_distinct_test.py @@ -1,14 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) data = {"a": [1, 1, 2, 3, 2], "b": [1, 2, 3, 2, 1]} diff --git a/tests/expr_and_series/is_nan_test.py b/tests/expr_and_series/is_nan_test.py index 672c5864e1..27790e27b2 100644 --- a/tests/expr_and_series/is_nan_test.py +++ b/tests/expr_and_series/is_nan_test.py @@ -1,21 +1,24 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from narwhals.testing.constructors import ConstructorName -from tests.utils import PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager - -NON_NULLABLE_CONSTRUCTORS = { - ConstructorName.MODIN, - ConstructorName.PANDAS, - ConstructorName.DASK, -} +from tests.conftest import ( + dask_lazy_p1_constructor, + dask_lazy_p2_constructor, + modin_constructor, + pandas_constructor, +) +from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data + +NON_NULLABLE_CONSTRUCTORS = [ + pandas_constructor, + dask_lazy_p1_constructor, + dask_lazy_p2_constructor, + modin_constructor, +] def test_nan(constructor: Constructor) -> None: @@ -30,7 +33,7 @@ def test_nan(constructor: Constructor) -> None: ) expected: dict[str, list[Any]] - if constructor.name in NON_NULLABLE_CONSTRUCTORS: + if any(constructor is c for c in NON_NULLABLE_CONSTRUCTORS): # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], @@ -67,7 +70,7 @@ def test_nan_series(constructor_eager: ConstructorEager) -> None: "float_na": df["float_na"].is_nan(), } expected: dict[str, list[Any]] - if constructor_eager.name in NON_NULLABLE_CONSTRUCTORS: + if any(constructor_eager is c for c in NON_NULLABLE_CONSTRUCTORS): # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], diff --git a/tests/expr_and_series/is_null_test.py b/tests/expr_and_series/is_null_test.py index 9e51c63201..7542af4513 100644 --- a/tests/expr_and_series/is_null_test.py +++ b/tests/expr_and_series/is_null_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data def test_null(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/is_unique_test.py b/tests/expr_and_series/is_unique_test.py index 9f125d1044..2df0518360 100644 --- a/tests/expr_and_series/is_unique_test.py +++ b/tests/expr_and_series/is_unique_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data def test_is_unique_expr(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/kurtosis_test.py b/tests/expr_and_series/kurtosis_test.py index 3e653e416e..55727445fe 100644 --- a/tests/expr_and_series/kurtosis_test.py +++ b/tests/expr_and_series/kurtosis_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data @pytest.mark.parametrize( diff --git a/tests/expr_and_series/len_test.py b/tests/expr_and_series/len_test.py index 86e1b88068..755c389a28 100644 --- a/tests/expr_and_series/len_test.py +++ b/tests/expr_and_series/len_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data def test_len_no_filter(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/list/contains_test.py b/tests/expr_and_series/list/contains_test.py index 6be2d53a7a..6e56846d6e 100644 --- a/tests/expr_and_series/list/contains_test.py +++ b/tests/expr_and_series/list/contains_test.py @@ -8,7 +8,8 @@ from tests.utils import assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager + data = {"a": [[2, 2, 3, None, None], None, []]} expected = {"a": [True, None, False]} diff --git a/tests/expr_and_series/list/get_test.py b/tests/expr_and_series/list/get_test.py index 55f9071a49..52ca3386ba 100644 --- a/tests/expr_and_series/list/get_test.py +++ b/tests/expr_and_series/list/get_test.py @@ -1,15 +1,12 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data data = {"a": [[1, 2], [None, 3], [None], None]} @@ -48,8 +45,9 @@ def test_get_series( pytest.skip() pytest.importorskip("pyarrow") - if str(constructor_eager).startswith("pandas") and "pyarrow" not in str( - constructor_eager + if ( + constructor_eager.__name__.startswith("pandas") + and "pyarrow" not in constructor_eager.__name__ ): df = nw.from_native(constructor_eager(data), eager_only=True) msg = re.escape("Series must be of PyArrow List type to support list namespace.") diff --git a/tests/expr_and_series/list/len_test.py b/tests/expr_and_series/list/len_test.py index 257db68327..918fda5831 100644 --- a/tests/expr_and_series/list/len_test.py +++ b/tests/expr_and_series/list/len_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data data = {"a": [[1, 2], [3, 4, None], None, [], [None]]} expected = {"a": [2, 3, None, 0, 1]} diff --git a/tests/expr_and_series/list/max_test.py b/tests/expr_and_series/list/max_test.py index baf9e8eaec..646bff2244 100644 --- a/tests/expr_and_series/list/max_test.py +++ b/tests/expr_and_series/list/max_test.py @@ -8,7 +8,7 @@ from tests.utils import PANDAS_VERSION, assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager data = {"a": [[3, None, 2, 2, 4, None], [-1], None, [None, None, None], []]} expected = [4, -1, None, None, None] diff --git a/tests/expr_and_series/list/mean_test.py b/tests/expr_and_series/list/mean_test.py index bb0e8848a3..9175d5b248 100644 --- a/tests/expr_and_series/list/mean_test.py +++ b/tests/expr_and_series/list/mean_test.py @@ -8,7 +8,7 @@ from tests.utils import PANDAS_VERSION, assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager data = {"a": [[3, None, 2, 2, 4, None], [-1], None, [None, None, None], []]} expected = [2.75, -1, None, None, None] diff --git a/tests/expr_and_series/list/median_test.py b/tests/expr_and_series/list/median_test.py index f39e896552..f0d3c615ff 100644 --- a/tests/expr_and_series/list/median_test.py +++ b/tests/expr_and_series/list/median_test.py @@ -9,7 +9,7 @@ from tests.utils import PANDAS_VERSION, POLARS_VERSION, assert_equal_data, is_windows if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager data = {"a": [[3, None, 2, 2, 4, None], [-1], None, [None, None, None], [], [3, 4, None]]} expected = [2.5, -1, None, None, None, 3.5] diff --git a/tests/expr_and_series/list/min_test.py b/tests/expr_and_series/list/min_test.py index 32ad33e381..2039f7de56 100644 --- a/tests/expr_and_series/list/min_test.py +++ b/tests/expr_and_series/list/min_test.py @@ -8,7 +8,7 @@ from tests.utils import PANDAS_VERSION, assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager data = {"a": [[3, None, 2, 2, 4, None], [-1], None, [None, None, None], []]} expected = [2, -1, None, None, None] diff --git a/tests/expr_and_series/list/sort_test.py b/tests/expr_and_series/list/sort_test.py index 872c95e00b..1866934a73 100644 --- a/tests/expr_and_series/list/sort_test.py +++ b/tests/expr_and_series/list/sort_test.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from typing import Any - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager data = {"a": [[3, 2, 2, 4, -10, None, None], [-1], None, [None, None, None], []]} diff --git a/tests/expr_and_series/list/sum_test.py b/tests/expr_and_series/list/sum_test.py index dd8290abd2..d66266e016 100644 --- a/tests/expr_and_series/list/sum_test.py +++ b/tests/expr_and_series/list/sum_test.py @@ -8,7 +8,8 @@ from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager + data = {"a": [[3, None, 2, 2, 4, None], [-1], None, [None, None, None], []]} expected = [11, -1, None, 0, 0] diff --git a/tests/expr_and_series/list/unique_test.py b/tests/expr_and_series/list/unique_test.py index 809e1ed82c..3d7c9dd039 100644 --- a/tests/expr_and_series/list/unique_test.py +++ b/tests/expr_and_series/list/unique_test.py @@ -8,7 +8,7 @@ from tests.utils import DUCKDB_VERSION if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager data = {"a": [[2, 2, 3, None, None], None, [], [None]]} expected = {2, 3, None} diff --git a/tests/expr_and_series/lit_test.py b/tests/expr_and_series/lit_test.py index d3187b2d2b..188292c0d1 100644 --- a/tests/expr_and_series/lit_test.py +++ b/tests/expr_and_series/lit_test.py @@ -12,12 +12,12 @@ PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, + Constructor, assert_equal_data, ) if TYPE_CHECKING: from narwhals.dtypes import DType - from narwhals.testing.typing import Constructor from narwhals.typing import IntoDType, PythonLiteral diff --git a/tests/expr_and_series/log_test.py b/tests/expr_and_series/log_test.py index dc3372c443..4157b5a4a6 100644 --- a/tests/expr_and_series/log_test.py +++ b/tests/expr_and_series/log_test.py @@ -1,15 +1,11 @@ from __future__ import annotations import math -from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data data = {"a": [-1, 0, 1, 2, 4]} diff --git a/tests/expr_and_series/map_batches_test.py b/tests/expr_and_series/map_batches_test.py index bac083b1f2..d6f8cc8b32 100644 --- a/tests/expr_and_series/map_batches_test.py +++ b/tests/expr_and_series/map_batches_test.py @@ -7,12 +7,15 @@ pytest.importorskip("numpy") import narwhals as nw -from tests.utils import PANDAS_VERSION, POLARS_VERSION, assert_equal_data +from tests.utils import ( + PANDAS_VERSION, + POLARS_VERSION, + ConstructorEager, + assert_equal_data, +) if TYPE_CHECKING: from narwhals.dtypes import DType - from narwhals.testing.typing import ConstructorEager - data = {"a": [1, 2, 3], "b": [4, 5, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/expr_and_series/max_horizontal_test.py b/tests/expr_and_series/max_horizontal_test.py index 788d176b18..cc0bddfb1a 100644 --- a/tests/expr_and_series/max_horizontal_test.py +++ b/tests/expr_and_series/max_horizontal_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data data = {"a": [1, 3, None, None], "b": [4, None, 6, None], "z": [3, 1, None, None]} expected_values = [4, 3, 6, None] diff --git a/tests/expr_and_series/max_test.py b/tests/expr_and_series/max_test.py index 47906bfc19..eb359121c6 100644 --- a/tests/expr_and_series/max_test.py +++ b/tests/expr_and_series/max_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/expr_and_series/mean_horizontal_test.py b/tests/expr_and_series/mean_horizontal_test.py index 7754d3b854..bc5bc12fa6 100644 --- a/tests/expr_and_series/mean_horizontal_test.py +++ b/tests/expr_and_series/mean_horizontal_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data def test_meanh(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/mean_test.py b/tests/expr_and_series/mean_test.py index b2983a0b51..39a97bd53a 100644 --- a/tests/expr_and_series/mean_test.py +++ b/tests/expr_and_series/mean_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": [1, 3, 2], "b": [4, 4, 7], "z": [7.0, 8.0, 9.0]} diff --git a/tests/expr_and_series/median_test.py b/tests/expr_and_series/median_test.py index b485bc6002..b01350586a 100644 --- a/tests/expr_and_series/median_test.py +++ b/tests/expr_and_series/median_test.py @@ -1,16 +1,12 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING import pytest import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = { "a": [3, 8, 2, None], diff --git a/tests/expr_and_series/min_horizontal_test.py b/tests/expr_and_series/min_horizontal_test.py index 575abf2d27..df9ff31feb 100644 --- a/tests/expr_and_series/min_horizontal_test.py +++ b/tests/expr_and_series/min_horizontal_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data data = {"a": [1, 3, None, None], "b": [4, None, 6, None], "z": [3, 1, None, None]} expected_values = [1, 1, 6, None] diff --git a/tests/expr_and_series/min_test.py b/tests/expr_and_series/min_test.py index f166baf165..842e70c166 100644 --- a/tests/expr_and_series/min_test.py +++ b/tests/expr_and_series/min_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/expr_and_series/mode_test.py b/tests/expr_and_series/mode_test.py index c50d5d3bf7..a17de7bd17 100644 --- a/tests/expr_and_series/mode_test.py +++ b/tests/expr_and_series/mode_test.py @@ -2,16 +2,13 @@ import re from contextlib import nullcontext as does_not_raise -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw from narwhals.exceptions import ShapeError -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data data = {"a": [1, 1, 2, 2, 3], "b": [1, 2, 3, 3, 4]} data_group = { diff --git a/tests/expr_and_series/n_unique_test.py b/tests/expr_and_series/n_unique_test.py index 206777ccf0..e9af1f0154 100644 --- a/tests/expr_and_series/n_unique_test.py +++ b/tests/expr_and_series/n_unique_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data data = {"a": [1.0, None, None, 3.0], "b": [1.0, None, 4.0, 5.0]} diff --git a/tests/expr_and_series/name/keep_test.py b/tests/expr_and_series/name/keep_test.py index 40b7e38f9b..e4a3ba17f6 100644 --- a/tests/expr_and_series/name/keep_test.py +++ b/tests/expr_and_series/name/keep_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} diff --git a/tests/expr_and_series/name/map_test.py b/tests/expr_and_series/name/map_test.py index 22f8609733..eb4b2bc22b 100644 --- a/tests/expr_and_series/name/map_test.py +++ b/tests/expr_and_series/name/map_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import POLARS_VERSION, Constructor, assert_equal_data data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} diff --git a/tests/expr_and_series/name/prefix_test.py b/tests/expr_and_series/name/prefix_test.py index f4447f5cde..a639455823 100644 --- a/tests/expr_and_series/name/prefix_test.py +++ b/tests/expr_and_series/name/prefix_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import POLARS_VERSION, Constructor, assert_equal_data data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} prefix = "with_prefix_" diff --git a/tests/expr_and_series/name/suffix_test.py b/tests/expr_and_series/name/suffix_test.py index 780dfd4c16..437d93e05a 100644 --- a/tests/expr_and_series/name/suffix_test.py +++ b/tests/expr_and_series/name/suffix_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import POLARS_VERSION, Constructor, assert_equal_data data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} suffix = "_with_suffix" diff --git a/tests/expr_and_series/name/to_lowercase_test.py b/tests/expr_and_series/name/to_lowercase_test.py index 43170dc311..41a88a4f9c 100644 --- a/tests/expr_and_series/name/to_lowercase_test.py +++ b/tests/expr_and_series/name/to_lowercase_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import POLARS_VERSION, Constructor, assert_equal_data data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} diff --git a/tests/expr_and_series/name/to_uppercase_test.py b/tests/expr_and_series/name/to_uppercase_test.py index 1080cff82d..9204ac86b5 100644 --- a/tests/expr_and_series/name/to_uppercase_test.py +++ b/tests/expr_and_series/name/to_uppercase_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import POLARS_VERSION, Constructor, assert_equal_data data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} diff --git a/tests/expr_and_series/nth_test.py b/tests/expr_and_series/nth_test.py index d3e035faff..1249f7f2e2 100644 --- a/tests/expr_and_series/nth_test.py +++ b/tests/expr_and_series/nth_test.py @@ -6,12 +6,12 @@ import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, assert_equal_data +from tests.utils import POLARS_VERSION, Constructor, assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import Constructor + from collections.abc import Mapping -data: dict[str, Any] = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8.0, 9.0]} +data: Mapping[str, Any] = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8.0, 9.0]} @pytest.mark.parametrize( diff --git a/tests/expr_and_series/null_count_test.py b/tests/expr_and_series/null_count_test.py index 0c7e6b511a..6efef0ada1 100644 --- a/tests/expr_and_series/null_count_test.py +++ b/tests/expr_and_series/null_count_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": [1.0, None, None, 3.0], "b": [1.0, None, 4.0, 5.0]} diff --git a/tests/expr_and_series/operators_test.py b/tests/expr_and_series/operators_test.py index add29a19c3..f505c8f972 100644 --- a/tests/expr_and_series/operators_test.py +++ b/tests/expr_and_series/operators_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DASK_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import DASK_VERSION, Constructor, ConstructorEager, assert_equal_data @pytest.mark.parametrize( diff --git a/tests/expr_and_series/order_dependent_lazy_test.py b/tests/expr_and_series/order_dependent_lazy_test.py index b88df38b53..70bd234828 100644 --- a/tests/expr_and_series/order_dependent_lazy_test.py +++ b/tests/expr_and_series/order_dependent_lazy_test.py @@ -8,7 +8,7 @@ from narwhals.exceptions import InvalidOperationError if TYPE_CHECKING: - from narwhals.testing.typing import Constructor + from tests.utils import Constructor def test_order_dependent_raises_in_lazy(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/over_pushdown_test.py b/tests/expr_and_series/over_pushdown_test.py index 8dfceedf6a..8a8c9795d0 100644 --- a/tests/expr_and_series/over_pushdown_test.py +++ b/tests/expr_and_series/over_pushdown_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import DUCKDB_VERSION, Constructor, assert_equal_data def test_over_pushdown(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/over_test.py b/tests/expr_and_series/over_test.py index 3273bb1dfc..7fbb8f0f9f 100644 --- a/tests/expr_and_series/over_test.py +++ b/tests/expr_and_series/over_test.py @@ -1,16 +1,19 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise -from typing import TYPE_CHECKING import pytest import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + PANDAS_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) data = { "a": ["a", "a", "b", "b", "b"], diff --git a/tests/expr_and_series/pipe_test.py b/tests/expr_and_series/pipe_test.py index 1bd7e8ca39..d32743e1ea 100644 --- a/tests/expr_and_series/pipe_test.py +++ b/tests/expr_and_series/pipe_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data input_list = {"a": [2, 4, 6, 8]} expected = [4, 16, 36, 64] diff --git a/tests/expr_and_series/quantile_test.py b/tests/expr_and_series/quantile_test.py index fd0a2a8f89..39489db89d 100644 --- a/tests/expr_and_series/quantile_test.py +++ b/tests/expr_and_series/quantile_test.py @@ -2,15 +2,12 @@ import re from contextlib import nullcontext as does_not_raise -from typing import TYPE_CHECKING, Literal +from typing import Literal import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data @pytest.mark.parametrize( @@ -46,7 +43,7 @@ def test_quantile_expr( ) context = ( pytest.raises(NotImplementedError, match=msg) - if "dask_lazy" in str(constructor) + if "dask_lazy_p2" in str(constructor) else does_not_raise() ) diff --git a/tests/expr_and_series/rank_test.py b/tests/expr_and_series/rank_test.py index 9fc1d0dd0e..fb50fdeb59 100644 --- a/tests/expr_and_series/rank_test.py +++ b/tests/expr_and_series/rank_test.py @@ -1,7 +1,7 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise -from typing import TYPE_CHECKING, Literal +from typing import Literal import pytest @@ -10,13 +10,12 @@ DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, + Constructor, + ConstructorEager, assert_equal_data, is_windows, ) -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager - rank_methods = ["average", "min", "max", "dense", "ordinal"] data_int = {"a": [3, 6, 1, 1, None, 6], "b": [1, 1, 2, 1, 2, 2], "i": [1, 2, 3, 4, 5, 6]} diff --git a/tests/expr_and_series/reduction_test.py b/tests/expr_and_series/reduction_test.py index 5ead8edda9..cba75767ea 100644 --- a/tests/expr_and_series/reduction_test.py +++ b/tests/expr_and_series/reduction_test.py @@ -1,15 +1,12 @@ from __future__ import annotations from itertools import chain -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data @pytest.mark.parametrize( diff --git a/tests/expr_and_series/replace_strict_test.py b/tests/expr_and_series/replace_strict_test.py index 96c29f49dc..e0fbcecd06 100644 --- a/tests/expr_and_series/replace_strict_test.py +++ b/tests/expr_and_series/replace_strict_test.py @@ -6,14 +6,18 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import POLARS_VERSION, assert_equal_data, xfail_if_pyspark_connect +from tests.utils import ( + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, + xfail_if_pyspark_connect, +) if TYPE_CHECKING: from collections.abc import Mapping, Sequence from narwhals.dtypes import DType - from narwhals.testing.typing import Constructor, ConstructorEager - polars_lt_v1 = POLARS_VERSION < (1, 0, 0) skip_reason = "replace_strict only available after 1.0" diff --git a/tests/expr_and_series/rolling_mean_test.py b/tests/expr_and_series/rolling_mean_test.py index c9768680c1..b4c0b9e656 100644 --- a/tests/expr_and_series/rolling_mean_test.py +++ b/tests/expr_and_series/rolling_mean_test.py @@ -1,17 +1,20 @@ from __future__ import annotations import random -from typing import TYPE_CHECKING, Any +from typing import Any import hypothesis.strategies as st import pytest from hypothesis import given import narwhals as nw -from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) data = {"a": [None, 1, 2, None, 4, 6, 11]} diff --git a/tests/expr_and_series/rolling_std_test.py b/tests/expr_and_series/rolling_std_test.py index 4daf1adbde..f027b450d9 100644 --- a/tests/expr_and_series/rolling_std_test.py +++ b/tests/expr_and_series/rolling_std_test.py @@ -1,15 +1,19 @@ from __future__ import annotations from math import sqrt -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + PANDAS_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) data = {"a": [1.0, 2.0, 1.0, 3.0, 1.0, 4.0, 1.0]} diff --git a/tests/expr_and_series/rolling_sum_test.py b/tests/expr_and_series/rolling_sum_test.py index fae3d9ebff..df7b481826 100644 --- a/tests/expr_and_series/rolling_sum_test.py +++ b/tests/expr_and_series/rolling_sum_test.py @@ -1,7 +1,7 @@ from __future__ import annotations import random -from typing import TYPE_CHECKING, Any +from typing import Any import hypothesis.strategies as st import pytest @@ -9,10 +9,14 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + PANDAS_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) data = {"a": [None, 1, 2, None, 4, 6, 11]} diff --git a/tests/expr_and_series/rolling_var_test.py b/tests/expr_and_series/rolling_var_test.py index 84c97b6fa8..b38ba5f077 100644 --- a/tests/expr_and_series/rolling_var_test.py +++ b/tests/expr_and_series/rolling_var_test.py @@ -1,21 +1,25 @@ from __future__ import annotations import random -from typing import TYPE_CHECKING, Any +from typing import Any import hypothesis.strategies as st import pytest from hypothesis import HealthCheck, given, settings import narwhals as nw -from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data +from tests.utils import ( + DUCKDB_VERSION, + PANDAS_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) pytest.importorskip("pandas") import pandas as pd -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager - data = {"a": [1.0, 2.0, 1.0, 3.0, 1.0, 4.0, 1.0]} kwargs_and_expected = ( diff --git a/tests/expr_and_series/round_test.py b/tests/expr_and_series/round_test.py index 061a3e0765..0709ad28b7 100644 --- a/tests/expr_and_series/round_test.py +++ b/tests/expr_and_series/round_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data @pytest.mark.parametrize("decimals", [0, 1, 2]) diff --git a/tests/expr_and_series/sample_test.py b/tests/expr_and_series/sample_test.py index 23ad3bac30..4ec44a8712 100644 --- a/tests/expr_and_series/sample_test.py +++ b/tests/expr_and_series/sample_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data def test_sample_fraction(constructor_eager: ConstructorEager) -> None: diff --git a/tests/expr_and_series/shift_test.py b/tests/expr_and_series/shift_test.py index 26f134b3a3..a06ff4a872 100644 --- a/tests/expr_and_series/shift_test.py +++ b/tests/expr_and_series/shift_test.py @@ -1,15 +1,18 @@ from __future__ import annotations from contextlib import nullcontext -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) data = { "i": [0, 1, 2, 3, 4], diff --git a/tests/expr_and_series/sin_test.py b/tests/expr_and_series/sin_test.py index 13ac55794b..b1fed2f501 100644 --- a/tests/expr_and_series/sin_test.py +++ b/tests/expr_and_series/sin_test.py @@ -6,10 +6,15 @@ import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, PYARROW_VERSION, assert_equal_data +from tests.utils import ( + PANDAS_VERSION, + PYARROW_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import DTypeBackend data = {"a": [-pi, -pi / 2, 0.0, pi / 2, pi]} diff --git a/tests/expr_and_series/skew_test.py b/tests/expr_and_series/skew_test.py index 7788d2be94..9be2056dac 100644 --- a/tests/expr_and_series/skew_test.py +++ b/tests/expr_and_series/skew_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data @pytest.mark.parametrize( diff --git a/tests/expr_and_series/sqrt_test.py b/tests/expr_and_series/sqrt_test.py index a890ea7491..057401d594 100644 --- a/tests/expr_and_series/sqrt_test.py +++ b/tests/expr_and_series/sqrt_test.py @@ -5,10 +5,15 @@ import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, PYARROW_VERSION, assert_equal_data +from tests.utils import ( + PANDAS_VERSION, + PYARROW_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import DTypeBackend data = {"a": [-1, 0, 1, 2, 4]} diff --git a/tests/expr_and_series/std_test.py b/tests/expr_and_series/std_test.py index f5dbbfbe5b..d2b3cb14ef 100644 --- a/tests/expr_and_series/std_test.py +++ b/tests/expr_and_series/std_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} data_with_nulls = {"a": [1, 3, 2, None], "b": [4, 4, 6, None], "z": [7.0, 8.0, 9.0, None]} diff --git a/tests/expr_and_series/str/contains_test.py b/tests/expr_and_series/str/contains_test.py index 737e3e92fb..7935b6c92f 100644 --- a/tests/expr_and_series/str/contains_test.py +++ b/tests/expr_and_series/str/contains_test.py @@ -1,14 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data data = {"pets": ["cat", "dog", "rabbit and parrot", "dove", "Parrot|dove", None]} """Test data for string literal pattern tests""" diff --git a/tests/expr_and_series/str/head_test.py b/tests/expr_and_series/str/head_test.py index 960229de51..d6d09b978e 100644 --- a/tests/expr_and_series/str/head_test.py +++ b/tests/expr_and_series/str/head_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": ["foo", "bars"]} diff --git a/tests/expr_and_series/str/len_chars_test.py b/tests/expr_and_series/str/len_chars_test.py index 4de51443fb..32a726ab26 100644 --- a/tests/expr_and_series/str/len_chars_test.py +++ b/tests/expr_and_series/str/len_chars_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": ["foo", "foobar", "Café", "345", "東京"]} diff --git a/tests/expr_and_series/str/pad_test.py b/tests/expr_and_series/str/pad_test.py index f51ecf4d28..763fd41043 100644 --- a/tests/expr_and_series/str/pad_test.py +++ b/tests/expr_and_series/str/pad_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data def test_str_pad_start_series(constructor_eager: ConstructorEager) -> None: diff --git a/tests/expr_and_series/str/replace_test.py b/tests/expr_and_series/str/replace_test.py index 0be107110d..e6f49f409b 100644 --- a/tests/expr_and_series/str/replace_test.py +++ b/tests/expr_and_series/str/replace_test.py @@ -1,15 +1,12 @@ from __future__ import annotations from contextlib import nullcontext -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data replace_data = [ ({"a": ["123abc", "abc456"]}, r"abc\b", "ABC", 1, False, {"a": ["123ABC", "abc456"]}), diff --git a/tests/expr_and_series/str/slice_test.py b/tests/expr_and_series/str/slice_test.py index 8f30717f9a..87486e6927 100644 --- a/tests/expr_and_series/str/slice_test.py +++ b/tests/expr_and_series/str/slice_test.py @@ -1,14 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": ["fdas", "edfas"]} diff --git a/tests/expr_and_series/str/split_test.py b/tests/expr_and_series/str/split_test.py index 2f4ce5fdf1..b6b25cd024 100644 --- a/tests/expr_and_series/str/split_test.py +++ b/tests/expr_and_series/str/split_test.py @@ -1,15 +1,12 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"s": ["foo bar", "foo_bar", "foo_bar_baz", "foo,bar"]} @@ -23,7 +20,8 @@ ) def test_str_split(constructor: Constructor, by: str, expected: Any) -> None: if "cudf" not in str(constructor) and ( - str(constructor).startswith("pandas") and "pyarrow" not in str(constructor) + constructor.__name__.startswith("pandas") + and "pyarrow" not in constructor.__name__ ): df = nw.from_native(constructor(data)) msg = re.escape("This operation requires a pyarrow-backed series. ") @@ -46,8 +44,8 @@ def test_str_split_series( constructor_eager: ConstructorEager, by: str, expected: Any ) -> None: if "cudf" not in str(constructor_eager) and ( - str(constructor_eager).startswith("pandas") - and "pyarrow" not in str(constructor_eager) + constructor_eager.__name__.startswith("pandas") + and "pyarrow" not in constructor_eager.__name__ ): df = nw.from_native(constructor_eager(data), eager_only=True) msg = re.escape("This operation requires a pyarrow-backed series. ") diff --git a/tests/expr_and_series/str/starts_with_ends_with_test.py b/tests/expr_and_series/str/starts_with_ends_with_test.py index e3d159a67a..73f60a355d 100644 --- a/tests/expr_and_series/str/starts_with_ends_with_test.py +++ b/tests/expr_and_series/str/starts_with_ends_with_test.py @@ -1,12 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +# Don't move this into typechecking block, for coverage +# purposes +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": ["fdas", "edfas"]} diff --git a/tests/expr_and_series/str/strip_chars_test.py b/tests/expr_and_series/str/strip_chars_test.py index 1736dba8b7..8944bce4de 100644 --- a/tests/expr_and_series/str/strip_chars_test.py +++ b/tests/expr_and_series/str/strip_chars_test.py @@ -1,14 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": ["foobar", "bar\n", " baz"]} diff --git a/tests/expr_and_series/str/tail_test.py b/tests/expr_and_series/str/tail_test.py index a7bad4d13b..8aa9d66c02 100644 --- a/tests/expr_and_series/str/tail_test.py +++ b/tests/expr_and_series/str/tail_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": ["foo", "bars"]} diff --git a/tests/expr_and_series/str/to_date_test.py b/tests/expr_and_series/str/to_date_test.py index 2d073564b9..d1f7558c9f 100644 --- a/tests/expr_and_series/str/to_date_test.py +++ b/tests/expr_and_series/str/to_date_test.py @@ -9,7 +9,7 @@ from tests.utils import assert_equal_data, uses_pyarrow_backend if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager data = {"a": ["2020-01-01", "2020-01-02", None]} expected = {"a": [date(2020, 1, 1), date(2020, 1, 2), None]} diff --git a/tests/expr_and_series/str/to_datetime_test.py b/tests/expr_and_series/str/to_datetime_test.py index c6f8d1a838..b98755a0cc 100644 --- a/tests/expr_and_series/str/to_datetime_test.py +++ b/tests/expr_and_series/str/to_datetime_test.py @@ -18,7 +18,7 @@ ) if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager data = {"a": ["2020-01-01T12:34:56"]} diff --git a/tests/expr_and_series/str/to_titlecase_test.py b/tests/expr_and_series/str/to_titlecase_test.py index 1e0a09bc79..ae61c3ecd2 100644 --- a/tests/expr_and_series/str/to_titlecase_test.py +++ b/tests/expr_and_series/str/to_titlecase_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data data = { "a": [ diff --git a/tests/expr_and_series/str/to_uppercase_to_lowercase_test.py b/tests/expr_and_series/str/to_uppercase_to_lowercase_test.py index c3b2237ea4..7c5c39a481 100644 --- a/tests/expr_and_series/str/to_uppercase_to_lowercase_test.py +++ b/tests/expr_and_series/str/to_uppercase_to_lowercase_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data @pytest.mark.parametrize( diff --git a/tests/expr_and_series/str/zfill_test.py b/tests/expr_and_series/str/zfill_test.py index 86293ecdfb..78f59cecb1 100644 --- a/tests/expr_and_series/str/zfill_test.py +++ b/tests/expr_and_series/str/zfill_test.py @@ -1,20 +1,17 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw from tests.utils import ( PANDAS_VERSION, POLARS_VERSION, + Constructor, + ConstructorEager, assert_equal_data, uses_pyarrow_backend, ) -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager - data = {"a": ["-1", "+1", "1", "12", "123", "99999", "+9999", None]} expected = {"a": ["-01", "+01", "001", "012", "123", "99999", "+9999", None]} diff --git a/tests/expr_and_series/struct_/field_test.py b/tests/expr_and_series/struct_/field_test.py index 2f50ff35af..a351c31500 100644 --- a/tests/expr_and_series/struct_/field_test.py +++ b/tests/expr_and_series/struct_/field_test.py @@ -1,14 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, cast +from typing import cast import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data def test_get_field_expr(request: pytest.FixtureRequest, constructor: Constructor) -> None: diff --git a/tests/expr_and_series/struct_test.py b/tests/expr_and_series/struct_test.py index 80c2dfa65a..b7f1eae1ca 100644 --- a/tests/expr_and_series/struct_test.py +++ b/tests/expr_and_series/struct_test.py @@ -1,14 +1,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, PYARROW_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DUCKDB_VERSION, + PANDAS_VERSION, + PYARROW_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) data = {"a": [1, 2, 3], "b": ["dogs", "cats", None], "c": ["play", "swim", "walk"]} diff --git a/tests/expr_and_series/sum_horizontal_test.py b/tests/expr_and_series/sum_horizontal_test.py index a80f6e0367..94cc32d4b0 100644 --- a/tests/expr_and_series/sum_horizontal_test.py +++ b/tests/expr_and_series/sum_horizontal_test.py @@ -1,14 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import DUCKDB_VERSION, Constructor, assert_equal_data def test_sumh(constructor: Constructor) -> None: diff --git a/tests/expr_and_series/sum_test.py b/tests/expr_and_series/sum_test.py index 00da2e1620..6501869a49 100644 --- a/tests/expr_and_series/sum_test.py +++ b/tests/expr_and_series/sum_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/expr_and_series/unary_test.py b/tests/expr_and_series/unary_test.py index b2c1e59332..038136a902 100644 --- a/tests/expr_and_series/unary_test.py +++ b/tests/expr_and_series/unary_test.py @@ -1,15 +1,11 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise -from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data def test_unary(constructor: Constructor, request: pytest.FixtureRequest) -> None: diff --git a/tests/expr_and_series/unique_test.py b/tests/expr_and_series/unique_test.py index 3c7bc7d20d..c54a0356c7 100644 --- a/tests/expr_and_series/unique_test.py +++ b/tests/expr_and_series/unique_test.py @@ -1,16 +1,12 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise -from typing import TYPE_CHECKING import pytest import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": [1, 1, None, 2]} data_str = {"a": ["x", "x", "y", None]} diff --git a/tests/expr_and_series/var_test.py b/tests/expr_and_series/var_test.py index c33b7f6917..f927dd1ac0 100644 --- a/tests/expr_and_series/var_test.py +++ b/tests/expr_and_series/var_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} data_with_nulls = {"a": [1, 3, 2, None], "b": [4, 4, 6, None], "z": [7.0, 8.0, 9.0, None]} diff --git a/tests/expr_and_series/when_test.py b/tests/expr_and_series/when_test.py index b0deb0714a..aefac8492c 100644 --- a/tests/expr_and_series/when_test.py +++ b/tests/expr_and_series/when_test.py @@ -6,10 +6,15 @@ import narwhals as nw from narwhals.exceptions import MultiOutputExpressionError -from tests.utils import DUCKDB_VERSION, assert_equal_data, uses_pyarrow_backend +from tests.utils import ( + DUCKDB_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, + uses_pyarrow_backend, +) if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import _1DArray data = { diff --git a/tests/expression_parsing_test.py b/tests/expression_parsing_test.py index 17d3b80104..138063eb06 100644 --- a/tests/expression_parsing_test.py +++ b/tests/expression_parsing_test.py @@ -1,15 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw from narwhals.exceptions import InvalidOperationError -from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import DUCKDB_VERSION, POLARS_VERSION, Constructor, assert_equal_data @pytest.mark.parametrize( diff --git a/tests/frame/add_test.py b/tests/frame/add_test.py index 082bb0076d..166be83fd6 100644 --- a/tests/frame/add_test.py +++ b/tests/frame/add_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import DUCKDB_VERSION, Constructor, assert_equal_data def test_add(constructor: Constructor) -> None: diff --git a/tests/frame/array_dunder_test.py b/tests/frame/array_dunder_test.py index f0ec7b90c8..d5db653c09 100644 --- a/tests/frame/array_dunder_test.py +++ b/tests/frame/array_dunder_test.py @@ -3,15 +3,16 @@ import pytest pytest.importorskip("numpy") -from typing import TYPE_CHECKING - import numpy as np import narwhals as nw -from tests.utils import PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ( + PANDAS_VERSION, + POLARS_VERSION, + PYARROW_VERSION, + ConstructorEager, + assert_equal_data, +) def test_array_dunder( diff --git a/tests/frame/clone_test.py b/tests/frame/clone_test.py index 06a56bff64..66b9771835 100644 --- a/tests/frame/clone_test.py +++ b/tests/frame/clone_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data def test_clone(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/collect_test.py b/tests/frame/collect_test.py index 09093cbeae..66ea38e979 100644 --- a/tests/frame/collect_test.py +++ b/tests/frame/collect_test.py @@ -7,11 +7,10 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.dependencies import get_cudf, get_modin, get_polars -from tests.utils import POLARS_VERSION, assert_equal_data +from tests.utils import POLARS_VERSION, Constructor, assert_equal_data if TYPE_CHECKING: from narwhals._typing import Arrow, Dask, IntoBackend, Modin, Pandas, Polars - from narwhals.testing.typing import Constructor data = {"a": [1, 2], "b": [3, 4]} diff --git a/tests/frame/columns_test.py b/tests/frame/columns_test.py index beea95c36f..3fda986219 100644 --- a/tests/frame/columns_test.py +++ b/tests/frame/columns_test.py @@ -8,7 +8,7 @@ from tests.utils import assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/frame/concat_test.py b/tests/frame/concat_test.py index 5a5f51cab4..00beb2ebc1 100644 --- a/tests/frame/concat_test.py +++ b/tests/frame/concat_test.py @@ -9,13 +9,11 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.exceptions import InvalidOperationError, NarwhalsError -from tests.utils import POLARS_VERSION, assert_equal_data +from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data if TYPE_CHECKING: from collections.abc import Iterator - from narwhals.testing.typing import Constructor, ConstructorEager - def test_concat_horizontal(constructor_eager: ConstructorEager) -> None: data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/frame/double_test.py b/tests/frame/double_test.py index ebcbb1fc77..6a8f5d6333 100644 --- a/tests/frame/double_test.py +++ b/tests/frame/double_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data def test_double(constructor: Constructor) -> None: diff --git a/tests/frame/drop_nulls_test.py b/tests/frame/drop_nulls_test.py index 561abadd9b..34d433979d 100644 --- a/tests/frame/drop_nulls_test.py +++ b/tests/frame/drop_nulls_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data data = {"alpha": [1.0, 2.0, None, 4.0], "beta gamma": [None, 3.0, None, 5.0]} diff --git a/tests/frame/drop_test.py b/tests/frame/drop_test.py index 22887c0ebc..233ab3003b 100644 --- a/tests/frame/drop_test.py +++ b/tests/frame/drop_test.py @@ -10,7 +10,7 @@ from tests.utils import POLARS_VERSION if TYPE_CHECKING: - from narwhals.testing.typing import Constructor + from tests.utils import Constructor @pytest.mark.parametrize( diff --git a/tests/frame/eq_test.py b/tests/frame/eq_test.py index fbe4786961..c100469451 100644 --- a/tests/frame/eq_test.py +++ b/tests/frame/eq_test.py @@ -7,7 +7,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import Constructor + from tests.utils import Constructor def test_eq_neq_raise(constructor: Constructor) -> None: diff --git a/tests/frame/estimated_size_test.py b/tests/frame/estimated_size_test.py index cef3c60216..78c1cc86f7 100644 --- a/tests/frame/estimated_size_test.py +++ b/tests/frame/estimated_size_test.py @@ -8,7 +8,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager data = {"a": list(range(100))} diff --git a/tests/frame/explode_test.py b/tests/frame/explode_test.py index 269ab4056f..596eaa82a9 100644 --- a/tests/frame/explode_test.py +++ b/tests/frame/explode_test.py @@ -6,13 +6,11 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError, ShapeError -from tests.utils import PANDAS_VERSION, POLARS_VERSION, assert_equal_data +from tests.utils import PANDAS_VERSION, POLARS_VERSION, Constructor, assert_equal_data if TYPE_CHECKING: from collections.abc import Sequence - from narwhals.testing.typing import Constructor - # For context, polars allows to explode multiple columns only if the columns # have matching element counts, therefore, l1 and l2 but not l1 and l3 together. data = { diff --git a/tests/frame/filter_test.py b/tests/frame/filter_test.py index f34adeb54e..f8048d8e4c 100644 --- a/tests/frame/filter_test.py +++ b/tests/frame/filter_test.py @@ -1,15 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw from narwhals.exceptions import ColumnNotFoundError, InvalidOperationError -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import Constructor, ConstructorEager, assert_equal_data data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/frame/from_dict_test.py b/tests/frame/from_dict_test.py index 280bee9d2e..4378d9820f 100644 --- a/tests/frame/from_dict_test.py +++ b/tests/frame/from_dict_test.py @@ -6,11 +6,10 @@ import narwhals as nw from narwhals._utils import Implementation -from tests.utils import assert_equal_data +from tests.utils import Constructor, assert_equal_data if TYPE_CHECKING: from narwhals._typing import EagerAllowed, Polars - from narwhals.testing.typing import Constructor def test_from_dict(eager_backend: EagerAllowed) -> None: diff --git a/tests/frame/get_column_test.py b/tests/frame/get_column_test.py index 3ec34968e5..254dceffc8 100644 --- a/tests/frame/get_column_test.py +++ b/tests/frame/get_column_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data def test_get_column(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/getitem_test.py b/tests/frame/getitem_test.py index 0b6f4c4fa4..c079d154bb 100644 --- a/tests/frame/getitem_test.py +++ b/tests/frame/getitem_test.py @@ -6,10 +6,9 @@ import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, assert_equal_data +from tests.utils import PANDAS_VERSION, ConstructorEager, assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager from narwhals.typing import _1DArray data: dict[str, Any] = { diff --git a/tests/frame/group_by_test.py b/tests/frame/group_by_test.py index 7690be5124..788a5363a4 100644 --- a/tests/frame/group_by_test.py +++ b/tests/frame/group_by_test.py @@ -15,13 +15,14 @@ PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, + Constructor, + ConstructorEager, assert_equal_data, ) if TYPE_CHECKING: from collections.abc import Mapping, Sequence - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import NonNestedLiteral diff --git a/tests/frame/head_test.py b/tests/frame/head_test.py index 9d4ba41351..1c9aaf4123 100644 --- a/tests/frame/head_test.py +++ b/tests/frame/head_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data def test_head(constructor: Constructor) -> None: diff --git a/tests/frame/invalid_test.py b/tests/frame/invalid_test.py index 2c0661d4cf..d59b67adb1 100644 --- a/tests/frame/invalid_test.py +++ b/tests/frame/invalid_test.py @@ -6,10 +6,7 @@ import narwhals as nw from narwhals.exceptions import MultiOutputExpressionError -from tests.utils import NUMPY_VERSION, POLARS_VERSION - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import NUMPY_VERSION, POLARS_VERSION, Constructor T = TypeVar("T") diff --git a/tests/frame/is_duplicated_test.py b/tests/frame/is_duplicated_test.py index 0aaf09bdfc..0703d2bd6c 100644 --- a/tests/frame/is_duplicated_test.py +++ b/tests/frame/is_duplicated_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data def test_is_duplicated(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/is_empty_test.py b/tests/frame/is_empty_test.py index 7f4125275f..5464d0e678 100644 --- a/tests/frame/is_empty_test.py +++ b/tests/frame/is_empty_test.py @@ -7,7 +7,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager @pytest.mark.parametrize(("threshold", "expected"), [(0, False), (10, True)]) diff --git a/tests/frame/is_unique_test.py b/tests/frame/is_unique_test.py index d2769bc064..10e9af910d 100644 --- a/tests/frame/is_unique_test.py +++ b/tests/frame/is_unique_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data def test_is_unique(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/item_test.py b/tests/frame/item_test.py index 92b904f856..997b91d847 100644 --- a/tests/frame/item_test.py +++ b/tests/frame/item_test.py @@ -1,15 +1,12 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data @pytest.mark.parametrize(("row", "column", "expected"), [(0, 2, 7), (1, "z", 8)]) diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py index bf06a62cab..42d52adafc 100644 --- a/tests/frame/join_test.py +++ b/tests/frame/join_test.py @@ -7,10 +7,15 @@ import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data +from tests.utils import ( + DUCKDB_VERSION, + PANDAS_VERSION, + POLARS_VERSION, + Constructor, + assert_equal_data, +) if TYPE_CHECKING: - from narwhals.testing.typing import Constructor from narwhals.typing import IntoDataFrame, IntoLazyFrameT, JoinStrategy diff --git a/tests/frame/lazy_test.py b/tests/frame/lazy_test.py index 258bacd73a..9e671c68d2 100644 --- a/tests/frame/lazy_test.py +++ b/tests/frame/lazy_test.py @@ -9,12 +9,17 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.dependencies import get_cudf, get_modin -from narwhals.testing.constructors import pyspark_session, sqlframe_session -from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, assert_equal_data +from tests.utils import ( + DUCKDB_VERSION, + PANDAS_VERSION, + assert_equal_data, + pyspark_session, + sqlframe_session, +) if TYPE_CHECKING: from narwhals._typing import LazyAllowed, SparkLike - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager data = {"a": [1, 2, 3], "b": ["x", "y", "z"]} diff --git a/tests/frame/len_test.py b/tests/frame/len_test.py index c403dd3da8..3e709701c5 100644 --- a/tests/frame/len_test.py +++ b/tests/frame/len_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager data = {"a": [1.0, 2.0, None, 4.0], "b": [None, 3.0, None, 5.0]} diff --git a/tests/frame/null_count_test.py b/tests/frame/null_count_test.py index 181f106fc9..eec6475c92 100644 --- a/tests/frame/null_count_test.py +++ b/tests/frame/null_count_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data def test_null_count(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/pipe_test.py b/tests/frame/pipe_test.py index 24e2fd0e83..0a2eac992d 100644 --- a/tests/frame/pipe_test.py +++ b/tests/frame/pipe_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data data = {"a": ["foo", "bars"], "ab": ["foo", "bars"]} diff --git a/tests/frame/pivot_test.py b/tests/frame/pivot_test.py index 1e1314e5c5..260006555e 100644 --- a/tests/frame/pivot_test.py +++ b/tests/frame/pivot_test.py @@ -1,16 +1,13 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw from narwhals.exceptions import NarwhalsError -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import POLARS_VERSION, ConstructorEager, assert_equal_data data = { "ix": [1, 2, 1, 1, 2, 2], diff --git a/tests/frame/rename_test.py b/tests/frame/rename_test.py index 9353d4f0d0..11ec77185c 100644 --- a/tests/frame/rename_test.py +++ b/tests/frame/rename_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data def test_rename(constructor: Constructor) -> None: diff --git a/tests/frame/row_test.py b/tests/frame/row_test.py index 614f04d7c5..186fbf3478 100644 --- a/tests/frame/row_test.py +++ b/tests/frame/row_test.py @@ -7,7 +7,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager def test_row_column(request: Any, constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/rows_test.py b/tests/frame/rows_test.py index ac5dae41ed..95fd2d8094 100644 --- a/tests/frame/rows_test.py +++ b/tests/frame/rows_test.py @@ -8,7 +8,7 @@ from tests.utils import is_pd_na if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} data_na = {"a": [None, 3, 2], "b": [4, 4, 6], "z": [7.0, None, 9]} diff --git a/tests/frame/sample_test.py b/tests/frame/sample_test.py index 7bd11b46a0..b86ddaee1d 100644 --- a/tests/frame/sample_test.py +++ b/tests/frame/sample_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager def test_sample_n(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/schema_test.py b/tests/frame/schema_test.py index b7147686cf..d90916b029 100644 --- a/tests/frame/schema_test.py +++ b/tests/frame/schema_test.py @@ -17,13 +17,13 @@ import polars as pl from typing_extensions import TypeAlias - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import ( DTypeBackend, IntoArrowSchema, IntoPandasSchema, IntoPolarsSchema, ) + from tests.utils import Constructor, ConstructorEager TimeUnit: TypeAlias = Literal["ns", "us"] @@ -588,8 +588,8 @@ def origin_pandas_like_pyarrow( if PANDAS_VERSION < (1, 5): pytest.skip(reason="pandas too old for `pyarrow`") name_pandas_like = {"pandas_pyarrow_constructor", "modin_pyarrow_constructor"} - if str(constructor_pandas_like) not in name_pandas_like: - pytest.skip(f"{str(constructor_pandas_like)!r} is not pandas_like_pyarrow") + if constructor_pandas_like.__name__ not in name_pandas_like: + pytest.skip(f"{constructor_pandas_like.__name__!r} is not pandas_like_pyarrow") data = { "a": [2, 1], "b": ["hello", "hi"], diff --git a/tests/frame/select_test.py b/tests/frame/select_test.py index 2a58de9a01..ad8bce44f8 100644 --- a/tests/frame/select_test.py +++ b/tests/frame/select_test.py @@ -1,15 +1,19 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw from narwhals.exceptions import ColumnNotFoundError, InvalidIntoExprError, NarwhalsError -from tests.utils import DASK_VERSION, DUCKDB_VERSION, assert_equal_data, maybe_collect - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + DASK_VERSION, + DUCKDB_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, + maybe_collect, +) class Foo: ... diff --git a/tests/frame/shape_test.py b/tests/frame/shape_test.py index 884776ba43..4a4cf710b9 100644 --- a/tests/frame/shape_test.py +++ b/tests/frame/shape_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager def test_shape(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/sink_parquet_test.py b/tests/frame/sink_parquet_test.py index fd8da6cd01..360828bded 100644 --- a/tests/frame/sink_parquet_test.py +++ b/tests/frame/sink_parquet_test.py @@ -8,7 +8,7 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: - from narwhals.testing.typing import Constructor + from tests.utils import Constructor pytest.importorskip("pyarrow") diff --git a/tests/frame/sort_test.py b/tests/frame/sort_test.py index 18ab40a6b3..4539d90606 100644 --- a/tests/frame/sort_test.py +++ b/tests/frame/sort_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data def test_sort(constructor: Constructor) -> None: diff --git a/tests/frame/tail_test.py b/tests/frame/tail_test.py index 461c52c614..4fdf4da8ef 100644 --- a/tests/frame/tail_test.py +++ b/tests/frame/tail_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data def test_tail(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/to_arrow_test.py b/tests/frame/to_arrow_test.py index a276851e2e..3bb74a9dc7 100644 --- a/tests/frame/to_arrow_test.py +++ b/tests/frame/to_arrow_test.py @@ -10,7 +10,7 @@ import pyarrow as pa if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager @pytest.mark.filterwarnings("ignore:.*is_sparse is deprecated:DeprecationWarning") diff --git a/tests/frame/to_dict_test.py b/tests/frame/to_dict_test.py index 5d54d45aa9..c382a0619f 100644 --- a/tests/frame/to_dict_test.py +++ b/tests/frame/to_dict_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data @pytest.mark.filterwarnings( diff --git a/tests/frame/to_native_test.py b/tests/frame/to_native_test.py index ee48d2da23..0ef0ae885a 100644 --- a/tests/frame/to_native_test.py +++ b/tests/frame/to_native_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import Constructor + from tests.utils import Constructor def test_to_native(constructor: Constructor) -> None: diff --git a/tests/frame/to_numpy_test.py b/tests/frame/to_numpy_test.py index db892e90e1..9330a2c7b3 100644 --- a/tests/frame/to_numpy_test.py +++ b/tests/frame/to_numpy_test.py @@ -12,7 +12,7 @@ from tests.utils import PANDAS_VERSION, is_windows if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager def test_to_numpy(constructor_eager: ConstructorEager) -> None: diff --git a/tests/frame/to_pandas_test.py b/tests/frame/to_pandas_test.py index 1e37494dfb..473b685c19 100644 --- a/tests/frame/to_pandas_test.py +++ b/tests/frame/to_pandas_test.py @@ -11,7 +11,7 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager @pytest.mark.filterwarnings("ignore:.*Passing a BlockManager.*:DeprecationWarning") @@ -22,7 +22,7 @@ def test_convert_pandas(constructor_eager: ConstructorEager) -> None: df_raw = constructor_eager(data) result = nw.from_native(df_raw, eager_only=True).to_pandas() - if str(constructor_eager).startswith("pandas"): + if constructor_eager.__name__.startswith("pandas"): expected = cast("pd.DataFrame", constructor_eager(data)) elif "modin_pyarrow" in str(constructor_eager): expected = pd.DataFrame(data).convert_dtypes(dtype_backend="pyarrow") diff --git a/tests/frame/to_polars_test.py b/tests/frame/to_polars_test.py index 11e44f344a..60ca653f32 100644 --- a/tests/frame/to_polars_test.py +++ b/tests/frame/to_polars_test.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: from collections.abc import Mapping - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager pytest.importorskip("polars") import polars as pl diff --git a/tests/frame/top_k_test.py b/tests/frame/top_k_test.py index e6f3de79bc..d0ba228df0 100644 --- a/tests/frame/top_k_test.py +++ b/tests/frame/top_k_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import DUCKDB_VERSION, POLARS_VERSION, Constructor, assert_equal_data def test_top_k(constructor: Constructor) -> None: diff --git a/tests/frame/unique_test.py b/tests/frame/unique_test.py index e4a44fff30..691540d0a2 100644 --- a/tests/frame/unique_test.py +++ b/tests/frame/unique_test.py @@ -1,15 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal +from typing import Literal import pytest import narwhals as nw from narwhals.exceptions import ColumnNotFoundError, InvalidOperationError -from tests.utils import DUCKDB_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import DUCKDB_VERSION, Constructor, ConstructorEager, assert_equal_data data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/frame/unpivot_test.py b/tests/frame/unpivot_test.py index 7ab51a1d49..8d661ae1de 100644 --- a/tests/frame/unpivot_test.py +++ b/tests/frame/unpivot_test.py @@ -6,11 +6,10 @@ import pytest import narwhals as nw -from tests.utils import PYARROW_VERSION, assert_equal_data +from tests.utils import PYARROW_VERSION, Constructor, assert_equal_data if TYPE_CHECKING: from narwhals.stable.v1.dtypes import DType - from narwhals.testing.typing import Constructor data = {"a": [7, 8, 9], "b": [1, 3, 5], "c": [2, 4, 6]} diff --git a/tests/frame/with_columns_sequence_test.py b/tests/frame/with_columns_sequence_test.py index 274cf5a40b..106f473886 100644 --- a/tests/frame/with_columns_sequence_test.py +++ b/tests/frame/with_columns_sequence_test.py @@ -3,15 +3,10 @@ import pytest pytest.importorskip("numpy") -from typing import TYPE_CHECKING - import numpy as np import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data data = {"a": ["foo", "bars"], "ab": ["foo", "bars"]} diff --git a/tests/frame/with_columns_test.py b/tests/frame/with_columns_test.py index 875dea0a01..e2325e9347 100644 --- a/tests/frame/with_columns_test.py +++ b/tests/frame/with_columns_test.py @@ -1,15 +1,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw from narwhals.exceptions import ColumnNotFoundError, ShapeError -from tests.utils import PYARROW_VERSION, assert_equal_data, maybe_collect - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager +from tests.utils import ( + PYARROW_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, + maybe_collect, +) def test_with_columns_int_col_name_pandas() -> None: diff --git a/tests/frame/with_row_index_test.py b/tests/frame/with_row_index_test.py index b065e72080..10310ca7f6 100644 --- a/tests/frame/with_row_index_test.py +++ b/tests/frame/with_row_index_test.py @@ -5,14 +5,18 @@ import pytest import narwhals as nw -from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, POLARS_VERSION, assert_equal_data +from tests.utils import ( + DUCKDB_VERSION, + PANDAS_VERSION, + POLARS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) if TYPE_CHECKING: from collections.abc import Sequence - from narwhals.testing.typing import Constructor, ConstructorEager - - data = {"abc": ["foo", "bars"], "xyz": [100, 200], "const": [42, 42]} diff --git a/tests/frame/write_csv_test.py b/tests/frame/write_csv_test.py index 9080e0319c..5f907ac78c 100644 --- a/tests/frame/write_csv_test.py +++ b/tests/frame/write_csv_test.py @@ -3,13 +3,11 @@ from typing import TYPE_CHECKING import narwhals as nw -from tests.utils import is_windows +from tests.utils import ConstructorEager, is_windows if TYPE_CHECKING: import pytest - from narwhals.testing.typing import ConstructorEager - def test_write_csv( constructor_eager: ConstructorEager, tmpdir: pytest.TempdirFactory diff --git a/tests/frame/write_parquet_test.py b/tests/frame/write_parquet_test.py index 47b277f7b9..cf86f1be00 100644 --- a/tests/frame/write_parquet_test.py +++ b/tests/frame/write_parquet_test.py @@ -8,7 +8,7 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager pytest.importorskip("pyarrow") data = {"a": [1, 2, 3]} diff --git a/tests/from_dict_test.py b/tests/from_dict_test.py index 06d3764987..93ea5b2880 100644 --- a/tests/from_dict_test.py +++ b/tests/from_dict_test.py @@ -7,11 +7,10 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError from narwhals.utils import Implementation -from tests.utils import PYARROW_VERSION, assert_equal_data +from tests.utils import PYARROW_VERSION, Constructor, assert_equal_data if TYPE_CHECKING: from narwhals._typing import EagerAllowed, Polars - from narwhals.testing.typing import Constructor def test_from_dict(eager_backend: EagerAllowed) -> None: diff --git a/tests/from_numpy_test.py b/tests/from_numpy_test.py index 834114107c..e8ebcbc6f0 100644 --- a/tests/from_numpy_test.py +++ b/tests/from_numpy_test.py @@ -8,10 +8,9 @@ import numpy as np import narwhals as nw -from tests.utils import assert_equal_data +from tests.utils import ConstructorEager, assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager from narwhals.typing import _2DArray data = {"a": [1, 2, 3], "b": [4, 5, 6]} diff --git a/tests/hypothesis/getitem_test.py b/tests/hypothesis/getitem_test.py index 638922567f..759a292f97 100644 --- a/tests/hypothesis/getitem_test.py +++ b/tests/hypothesis/getitem_test.py @@ -7,7 +7,7 @@ from hypothesis import assume, given import narwhals as nw -from narwhals.testing.constructors import ConstructorName +from tests.conftest import pandas_constructor, pyarrow_table_constructor from tests.utils import assert_equal_data if TYPE_CHECKING: @@ -19,10 +19,8 @@ pytest.importorskip("polars") import polars as pl -params = [ConstructorName.PANDAS.constructor, ConstructorName.PYARROW.constructor] - -@pytest.fixture(params=params, scope="module") +@pytest.fixture(params=[pandas_constructor, pyarrow_table_constructor], scope="module") def pandas_or_pyarrow_constructor( request: pytest.FixtureRequest, ) -> Callable[[Any], IntoDataFrame]: @@ -127,7 +125,7 @@ def test_getitem(pandas_or_pyarrow_constructor: Any, selector: Any) -> None: # NotImplementedError: Slicing with step is not supported on PyArrow tables assume( not ( - pandas_or_pyarrow_constructor.name.is_pyarrow + pandas_or_pyarrow_constructor is pyarrow_table_constructor and isinstance(selector, slice) and selector.step is not None ) @@ -136,7 +134,7 @@ def test_getitem(pandas_or_pyarrow_constructor: Any, selector: Any) -> None: # NotImplementedError: Slicing with step is not supported on PyArrow tables assume( not ( - pandas_or_pyarrow_constructor.name.is_pyarrow + pandas_or_pyarrow_constructor is pyarrow_table_constructor and isinstance(selector, tuple) and ( (isinstance(selector[0], slice) and selector[0].step is not None) diff --git a/tests/joblib_test.py b/tests/joblib_test.py index 288562928e..72b9440544 100644 --- a/tests/joblib_test.py +++ b/tests/joblib_test.py @@ -1,19 +1,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import assert_equal_data +from tests.utils import ConstructorEager, assert_equal_data pytest.importorskip("joblib") from joblib import Parallel, delayed -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager - def test_parallelisability(constructor_eager: ConstructorEager) -> None: # https://github.com/narwhals-dev/narwhals/issues/2450 diff --git a/tests/modern_polars/ewm_mean_test.py b/tests/modern_polars/ewm_mean_test.py index b34f5650c9..0b3ea06923 100644 --- a/tests/modern_polars/ewm_mean_test.py +++ b/tests/modern_polars/ewm_mean_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import POLARS_VERSION, ConstructorEager, assert_equal_data def test_ew_mean( diff --git a/tests/modern_polars/filter_test.py b/tests/modern_polars/filter_test.py index bd51934573..a6a0664cf8 100644 --- a/tests/modern_polars/filter_test.py +++ b/tests/modern_polars/filter_test.py @@ -1,13 +1,9 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data def test_filter(constructor: Constructor) -> None: diff --git a/tests/modern_polars/method_chaining_2_test.py b/tests/modern_polars/method_chaining_2_test.py index 4845728ef6..58451e9f9a 100644 --- a/tests/modern_polars/method_chaining_2_test.py +++ b/tests/modern_polars/method_chaining_2_test.py @@ -1,13 +1,9 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data data = { "flight_date": [ diff --git a/tests/modern_polars/method_chaining_test.py b/tests/modern_polars/method_chaining_test.py index 42798cf532..611f85973e 100644 --- a/tests/modern_polars/method_chaining_test.py +++ b/tests/modern_polars/method_chaining_test.py @@ -1,15 +1,11 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import PANDAS_VERSION, Constructor, assert_equal_data data = { "OriginCityName": [ @@ -42,7 +38,10 @@ def test_split_list_get(request: pytest.FixtureRequest, constructor: Constructor if PANDAS_VERSION < (2, 2): pytest.skip() pytest.importorskip("pyarrow") - if str(constructor).startswith("pandas") and "pyarrow" not in str(constructor): + if ( + constructor.__name__.startswith("pandas") + and "pyarrow" not in constructor.__name__ + ): df = nw.from_native(constructor(data)) msg = re.escape("This operation requires a pyarrow-backed series. ") with pytest.raises(TypeError, match=msg): diff --git a/tests/modern_polars/performance_test.py b/tests/modern_polars/performance_test.py index 03462bcd7f..b765055cbd 100644 --- a/tests/modern_polars/performance_test.py +++ b/tests/modern_polars/performance_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data data = {"weight": ["89kg", "83", "79kg", "68kg", "78kg", "73", "86kg"]} diff --git a/tests/modern_polars/pivot_test.py b/tests/modern_polars/pivot_test.py index 41accde4ab..b6e6b16dde 100644 --- a/tests/modern_polars/pivot_test.py +++ b/tests/modern_polars/pivot_test.py @@ -1,15 +1,11 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import POLARS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import POLARS_VERSION, ConstructorEager, assert_equal_data def test_pivot( diff --git a/tests/modern_polars/unpivot_test.py b/tests/modern_polars/unpivot_test.py index b5ae2b192e..c5bff87dc6 100644 --- a/tests/modern_polars/unpivot_test.py +++ b/tests/modern_polars/unpivot_test.py @@ -1,13 +1,9 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor +from tests.utils import Constructor, assert_equal_data def test_unpivot(constructor: Constructor) -> None: diff --git a/tests/namespace_test.py b/tests/namespace_test.py index 46623b266c..e94a6690c1 100644 --- a/tests/namespace_test.py +++ b/tests/namespace_test.py @@ -21,8 +21,8 @@ from narwhals._pandas_like.namespace import PandasLikeNamespace # noqa: F401 from narwhals._polars.namespace import PolarsNamespace # noqa: F401 from narwhals._typing import BackendName, _EagerAllowed - from narwhals.testing.typing import Constructor from narwhals.typing import _2DArray + from tests.utils import Constructor ExprT = TypeVar("ExprT", bound="CompliantExprAny") diff --git a/tests/new_series_test.py b/tests/new_series_test.py index a311694bf7..9360ef6fdc 100644 --- a/tests/new_series_test.py +++ b/tests/new_series_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data def test_new_series(constructor_eager: ConstructorEager) -> None: diff --git a/tests/read_scan_test.py b/tests/read_scan_test.py index 487132382b..4548f76a87 100644 --- a/tests/read_scan_test.py +++ b/tests/read_scan_test.py @@ -6,8 +6,13 @@ import pytest import narwhals as nw -from narwhals.testing.constructors import pyspark_session, sqlframe_session -from tests.utils import PANDAS_VERSION, assert_equal_data +from tests.utils import ( + PANDAS_VERSION, + Constructor, + assert_equal_data, + pyspark_session, + sqlframe_session, +) pytest.importorskip("polars") pytest.importorskip("pyarrow") @@ -21,7 +26,6 @@ from typing_extensions import TypeAlias from narwhals._typing import EagerAllowed, _LazyOnly, _SparkLike - from narwhals.testing.typing import Constructor from narwhals.typing import FileSource Factory: TypeAlias = pytest.TempPathFactory diff --git a/tests/selectors_test.py b/tests/selectors_test.py index d1a8c562cf..63c6b387e5 100644 --- a/tests/selectors_test.py +++ b/tests/selectors_test.py @@ -2,7 +2,7 @@ import re from datetime import datetime, timezone -from typing import TYPE_CHECKING, Literal +from typing import Literal import pytest @@ -12,13 +12,11 @@ PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, + Constructor, assert_equal_data, is_windows, ) -if TYPE_CHECKING: - from narwhals.testing.typing import Constructor - data = { "a": [1, 1, 2], "b": ["a", "b", "c"], diff --git a/tests/series_only/__contains___test.py b/tests/series_only/__contains___test.py index c220e6570e..13bc0eb4e0 100644 --- a/tests/series_only/__contains___test.py +++ b/tests/series_only/__contains___test.py @@ -8,7 +8,7 @@ from narwhals.exceptions import InvalidOperationError if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager data = [100, 200, None] diff --git a/tests/series_only/__iter___test.py b/tests/series_only/__iter___test.py index 201e04a677..5ed9e083f9 100644 --- a/tests/series_only/__iter___test.py +++ b/tests/series_only/__iter___test.py @@ -9,7 +9,7 @@ from tests.utils import assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager data = [1, 2, 3] diff --git a/tests/series_only/alias_rename_test.py b/tests/series_only/alias_rename_test.py index a609cee024..e8420e8089 100644 --- a/tests/series_only/alias_rename_test.py +++ b/tests/series_only/alias_rename_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data def test_alias_rename(constructor_eager: ConstructorEager) -> None: diff --git a/tests/series_only/arg_max_test.py b/tests/series_only/arg_max_test.py index 9e43034b24..c6460b7183 100644 --- a/tests/series_only/arg_max_test.py +++ b/tests/series_only/arg_max_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0], "i": [3, 1, 5]} diff --git a/tests/series_only/arg_min_test.py b/tests/series_only/arg_min_test.py index 1edf98889d..34a38a57f9 100644 --- a/tests/series_only/arg_min_test.py +++ b/tests/series_only/arg_min_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} diff --git a/tests/series_only/arg_true_test.py b/tests/series_only/arg_true_test.py index 3942d1331b..ccc25a83e7 100644 --- a/tests/series_only/arg_true_test.py +++ b/tests/series_only/arg_true_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data def test_arg_true_series(constructor_eager: ConstructorEager) -> None: diff --git a/tests/series_only/array_dunder_test.py b/tests/series_only/array_dunder_test.py index 4a7a6a3a14..b50d033aa2 100644 --- a/tests/series_only/array_dunder_test.py +++ b/tests/series_only/array_dunder_test.py @@ -3,15 +3,15 @@ import pytest pytest.importorskip("numpy") -from typing import TYPE_CHECKING - import numpy as np import narwhals as nw -from tests.utils import PANDAS_VERSION, PYARROW_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ( + PANDAS_VERSION, + PYARROW_VERSION, + ConstructorEager, + assert_equal_data, +) def test_array_dunder( diff --git a/tests/series_only/cast_test.py b/tests/series_only/cast_test.py index 622f5da8ca..b646f38299 100644 --- a/tests/series_only/cast_test.py +++ b/tests/series_only/cast_test.py @@ -9,7 +9,7 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager def test_cast_253( diff --git a/tests/series_only/gather_every_test.py b/tests/series_only/gather_every_test.py index 5c8d2b238d..e7d55fb03c 100644 --- a/tests/series_only/gather_every_test.py +++ b/tests/series_only/gather_every_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data data = {"a": list(range(10))} diff --git a/tests/series_only/getitem_test.py b/tests/series_only/getitem_test.py index ce8a7eb549..aa63ac8414 100644 --- a/tests/series_only/getitem_test.py +++ b/tests/series_only/getitem_test.py @@ -8,7 +8,7 @@ from tests.utils import assert_equal_data, assert_equal_series if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager def test_by_slice(constructor_eager: ConstructorEager) -> None: diff --git a/tests/series_only/head_test.py b/tests/series_only/head_test.py index ae09fcc6df..5697bc59e5 100644 --- a/tests/series_only/head_test.py +++ b/tests/series_only/head_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data @pytest.mark.parametrize("n", [2, -1]) diff --git a/tests/series_only/hist_test.py b/tests/series_only/hist_test.py index 3e58e5b524..5b42eae346 100644 --- a/tests/series_only/hist_test.py +++ b/tests/series_only/hist_test.py @@ -12,13 +12,11 @@ import narwhals as nw from narwhals.exceptions import ComputeError from narwhals.testing.constructors import ConstructorName -from tests.utils import POLARS_VERSION, assert_equal_data +from tests.utils import POLARS_VERSION, ConstructorEager, assert_equal_data if TYPE_CHECKING: from collections.abc import Sequence - from narwhals.testing.typing import ConstructorEager - rnd = Random(0) # noqa: S311 data: dict[str, Any] = { diff --git a/tests/series_only/is_empty_test.py b/tests/series_only/is_empty_test.py index 43c885f645..cfdc8bd15d 100644 --- a/tests/series_only/is_empty_test.py +++ b/tests/series_only/is_empty_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager def test_is_empty(constructor_eager: ConstructorEager) -> None: diff --git a/tests/series_only/is_ordered_categorical_test.py b/tests/series_only/is_ordered_categorical_test.py index 55f1057688..8833957b1c 100644 --- a/tests/series_only/is_ordered_categorical_test.py +++ b/tests/series_only/is_ordered_categorical_test.py @@ -9,7 +9,7 @@ from tests.utils import POLARS_VERSION if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager class MockCompliantSeries: diff --git a/tests/series_only/is_sorted_test.py b/tests/series_only/is_sorted_test.py index 716b382b25..4efddf542f 100644 --- a/tests/series_only/is_sorted_test.py +++ b/tests/series_only/is_sorted_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data data = [1, 3, 2] data_dups = [4, 4, 6] diff --git a/tests/series_only/item_test.py b/tests/series_only/item_test.py index de267acb64..521ce5af55 100644 --- a/tests/series_only/item_test.py +++ b/tests/series_only/item_test.py @@ -1,15 +1,11 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data data = [1, 3, 2] diff --git a/tests/series_only/scatter_test.py b/tests/series_only/scatter_test.py index 5046df0cdc..d608f78f1a 100644 --- a/tests/series_only/scatter_test.py +++ b/tests/series_only/scatter_test.py @@ -6,13 +6,11 @@ import pytest import narwhals as nw -from tests.utils import assert_equal_data, assert_equal_series +from tests.utils import ConstructorEager, assert_equal_data, assert_equal_series if TYPE_CHECKING: from collections.abc import Collection - from narwhals.testing.typing import ConstructorEager - def series(frame: ConstructorEager, name: str, values: Collection[Any]) -> nw.Series[Any]: return nw.from_native(frame({name: values})).get_column(name) diff --git a/tests/series_only/shape_test.py b/tests/series_only/shape_test.py index efad8917bc..8f8ba638e5 100644 --- a/tests/series_only/shape_test.py +++ b/tests/series_only/shape_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager def test_shape(constructor_eager: ConstructorEager) -> None: diff --git a/tests/series_only/sort_test.py b/tests/series_only/sort_test.py index def1013670..8b74c6623b 100644 --- a/tests/series_only/sort_test.py +++ b/tests/series_only/sort_test.py @@ -8,7 +8,7 @@ from tests.utils import assert_equal_data if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager @pytest.mark.parametrize( diff --git a/tests/series_only/tail_test.py b/tests/series_only/tail_test.py index 6b6e2573d2..43f9866094 100644 --- a/tests/series_only/tail_test.py +++ b/tests/series_only/tail_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data @pytest.mark.parametrize("n", [2, -1]) diff --git a/tests/series_only/to_arrow_test.py b/tests/series_only/to_arrow_test.py index a3c7c54b84..6675ea2e31 100644 --- a/tests/series_only/to_arrow_test.py +++ b/tests/series_only/to_arrow_test.py @@ -11,7 +11,7 @@ import pyarrow.compute as pc if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager def test_to_arrow(constructor_eager: ConstructorEager) -> None: diff --git a/tests/series_only/to_dummy_test.py b/tests/series_only/to_dummy_test.py index f819e5d447..df301a6978 100644 --- a/tests/series_only/to_dummy_test.py +++ b/tests/series_only/to_dummy_test.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data data = ["x", "y", "z"] data_na = ["x", "y", None] diff --git a/tests/series_only/to_frame_test.py b/tests/series_only/to_frame_test.py index 10a10ee732..8f56f7f6fa 100644 --- a/tests/series_only/to_frame_test.py +++ b/tests/series_only/to_frame_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data data = [1, 2, 3] diff --git a/tests/series_only/to_list_test.py b/tests/series_only/to_list_test.py index 859a6e92f4..e532f54799 100644 --- a/tests/series_only/to_list_test.py +++ b/tests/series_only/to_list_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data data = [1, 2, 3] diff --git a/tests/series_only/to_native_test.py b/tests/series_only/to_native_test.py index 6780c1c73a..350d81764d 100644 --- a/tests/series_only/to_native_test.py +++ b/tests/series_only/to_native_test.py @@ -5,7 +5,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager data = [4, 4, 4, 1, 6, 6, 4, 4, 1, 1] diff --git a/tests/series_only/to_numpy_test.py b/tests/series_only/to_numpy_test.py index c08945be88..21873760cb 100644 --- a/tests/series_only/to_numpy_test.py +++ b/tests/series_only/to_numpy_test.py @@ -13,7 +13,7 @@ from tests.utils import PANDAS_VERSION, is_windows if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager def test_to_numpy( diff --git a/tests/series_only/to_pandas_test.py b/tests/series_only/to_pandas_test.py index 7e616f971c..95cd969f14 100644 --- a/tests/series_only/to_pandas_test.py +++ b/tests/series_only/to_pandas_test.py @@ -12,7 +12,7 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager data = [1, 3, 2] diff --git a/tests/series_only/to_polars_test.py b/tests/series_only/to_polars_test.py index f3bfe0a223..c99d6e889e 100644 --- a/tests/series_only/to_polars_test.py +++ b/tests/series_only/to_polars_test.py @@ -7,7 +7,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager pytest.importorskip("polars") import polars as pl diff --git a/tests/series_only/value_counts_test.py b/tests/series_only/value_counts_test.py index 41dae97228..77a7656cf2 100644 --- a/tests/series_only/value_counts_test.py +++ b/tests/series_only/value_counts_test.py @@ -1,14 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import PANDAS_VERSION, ConstructorEager, assert_equal_data data = [4, 4, 4, 1, 6, 6, 4, 4, 1, 1] diff --git a/tests/series_only/zip_with_test.py b/tests/series_only/zip_with_test.py index d77055689c..b40ac19fd6 100644 --- a/tests/series_only/zip_with_test.py +++ b/tests/series_only/zip_with_test.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import narwhals as nw -from tests.utils import assert_equal_data - -if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager +from tests.utils import ConstructorEager, assert_equal_data def test_zip_with(constructor_eager: ConstructorEager) -> None: diff --git a/tests/testing/assert_frame_equal_test.py b/tests/testing/assert_frame_equal_test.py index 58365297f8..c1b3b4e357 100644 --- a/tests/testing/assert_frame_equal_test.py +++ b/tests/testing/assert_frame_equal_test.py @@ -12,9 +12,9 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import IntoSchema from tests.conftest import Data + from tests.utils import Constructor, ConstructorEager def _assertion_error(detail: str) -> pytest.RaisesExc: diff --git a/tests/testing/assert_series_equal_test.py b/tests/testing/assert_series_equal_test.py index 5ab9f2a515..c4826c695e 100644 --- a/tests/testing/assert_series_equal_test.py +++ b/tests/testing/assert_series_equal_test.py @@ -13,9 +13,9 @@ if TYPE_CHECKING: from typing_extensions import TypeAlias - from narwhals.testing.typing import ConstructorEager from narwhals.typing import IntoSchema, IntoSeriesT from tests.conftest import Data + from tests.utils import ConstructorEager SetupFn: TypeAlias = Callable[[nw.Series[Any]], tuple[nw.Series[Any], nw.Series[Any]]] diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 33da5bc8e4..8d076699c0 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -30,8 +30,8 @@ import narwhals as nw from narwhals._utils import Version -from narwhals.testing.constructors import ConstructorName -from tests.utils import maybe_get_modin_df +from tests.conftest import sqlframe_pyspark_lazy_constructor +from tests.utils import Constructor, maybe_get_modin_df if TYPE_CHECKING: from collections.abc import Iterable, Iterator @@ -39,8 +39,6 @@ from _pytest.mark import ParameterSet from typing_extensions import assert_type - from narwhals.testing.typing import Constructor - class MockDataFrame: def __init__(self, version: Version) -> None: @@ -296,8 +294,7 @@ def test_eager_only_lazy_dask(eager_only: Any, context: Any) -> None: def test_series_only_sqlframe() -> None: # pragma: no cover pytest.importorskip("sqlframe") - constructor = ConstructorName.SQLFRAME.constructor - df = constructor(data) + df = sqlframe_pyspark_lazy_constructor(data) with pytest.raises(TypeError, match="Cannot only use `series_only`"): nw.from_native(df, series_only=True) # pyright: ignore[reportArgumentType, reportCallIssue] @@ -318,8 +315,7 @@ def test_series_only_sqlframe() -> None: # pragma: no cover ) def test_eager_only_sqlframe(eager_only: Any, context: Any) -> None: # pragma: no cover pytest.importorskip("sqlframe") - constructor = ConstructorName.SQLFRAME.constructor - df = constructor(data) + df = sqlframe_pyspark_lazy_constructor(data) with context: res = nw.from_native(df, eager_only=eager_only) diff --git a/tests/translate/get_native_namespace_test.py b/tests/translate/get_native_namespace_test.py index 9f3541584b..821443ea64 100644 --- a/tests/translate/get_native_namespace_test.py +++ b/tests/translate/get_native_namespace_test.py @@ -7,7 +7,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager data = {"a": [1, 2, 3]} @@ -76,7 +76,7 @@ def test_native_namespace_frame(constructor: Constructor) -> None: def test_native_namespace_series(constructor_eager: ConstructorEager) -> None: - constructor_name = str(constructor_eager) + constructor_name = constructor_eager.__name__ expected_namespace = _get_expected_namespace(constructor_name=constructor_name) diff --git a/tests/translate/to_native_test.py b/tests/translate/to_native_test.py index 594c95f08c..f84cf80dd6 100644 --- a/tests/translate/to_native_test.py +++ b/tests/translate/to_native_test.py @@ -8,7 +8,7 @@ import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager + from tests.utils import ConstructorEager @pytest.mark.parametrize( diff --git a/tests/utils.py b/tests/utils.py index 1b470bd71e..4d01223b2a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,7 +6,7 @@ import warnings from datetime import date, datetime from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, cast import pytest @@ -19,10 +19,12 @@ from collections.abc import Mapping, Sequence import pandas as pd + from pyspark.sql import SparkSession + from sqlframe.duckdb import DuckDBSession from typing_extensions import TypeAlias - from narwhals.testing.typing import Constructor, ConstructorEager - from narwhals.typing import Frame, TimeUnit + from narwhals._native import NativeLazyFrame + from narwhals.typing import Frame, IntoDataFrame, TimeUnit def get_module_version_as_tuple(module_name: str) -> tuple[int, ...]: @@ -42,6 +44,9 @@ def get_module_version_as_tuple(module_name: str) -> tuple[int, ...]: PYSPARK_VERSION: tuple[int, ...] = get_module_version_as_tuple("pyspark") CUDF_VERSION: tuple[int, ...] = get_module_version_as_tuple("cudf") +Constructor: TypeAlias = Callable[[Any], "NativeLazyFrame | IntoDataFrame"] +ConstructorEager: TypeAlias = Callable[[Any], "IntoDataFrame"] +ConstructorLazy: TypeAlias = Callable[[Any], "NativeLazyFrame"] ConstructorPandasLike: TypeAlias = Callable[[Any], "pd.DataFrame"] NestedOrEnumDType: TypeAlias = "nw.List | nw.Array | nw.Struct | nw.Enum" @@ -169,6 +174,33 @@ def assert_equal_hash(left: Any, right: Any) -> None: ) +def sqlframe_session() -> DuckDBSession: + from sqlframe.duckdb import DuckDBSession + + # NOTE: `__new__` override inferred by `pyright` only + # https://github.com/eakmanrq/sqlframe/blob/772b3a6bfe5a1ffd569b7749d84bea2f3a314510/sqlframe/base/session.py#L181-L184 + return cast("DuckDBSession", DuckDBSession()) # type: ignore[redundant-cast] + + +def pyspark_session() -> SparkSession: # pragma: no cover + if is_spark_connect := os.environ.get("SPARK_CONNECT", None): + from pyspark.sql.connect.session import SparkSession + else: + from pyspark.sql import SparkSession + builder = cast("SparkSession.Builder", SparkSession.builder).appName("unit-tests") + builder = ( + builder.remote(f"sc://localhost:{os.environ.get('SPARK_PORT', '15002')}") + if is_spark_connect + else builder.master("local[1]").config("spark.ui.enabled", "false") + ) + return ( + builder.config("spark.default.parallelism", "1") + .config("spark.sql.shuffle.partitions", "2") + .config("spark.sql.session.timeZone", "UTC") + .getOrCreate() + ) + + def maybe_get_modin_df(df_pandas: pd.DataFrame) -> Any: # pragma: no cover """Convert a pandas DataFrame to a Modin DataFrame if Modin is available.""" try: @@ -198,7 +230,10 @@ def is_pyarrow_windows_no_tzdata(constructor: Constructor, /) -> bool: def uses_pyarrow_backend(constructor: Constructor | ConstructorEager) -> bool: """Checks if the pandas-like constructor uses pyarrow backend.""" - return str(constructor) in {"pandas_pyarrow_constructor", "modin_pyarrow_constructor"} + return constructor.__name__ in { + "pandas_pyarrow_constructor", + "modin_pyarrow_constructor", + } def maybe_collect(df: Frame) -> Frame: diff --git a/tests/v1_test.py b/tests/v1_test.py index a71daa087f..9882c4ed15 100644 --- a/tests/v1_test.py +++ b/tests/v1_test.py @@ -36,6 +36,8 @@ PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, + Constructor, + ConstructorEager, assert_equal_data, assert_equal_hash, assert_equal_series, @@ -49,8 +51,8 @@ from narwhals._typing import EagerAllowed from narwhals.dtypes import DType from narwhals.stable.v1.typing import IntoDataFrameT - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import IntoDType, _1DArray, _2DArray + from tests.utils import Constructor, ConstructorEager def test_toplevel() -> None: diff --git a/tests/v2_test.py b/tests/v2_test.py index 94576d9bb3..7a1903425c 100644 --- a/tests/v2_test.py +++ b/tests/v2_test.py @@ -18,6 +18,8 @@ PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, + Constructor, + ConstructorEager, assert_equal_data, assert_equal_series, ) @@ -29,7 +31,6 @@ from narwhals._typing import EagerAllowed from narwhals.stable.v2.typing import IntoDataFrameT - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import IntoDType, _1DArray, _2DArray From 81f41c946287f9971ea130c43894dc79165dab20 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 16:29:32 +0200 Subject: [PATCH 18/71] forgot one file :( --- tests/utils.py | 43 ++++--------------------------------------- 1 file changed, 4 insertions(+), 39 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index 4d01223b2a..1b470bd71e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,7 +6,7 @@ import warnings from datetime import date, datetime from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import TYPE_CHECKING, Any, Callable import pytest @@ -19,12 +19,10 @@ from collections.abc import Mapping, Sequence import pandas as pd - from pyspark.sql import SparkSession - from sqlframe.duckdb import DuckDBSession from typing_extensions import TypeAlias - from narwhals._native import NativeLazyFrame - from narwhals.typing import Frame, IntoDataFrame, TimeUnit + from narwhals.testing.typing import Constructor, ConstructorEager + from narwhals.typing import Frame, TimeUnit def get_module_version_as_tuple(module_name: str) -> tuple[int, ...]: @@ -44,9 +42,6 @@ def get_module_version_as_tuple(module_name: str) -> tuple[int, ...]: PYSPARK_VERSION: tuple[int, ...] = get_module_version_as_tuple("pyspark") CUDF_VERSION: tuple[int, ...] = get_module_version_as_tuple("cudf") -Constructor: TypeAlias = Callable[[Any], "NativeLazyFrame | IntoDataFrame"] -ConstructorEager: TypeAlias = Callable[[Any], "IntoDataFrame"] -ConstructorLazy: TypeAlias = Callable[[Any], "NativeLazyFrame"] ConstructorPandasLike: TypeAlias = Callable[[Any], "pd.DataFrame"] NestedOrEnumDType: TypeAlias = "nw.List | nw.Array | nw.Struct | nw.Enum" @@ -174,33 +169,6 @@ def assert_equal_hash(left: Any, right: Any) -> None: ) -def sqlframe_session() -> DuckDBSession: - from sqlframe.duckdb import DuckDBSession - - # NOTE: `__new__` override inferred by `pyright` only - # https://github.com/eakmanrq/sqlframe/blob/772b3a6bfe5a1ffd569b7749d84bea2f3a314510/sqlframe/base/session.py#L181-L184 - return cast("DuckDBSession", DuckDBSession()) # type: ignore[redundant-cast] - - -def pyspark_session() -> SparkSession: # pragma: no cover - if is_spark_connect := os.environ.get("SPARK_CONNECT", None): - from pyspark.sql.connect.session import SparkSession - else: - from pyspark.sql import SparkSession - builder = cast("SparkSession.Builder", SparkSession.builder).appName("unit-tests") - builder = ( - builder.remote(f"sc://localhost:{os.environ.get('SPARK_PORT', '15002')}") - if is_spark_connect - else builder.master("local[1]").config("spark.ui.enabled", "false") - ) - return ( - builder.config("spark.default.parallelism", "1") - .config("spark.sql.shuffle.partitions", "2") - .config("spark.sql.session.timeZone", "UTC") - .getOrCreate() - ) - - def maybe_get_modin_df(df_pandas: pd.DataFrame) -> Any: # pragma: no cover """Convert a pandas DataFrame to a Modin DataFrame if Modin is available.""" try: @@ -230,10 +198,7 @@ def is_pyarrow_windows_no_tzdata(constructor: Constructor, /) -> bool: def uses_pyarrow_backend(constructor: Constructor | ConstructorEager) -> bool: """Checks if the pandas-like constructor uses pyarrow backend.""" - return constructor.__name__ in { - "pandas_pyarrow_constructor", - "modin_pyarrow_constructor", - } + return str(constructor) in {"pandas_pyarrow_constructor", "modin_pyarrow_constructor"} def maybe_collect(df: Frame) -> Frame: From 92a2ab1295965b567ddf46b5f478a47b45df5c36 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 16:32:45 +0200 Subject: [PATCH 19/71] alias imports --- tests/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index 1b470bd71e..86202b465b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -13,6 +13,10 @@ import narwhals as nw from narwhals._utils import Implementation, parse_version, zip_strict from narwhals.dependencies import get_pandas +from narwhals.testing.typing import ( + Constructor as Constructor, # noqa: PLC0414,TC001 + ConstructorEager as ConstructorEager, # noqa: PLC0414,TC001 +) from narwhals.translate import from_native if TYPE_CHECKING: @@ -21,7 +25,6 @@ import pandas as pd from typing_extensions import TypeAlias - from narwhals.testing.typing import Constructor, ConstructorEager from narwhals.typing import Frame, TimeUnit From 9d9d0d2ee3e68914e6e83ee616a9187dee1b0107 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 16:45:07 +0200 Subject: [PATCH 20/71] fixup tests --- tests/dtypes/dtypes_test.py | 9 ++------- .../dt/convert_time_zone_test.py | 2 +- .../dt/replace_time_zone_test.py | 9 ++------- tests/expr_and_series/fill_nan_test.py | 17 ++-------------- tests/expr_and_series/is_close_test.py | 20 ++++--------------- tests/expr_and_series/is_finite_test.py | 15 +------------- tests/expr_and_series/is_nan_test.py | 17 ++-------------- tests/frame/lazy_test.py | 9 ++------- tests/hypothesis/getitem_test.py | 8 ++++---- tests/read_scan_test.py | 9 ++------- tests/translate/from_native_test.py | 6 +++--- 11 files changed, 25 insertions(+), 96 deletions(-) diff --git a/tests/dtypes/dtypes_test.py b/tests/dtypes/dtypes_test.py index 33fa61ac08..a233f955f3 100644 --- a/tests/dtypes/dtypes_test.py +++ b/tests/dtypes/dtypes_test.py @@ -9,13 +9,8 @@ import narwhals as nw from narwhals.exceptions import InvalidOperationError, PerformanceWarning -from tests.utils import ( - PANDAS_VERSION, - POLARS_VERSION, - PYARROW_VERSION, - assert_equal_hash, - pyspark_session, -) +from narwhals.testing.constructors import pyspark_session +from tests.utils import PANDAS_VERSION, POLARS_VERSION, PYARROW_VERSION, assert_equal_hash if TYPE_CHECKING: from collections.abc import Iterable diff --git a/tests/expr_and_series/dt/convert_time_zone_test.py b/tests/expr_and_series/dt/convert_time_zone_test.py index 65d1a6e3b6..8fd654ad6d 100644 --- a/tests/expr_and_series/dt/convert_time_zone_test.py +++ b/tests/expr_and_series/dt/convert_time_zone_test.py @@ -7,13 +7,13 @@ import pytest import narwhals as nw +from narwhals.testing.constructors import pyspark_session from tests.utils import ( PANDAS_VERSION, POLARS_VERSION, Constructor, assert_equal_data, is_windows, - pyspark_session, ) if TYPE_CHECKING: diff --git a/tests/expr_and_series/dt/replace_time_zone_test.py b/tests/expr_and_series/dt/replace_time_zone_test.py index 1c9dff7d59..27bc394b69 100644 --- a/tests/expr_and_series/dt/replace_time_zone_test.py +++ b/tests/expr_and_series/dt/replace_time_zone_test.py @@ -7,13 +7,8 @@ import pytest import narwhals as nw -from tests.utils import ( - PANDAS_VERSION, - Constructor, - assert_equal_data, - is_windows, - pyspark_session, -) +from narwhals.testing.constructors import pyspark_session +from tests.utils import PANDAS_VERSION, Constructor, assert_equal_data, is_windows if TYPE_CHECKING: from tests.utils import ConstructorEager diff --git a/tests/expr_and_series/fill_nan_test.py b/tests/expr_and_series/fill_nan_test.py index 132b553c50..dd63801ff0 100644 --- a/tests/expr_and_series/fill_nan_test.py +++ b/tests/expr_and_series/fill_nan_test.py @@ -3,21 +3,8 @@ import pytest import narwhals as nw -from tests.conftest import ( - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, - pandas_constructor, -) from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data -NON_NULLABLE_CONSTRUCTORS = [ - pandas_constructor, - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, -] - def test_fill_nan(request: pytest.FixtureRequest, constructor: Constructor) -> None: if "cudf" in str(constructor): @@ -36,7 +23,7 @@ def test_fill_nan(request: pytest.FixtureRequest, constructor: Constructor) -> N assert_equal_data(result, expected) assert result.lazy().collect()["float_na"].null_count() == 2 result = df.select(nw.all().fill_nan(3.0)) - if any(constructor is c for c in NON_NULLABLE_CONSTRUCTORS): + if constructor.name.is_non_nullable: # no nan vs null distinction expected = {"float": [-1.0, 1.0, 3.0], "float_na": [3.0, 1.0, 3.0]} assert result.lazy().collect()["float_na"].null_count() == 0 @@ -55,7 +42,7 @@ def test_fill_nan_series(constructor_eager: ConstructorEager) -> None: "float_na" ] result = s.fill_nan(999) - if any(constructor_eager is c for c in NON_NULLABLE_CONSTRUCTORS): + if constructor_eager.name.is_non_nullable: # no nan vs null distinction assert_equal_data({"a": result}, {"a": [999.0, 1.0, 999.0]}) elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): diff --git a/tests/expr_and_series/is_close_test.py b/tests/expr_and_series/is_close_test.py index 16c59536ca..f580dd5eb4 100644 --- a/tests/expr_and_series/is_close_test.py +++ b/tests/expr_and_series/is_close_test.py @@ -12,12 +12,6 @@ import narwhals as nw from narwhals.exceptions import ComputeError, InvalidOperationError -from tests.conftest import ( - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, - pandas_constructor, -) from tests.utils import ( PANDAS_VERSION, PYARROW_VERSION, @@ -29,12 +23,6 @@ if TYPE_CHECKING: from narwhals.typing import NumericLiteral -NON_NULLABLE_CONSTRUCTORS = ( - pandas_constructor, - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, -) NULL_PLACEHOLDER, NAN_PLACEHOLDER = 9999.0, -1.0 INF_POS, INF_NEG = float("inf"), float("-inf") @@ -126,7 +114,7 @@ def test_is_close_series_with_series( y = y.zip_with(y != NAN_PLACEHOLDER, y**0.5).zip_with(y != NULL_PLACEHOLDER, nulls) result = x.is_close(y, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal) - if constructor_eager in NON_NULLABLE_CONSTRUCTORS: + if constructor_eager.name.is_non_nullable: expected = [v if v is not None else nans_equal for v in expected] elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): expected = [ @@ -154,7 +142,7 @@ def test_is_close_series_with_scalar( y = y.zip_with(y != NAN_PLACEHOLDER, y**0.5).zip_with(y != NULL_PLACEHOLDER, nulls) result = y.is_close(other, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal) - if constructor_eager in NON_NULLABLE_CONSTRUCTORS: + if constructor_eager.name.is_non_nullable: expected = [v if v is not None else False for v in expected] elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): expected = [ @@ -199,7 +187,7 @@ def test_is_close_expr_with_expr( ) .sort("idx") ) - if constructor in NON_NULLABLE_CONSTRUCTORS: + if constructor.name.is_non_nullable: expected = [v if v is not None else nans_equal for v in expected] elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): expected = [ @@ -240,7 +228,7 @@ def test_is_close_expr_with_scalar( ) .sort("idx") ) - if constructor in NON_NULLABLE_CONSTRUCTORS: + if constructor.name.is_non_nullable: expected = [v if v is not None else False for v in expected] elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): expected = [ diff --git a/tests/expr_and_series/is_finite_test.py b/tests/expr_and_series/is_finite_test.py index eb07b2a41e..d9c3d68965 100644 --- a/tests/expr_and_series/is_finite_test.py +++ b/tests/expr_and_series/is_finite_test.py @@ -5,21 +5,8 @@ import pytest import narwhals as nw -from tests.conftest import ( - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, - pandas_constructor, -) from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data -NON_NULLABLE_CONSTRUCTORS = [ - pandas_constructor, - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, -] - data = {"a": [float("nan"), float("inf"), 2.0, None]} @@ -77,7 +64,7 @@ def test_is_finite_column_with_null(constructor: Constructor, data: list[float]) result = df.select(nw.col("a").is_finite()) expected: dict[str, list[Any]] - if any(constructor is c for c in NON_NULLABLE_CONSTRUCTORS): + if constructor.name.is_non_nullable: # Null values are coerced to NaN for non-nullable datatypes expected = {"a": [True, True, False]} else: diff --git a/tests/expr_and_series/is_nan_test.py b/tests/expr_and_series/is_nan_test.py index 27790e27b2..632710ae39 100644 --- a/tests/expr_and_series/is_nan_test.py +++ b/tests/expr_and_series/is_nan_test.py @@ -5,21 +5,8 @@ import pytest import narwhals as nw -from tests.conftest import ( - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, - pandas_constructor, -) from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data -NON_NULLABLE_CONSTRUCTORS = [ - pandas_constructor, - dask_lazy_p1_constructor, - dask_lazy_p2_constructor, - modin_constructor, -] - def test_nan(constructor: Constructor) -> None: data_na = {"int": [-1, 1, None]} @@ -33,7 +20,7 @@ def test_nan(constructor: Constructor) -> None: ) expected: dict[str, list[Any]] - if any(constructor is c for c in NON_NULLABLE_CONSTRUCTORS): + if constructor.name.is_non_nullable: # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], @@ -70,7 +57,7 @@ def test_nan_series(constructor_eager: ConstructorEager) -> None: "float_na": df["float_na"].is_nan(), } expected: dict[str, list[Any]] - if any(constructor_eager is c for c in NON_NULLABLE_CONSTRUCTORS): + if constructor_eager.name.is_non_nullable: # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], diff --git a/tests/frame/lazy_test.py b/tests/frame/lazy_test.py index 9e671c68d2..658a61c68b 100644 --- a/tests/frame/lazy_test.py +++ b/tests/frame/lazy_test.py @@ -9,13 +9,8 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.dependencies import get_cudf, get_modin -from tests.utils import ( - DUCKDB_VERSION, - PANDAS_VERSION, - assert_equal_data, - pyspark_session, - sqlframe_session, -) +from narwhals.testing.constructors import pyspark_session, sqlframe_session +from tests.utils import DUCKDB_VERSION, PANDAS_VERSION, assert_equal_data if TYPE_CHECKING: from narwhals._typing import LazyAllowed, SparkLike diff --git a/tests/hypothesis/getitem_test.py b/tests/hypothesis/getitem_test.py index 759a292f97..86cf9281d4 100644 --- a/tests/hypothesis/getitem_test.py +++ b/tests/hypothesis/getitem_test.py @@ -7,7 +7,7 @@ from hypothesis import assume, given import narwhals as nw -from tests.conftest import pandas_constructor, pyarrow_table_constructor +from narwhals.testing.constructors._classes import PandasConstructor, PyArrowConstructor from tests.utils import assert_equal_data if TYPE_CHECKING: @@ -20,7 +20,7 @@ import polars as pl -@pytest.fixture(params=[pandas_constructor, pyarrow_table_constructor], scope="module") +@pytest.fixture(params=[PandasConstructor(), PyArrowConstructor()], scope="module") def pandas_or_pyarrow_constructor( request: pytest.FixtureRequest, ) -> Callable[[Any], IntoDataFrame]: @@ -125,7 +125,7 @@ def test_getitem(pandas_or_pyarrow_constructor: Any, selector: Any) -> None: # NotImplementedError: Slicing with step is not supported on PyArrow tables assume( not ( - pandas_or_pyarrow_constructor is pyarrow_table_constructor + pandas_or_pyarrow_constructor is PyArrowConstructor() and isinstance(selector, slice) and selector.step is not None ) @@ -134,7 +134,7 @@ def test_getitem(pandas_or_pyarrow_constructor: Any, selector: Any) -> None: # NotImplementedError: Slicing with step is not supported on PyArrow tables assume( not ( - pandas_or_pyarrow_constructor is pyarrow_table_constructor + pandas_or_pyarrow_constructor is PyArrowConstructor() and isinstance(selector, tuple) and ( (isinstance(selector[0], slice) and selector[0].step is not None) diff --git a/tests/read_scan_test.py b/tests/read_scan_test.py index 4548f76a87..aeee5229d7 100644 --- a/tests/read_scan_test.py +++ b/tests/read_scan_test.py @@ -6,13 +6,8 @@ import pytest import narwhals as nw -from tests.utils import ( - PANDAS_VERSION, - Constructor, - assert_equal_data, - pyspark_session, - sqlframe_session, -) +from narwhals.testing.constructors import pyspark_session, sqlframe_session +from tests.utils import PANDAS_VERSION, Constructor, assert_equal_data pytest.importorskip("polars") pytest.importorskip("pyarrow") diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 8d076699c0..f5b8d635d4 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -30,7 +30,7 @@ import narwhals as nw from narwhals._utils import Version -from tests.conftest import sqlframe_pyspark_lazy_constructor +from narwhals.testing.constructors._classes import SQLFrameConstructor from tests.utils import Constructor, maybe_get_modin_df if TYPE_CHECKING: @@ -294,7 +294,7 @@ def test_eager_only_lazy_dask(eager_only: Any, context: Any) -> None: def test_series_only_sqlframe() -> None: # pragma: no cover pytest.importorskip("sqlframe") - df = sqlframe_pyspark_lazy_constructor(data) + df = SQLFrameConstructor()(data) with pytest.raises(TypeError, match="Cannot only use `series_only`"): nw.from_native(df, series_only=True) # pyright: ignore[reportArgumentType, reportCallIssue] @@ -315,7 +315,7 @@ def test_series_only_sqlframe() -> None: # pragma: no cover ) def test_eager_only_sqlframe(eager_only: Any, context: Any) -> None: # pragma: no cover pytest.importorskip("sqlframe") - df = sqlframe_pyspark_lazy_constructor(data) + df = SQLFrameConstructor()(data) with context: res = nw.from_native(df, eager_only=eager_only) From af31a603be6da8318bb3a79e2e8ebc89bf188179 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 16:45:22 +0200 Subject: [PATCH 21/71] try inline && --- .github/workflows/extremes.yml | 12 ++++-------- .github/workflows/pytest-pyspark.yml | 6 ++---- .github/workflows/pytest.yml | 15 +++++---------- .github/workflows/random_ci_pytest.yml | 3 +-- 4 files changed, 12 insertions(+), 24 deletions(-) diff --git a/.github/workflows/extremes.yml b/.github/workflows/extremes.yml index edbd0ddb2d..27f2dc276c 100644 --- a/.github/workflows/extremes.yml +++ b/.github/workflows/extremes.yml @@ -45,8 +45,7 @@ jobs: echo "$DEPS" | grep 'duckdb==1.1' - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb \ - && coverage report + coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb && coverage report pretty_old_versions: strategy: @@ -85,8 +84,7 @@ jobs: echo "$DEPS" | grep 'duckdb==1.2' - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb \ - && coverage report + coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb && coverage report not_so_old_versions: strategy: @@ -124,8 +122,7 @@ jobs: echo "$DEPS" | grep 'duckdb==1.3' - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb \ - && coverage report + coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb && coverage report nightlies: strategy: @@ -181,5 +178,4 @@ jobs: echo "$DEPS" | grep 'dask.*@' - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb \ - && coverage report + coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb && coverage report diff --git a/.github/workflows/pytest-pyspark.yml b/.github/workflows/pytest-pyspark.yml index 341ff1eb50..c10b32fd6c 100644 --- a/.github/workflows/pytest-pyspark.yml +++ b/.github/workflows/pytest-pyspark.yml @@ -41,8 +41,7 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors pyspark \ - && coverage report + coverage run -m pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors pyspark && coverage report pytest-pyspark-min-version-constructor: strategy: @@ -135,8 +134,7 @@ jobs: - name: Run pytest run: | - coverage run -m pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors "pyspark[connect]" \ - && coverage report + coverage run -m pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors "pyspark[connect]" && coverage report - name: Stop Spark Connect server if: always() diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index d87b1874a1..c1bf8fd9b2 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -31,8 +31,7 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] \ - && coverage report + coverage run -m pytest tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] && coverage report - name: install-test-plugin run: uv pip install -e test-plugin/. @@ -62,8 +61,7 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --cov-fail-under=95 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 \ - && coverage report + coverage run -m pytest tests --runslow --cov-fail-under=95 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 && coverage report pytest-full-coverage: strategy: @@ -95,8 +93,7 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=100 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 \ - && coverage report + coverage run -m pytest tests --cov-fail-under=100 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 && coverage report - name: Run doctests # reprs differ between versions, so we only run doctests on the latest Python if: matrix.python-version == '3.13' @@ -163,8 +160,7 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --cov-fail-under=50 \ - && coverage report + coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --cov-fail-under=50 && coverage report python-314t: strategy: @@ -191,5 +187,4 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow --cov-fail-under=50 \ - && coverage report + coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow --cov-fail-under=50 && coverage report diff --git a/.github/workflows/random_ci_pytest.yml b/.github/workflows/random_ci_pytest.yml index d8ecc212df..38acf60bc9 100644 --- a/.github/workflows/random_ci_pytest.yml +++ b/.github/workflows/random_ci_pytest.yml @@ -36,5 +36,4 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] \ - && coverage report + coverage run -m pytest tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] && coverage report From 06be3005bde76de3c5e334e29902731bc4b803f4 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 16:49:17 +0200 Subject: [PATCH 22/71] c.__name__ -> str(c) --- tests/expr_and_series/list/get_test.py | 5 ++--- tests/expr_and_series/str/split_test.py | 7 +++---- tests/frame/schema_test.py | 4 ++-- tests/frame/to_pandas_test.py | 2 +- tests/modern_polars/method_chaining_test.py | 5 +---- tests/translate/get_native_namespace_test.py | 2 +- 6 files changed, 10 insertions(+), 15 deletions(-) diff --git a/tests/expr_and_series/list/get_test.py b/tests/expr_and_series/list/get_test.py index 52ca3386ba..338ab6197a 100644 --- a/tests/expr_and_series/list/get_test.py +++ b/tests/expr_and_series/list/get_test.py @@ -45,9 +45,8 @@ def test_get_series( pytest.skip() pytest.importorskip("pyarrow") - if ( - constructor_eager.__name__.startswith("pandas") - and "pyarrow" not in constructor_eager.__name__ + if str(constructor_eager).startswith("pandas") and "pyarrow" not in str( + constructor_eager ): df = nw.from_native(constructor_eager(data), eager_only=True) msg = re.escape("Series must be of PyArrow List type to support list namespace.") diff --git a/tests/expr_and_series/str/split_test.py b/tests/expr_and_series/str/split_test.py index b6b25cd024..f206b84e35 100644 --- a/tests/expr_and_series/str/split_test.py +++ b/tests/expr_and_series/str/split_test.py @@ -20,8 +20,7 @@ ) def test_str_split(constructor: Constructor, by: str, expected: Any) -> None: if "cudf" not in str(constructor) and ( - constructor.__name__.startswith("pandas") - and "pyarrow" not in constructor.__name__ + str(constructor).startswith("pandas") and "pyarrow" not in str(constructor) ): df = nw.from_native(constructor(data)) msg = re.escape("This operation requires a pyarrow-backed series. ") @@ -44,8 +43,8 @@ def test_str_split_series( constructor_eager: ConstructorEager, by: str, expected: Any ) -> None: if "cudf" not in str(constructor_eager) and ( - constructor_eager.__name__.startswith("pandas") - and "pyarrow" not in constructor_eager.__name__ + str(constructor_eager).startswith("pandas") + and "pyarrow" not in str(constructor_eager) ): df = nw.from_native(constructor_eager(data), eager_only=True) msg = re.escape("This operation requires a pyarrow-backed series. ") diff --git a/tests/frame/schema_test.py b/tests/frame/schema_test.py index d90916b029..93779cd129 100644 --- a/tests/frame/schema_test.py +++ b/tests/frame/schema_test.py @@ -588,8 +588,8 @@ def origin_pandas_like_pyarrow( if PANDAS_VERSION < (1, 5): pytest.skip(reason="pandas too old for `pyarrow`") name_pandas_like = {"pandas_pyarrow_constructor", "modin_pyarrow_constructor"} - if constructor_pandas_like.__name__ not in name_pandas_like: - pytest.skip(f"{constructor_pandas_like.__name__!r} is not pandas_like_pyarrow") + if str(constructor_pandas_like) not in name_pandas_like: + pytest.skip(f"{constructor_pandas_like!s} is not pandas_like_pyarrow") data = { "a": [2, 1], "b": ["hello", "hi"], diff --git a/tests/frame/to_pandas_test.py b/tests/frame/to_pandas_test.py index 473b685c19..b74c9a98b1 100644 --- a/tests/frame/to_pandas_test.py +++ b/tests/frame/to_pandas_test.py @@ -22,7 +22,7 @@ def test_convert_pandas(constructor_eager: ConstructorEager) -> None: df_raw = constructor_eager(data) result = nw.from_native(df_raw, eager_only=True).to_pandas() - if constructor_eager.__name__.startswith("pandas"): + if str(constructor_eager).startswith("pandas"): expected = cast("pd.DataFrame", constructor_eager(data)) elif "modin_pyarrow" in str(constructor_eager): expected = pd.DataFrame(data).convert_dtypes(dtype_backend="pyarrow") diff --git a/tests/modern_polars/method_chaining_test.py b/tests/modern_polars/method_chaining_test.py index 611f85973e..ba7a06b894 100644 --- a/tests/modern_polars/method_chaining_test.py +++ b/tests/modern_polars/method_chaining_test.py @@ -38,10 +38,7 @@ def test_split_list_get(request: pytest.FixtureRequest, constructor: Constructor if PANDAS_VERSION < (2, 2): pytest.skip() pytest.importorskip("pyarrow") - if ( - constructor.__name__.startswith("pandas") - and "pyarrow" not in constructor.__name__ - ): + if str(constructor).startswith("pandas") and "pyarrow" not in str(constructor): df = nw.from_native(constructor(data)) msg = re.escape("This operation requires a pyarrow-backed series. ") with pytest.raises(TypeError, match=msg): diff --git a/tests/translate/get_native_namespace_test.py b/tests/translate/get_native_namespace_test.py index 821443ea64..5a15069ed2 100644 --- a/tests/translate/get_native_namespace_test.py +++ b/tests/translate/get_native_namespace_test.py @@ -76,7 +76,7 @@ def test_native_namespace_frame(constructor: Constructor) -> None: def test_native_namespace_series(constructor_eager: ConstructorEager) -> None: - constructor_name = constructor_eager.__name__ + constructor_name = str(constructor_eager) expected_namespace = _get_expected_namespace(constructor_name=constructor_name) From e9b94bb380cdf3b8246b85c2191049fc81b6d9df Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 17:13:03 +0200 Subject: [PATCH 23/71] try with fail-under flag under report --- .github/workflows/extremes.yml | 12 ++++++++---- .github/workflows/pytest-pyspark.yml | 6 ++++-- .github/workflows/pytest.yml | 15 ++++++++++----- .github/workflows/random_ci_pytest.yml | 3 ++- Makefile | 4 ++-- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.github/workflows/extremes.yml b/.github/workflows/extremes.yml index 27f2dc276c..e51b8ae787 100644 --- a/.github/workflows/extremes.yml +++ b/.github/workflows/extremes.yml @@ -45,7 +45,8 @@ jobs: echo "$DEPS" | grep 'duckdb==1.1' - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb && coverage report + coverage run -m pytest tests --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb + coverage report --fail-under=50 pretty_old_versions: strategy: @@ -84,7 +85,8 @@ jobs: echo "$DEPS" | grep 'duckdb==1.2' - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb && coverage report + coverage run -m pytest tests --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb + coverage report --fail-under=50 not_so_old_versions: strategy: @@ -122,7 +124,8 @@ jobs: echo "$DEPS" | grep 'duckdb==1.3' - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb && coverage report + coverage run -m pytest tests --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb + coverage report --fail-under=50 nightlies: strategy: @@ -178,4 +181,5 @@ jobs: echo "$DEPS" | grep 'dask.*@' - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=50 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb && coverage report + coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb + coverage report --fail-under=50 diff --git a/.github/workflows/pytest-pyspark.yml b/.github/workflows/pytest-pyspark.yml index c10b32fd6c..7d0b636dc6 100644 --- a/.github/workflows/pytest-pyspark.yml +++ b/.github/workflows/pytest-pyspark.yml @@ -41,7 +41,8 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors pyspark && coverage report + coverage run -m pytest tests --cov=narwhals/_spark_like --runslow --constructors pyspark + coverage report --fail-under=95 pytest-pyspark-min-version-constructor: strategy: @@ -134,7 +135,8 @@ jobs: - name: Run pytest run: | - coverage run -m pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors "pyspark[connect]" && coverage report + coverage run -m pytest tests --cov=narwhals/_spark_like --runslow --constructors "pyspark[connect]" + coverage report --fail-under=95 - name: Stop Spark Connect server if: always() diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c1bf8fd9b2..39f59dbcd2 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -31,7 +31,8 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] && coverage report + coverage run -m pytest tests --constructors=pandas,pyarrow,polars[eager],polars[lazy] + coverage report --fail-under=75 - name: install-test-plugin run: uv pip install -e test-plugin/. @@ -61,7 +62,8 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --cov-fail-under=95 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 && coverage report + coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 + coverage report --fail-under=95 pytest-full-coverage: strategy: @@ -93,7 +95,8 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=100 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 && coverage report + coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 + coverage report --fail-under=100 - name: Run doctests # reprs differ between versions, so we only run doctests on the latest Python if: matrix.python-version == '3.13' @@ -160,7 +163,8 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --cov-fail-under=50 && coverage report + coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe + coverage report --fail-under=50 python-314t: strategy: @@ -187,4 +191,5 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow --cov-fail-under=50 && coverage report + coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow + coverage report --fail-under=50 diff --git a/.github/workflows/random_ci_pytest.yml b/.github/workflows/random_ci_pytest.yml index 38acf60bc9..e9fc21cc61 100644 --- a/.github/workflows/random_ci_pytest.yml +++ b/.github/workflows/random_ci_pytest.yml @@ -36,4 +36,5 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] && coverage report + coverage run -m pytest tests --constructors=pandas,pyarrow,polars[eager],polars[lazy] + coverage report --fail-under=75 diff --git a/Makefile b/Makefile index 9d353c8ae1..824f91f746 100644 --- a/Makefile +++ b/Makefile @@ -47,5 +47,5 @@ test: ## Run unittest --editable .[ibis,modin,pyspark] \ --group core \ --group tests - $(VENV_BIN)/uv run --no-sync coverage run -m pytest tests --cov-fail-under=95 --all-cpu-constructors - $(VENV_BIN)/uv run --no-sync coverage report + $(VENV_BIN)/uv run --no-sync coverage run -m pytest tests --all-cpu-constructors + $(VENV_BIN)/uv run --no-sync coverage report --fail-under=95 From 2ac3da9cc9aca6705e5d85112419de679594ead1 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 17:22:07 +0200 Subject: [PATCH 24/71] try no pytest-cov --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 024538f384..99b7211ba0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,6 @@ core = [ tests = [ "covdefaults", "pytest", - "pytest-cov", "pytest-env", "pytest-randomly", "pytest-xdist", From 0e313dec1e55faa772573720c4677bb11505bc10 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 17:28:31 +0200 Subject: [PATCH 25/71] fixup command --- .github/workflows/pytest-pyspark.yml | 8 ++++---- CONTRIBUTING.md | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pytest-pyspark.yml b/.github/workflows/pytest-pyspark.yml index 7d0b636dc6..7dd4c3517f 100644 --- a/.github/workflows/pytest-pyspark.yml +++ b/.github/workflows/pytest-pyspark.yml @@ -41,8 +41,8 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --cov=narwhals/_spark_like --runslow --constructors pyspark - coverage report --fail-under=95 + coverage run -m pytest tests --runslow --constructors pyspark + coverage report --fail-under=95 --include "narwhals/_spark_like/*" pytest-pyspark-min-version-constructor: strategy: @@ -135,8 +135,8 @@ jobs: - name: Run pytest run: | - coverage run -m pytest tests --cov=narwhals/_spark_like --runslow --constructors "pyspark[connect]" - coverage report --fail-under=95 + coverage run -m pytest tests --runslow --constructors "pyspark[connect]" + coverage report --fail-under=95 --include="narwhals/_spark_like/*" - name: Stop Spark Connect server if: always() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 98a274e4ed..ec02c937cf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -147,9 +147,9 @@ If you add code that should be tested, please add tests. ### 7. Running tests -- To run tests, run `pytest`. To check coverage: `pytest --cov=narwhals` +- To run tests, run `pytest`. To check coverage: `make test` - To run tests on the doctests, use `pytest narwhals --doctest-modules` -- To run unit tests and doctests at the same time, run `pytest tests narwhals --cov=narwhals --doctest-modules` +- To run unit tests and doctests at the same time, run `pytest tests narwhals --doctest-modules` - To run tests multiprocessed, you may also want to use [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) (optional) - To choose which backends to run tests with you, you can use the `--constructors` flag: - To only run tests for pandas, Polars, and PyArrow, use `pytest --constructors=pandas,pyarrow,polars` From 6e09bfc6ea18db3eacb9e2190868dac2940ec7a1 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 21:29:40 +0200 Subject: [PATCH 26/71] fixup pyarrow getitem --- tests/hypothesis/getitem_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/hypothesis/getitem_test.py b/tests/hypothesis/getitem_test.py index 86cf9281d4..a85f7d57f8 100644 --- a/tests/hypothesis/getitem_test.py +++ b/tests/hypothesis/getitem_test.py @@ -13,6 +13,7 @@ if TYPE_CHECKING: from collections.abc import Sequence + from narwhals.testing.typing import ConstructorBase from narwhals.typing import IntoDataFrame pytest.importorskip("pandas") @@ -117,7 +118,7 @@ def tuple_selector(draw: st.DrawFn) -> tuple[Any, Any]: @given(selector=st.one_of(single_selector, tuple_selector())) @pytest.mark.slow -def test_getitem(pandas_or_pyarrow_constructor: Any, selector: Any) -> None: +def test_getitem(pandas_or_pyarrow_constructor: ConstructorBase, selector: Any) -> None: """Compare __getitem__ against polars.""" # TODO(PR - clean up): documenting current differences # These assume(...) lines each filter out a known difference. @@ -125,7 +126,7 @@ def test_getitem(pandas_or_pyarrow_constructor: Any, selector: Any) -> None: # NotImplementedError: Slicing with step is not supported on PyArrow tables assume( not ( - pandas_or_pyarrow_constructor is PyArrowConstructor() + pandas_or_pyarrow_constructor.name.is_pyarrow and isinstance(selector, slice) and selector.step is not None ) @@ -134,7 +135,7 @@ def test_getitem(pandas_or_pyarrow_constructor: Any, selector: Any) -> None: # NotImplementedError: Slicing with step is not supported on PyArrow tables assume( not ( - pandas_or_pyarrow_constructor is PyArrowConstructor() + pandas_or_pyarrow_constructor.name.is_pyarrow and isinstance(selector, tuple) and ( (isinstance(selector[0], slice) and selector[0].step is not None) From 5b5f507b7e039f21f20fba9a941713ceca8a7093 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 16 Apr 2026 21:52:17 +0200 Subject: [PATCH 27/71] back to pytest-cov --- .github/workflows/extremes.yml | 17 ++++++----------- .github/workflows/pytest-ibis.yml | 1 + .github/workflows/pytest-modin.yml | 1 + .github/workflows/pytest-pyspark.yml | 10 ++++------ .github/workflows/pytest.yml | 20 ++++++-------------- .github/workflows/random_ci_pytest.yml | 5 +++-- CONTRIBUTING.md | 4 ++-- pyproject.toml | 2 ++ 8 files changed, 25 insertions(+), 35 deletions(-) diff --git a/.github/workflows/extremes.yml b/.github/workflows/extremes.yml index e51b8ae787..296ce0efaf 100644 --- a/.github/workflows/extremes.yml +++ b/.github/workflows/extremes.yml @@ -6,6 +6,7 @@ on: env: PY_COLORS: 1 PYTEST_ADDOPTS: "--numprocesses=logical" + COVERAGE_PROCESS_START: pyproject.toml UV_SYSTEM_PYTHON: 1 jobs: minimum_versions: @@ -44,9 +45,7 @@ jobs: echo "$DEPS" | grep 'scikit-learn==1.1.0' echo "$DEPS" | grep 'duckdb==1.1' - name: Run pytest - run: | - coverage run -m pytest tests --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb - coverage report --fail-under=50 + run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb pretty_old_versions: strategy: @@ -84,9 +83,7 @@ jobs: echo "$DEPS" | grep 'scikit-learn==1.1.0' echo "$DEPS" | grep 'duckdb==1.2' - name: Run pytest - run: | - coverage run -m pytest tests --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb - coverage report --fail-under=50 + run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb not_so_old_versions: strategy: @@ -123,9 +120,7 @@ jobs: echo "$DEPS" | grep 'dask==2024.10' echo "$DEPS" | grep 'duckdb==1.3' - name: Run pytest - run: | - coverage run -m pytest tests --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb - coverage report --fail-under=50 + run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb nightlies: strategy: @@ -181,5 +176,5 @@ jobs: echo "$DEPS" | grep 'dask.*@' - name: Run pytest run: | - coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb - coverage report --fail-under=50 + pytest tests --cov=narwhals --cov=tests --cov-fail-under=50 --runslow \ + --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb diff --git a/.github/workflows/pytest-ibis.yml b/.github/workflows/pytest-ibis.yml index 2910a597f0..5672b5aa06 100644 --- a/.github/workflows/pytest-ibis.yml +++ b/.github/workflows/pytest-ibis.yml @@ -6,6 +6,7 @@ on: env: PY_COLORS: 1 PYTEST_ADDOPTS: "--numprocesses=logical" + COVERAGE_PROCESS_START: pyproject.toml UV_SYSTEM_PYTHON: 1 jobs: diff --git a/.github/workflows/pytest-modin.yml b/.github/workflows/pytest-modin.yml index ff717b2e32..2a09e2bd2c 100644 --- a/.github/workflows/pytest-modin.yml +++ b/.github/workflows/pytest-modin.yml @@ -6,6 +6,7 @@ on: env: PY_COLORS: 1 PYTEST_ADDOPTS: "--numprocesses=logical" + COVERAGE_PROCESS_START: pyproject.toml UV_SYSTEM_PYTHON: 1 jobs: diff --git a/.github/workflows/pytest-pyspark.yml b/.github/workflows/pytest-pyspark.yml index 7dd4c3517f..0c003b8929 100644 --- a/.github/workflows/pytest-pyspark.yml +++ b/.github/workflows/pytest-pyspark.yml @@ -14,6 +14,7 @@ on: env: PY_COLORS: 1 PYTEST_ADDOPTS: "--numprocesses=logical" + COVERAGE_PROCESS_START: pyproject.toml UV_SYSTEM_PYTHON: 1 jobs: pytest-pyspark-constructor: @@ -40,9 +41,8 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: | - coverage run -m pytest tests --runslow --constructors pyspark - coverage report --fail-under=95 --include "narwhals/_spark_like/*" + run: pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors pyspark + pytest-pyspark-min-version-constructor: strategy: @@ -134,9 +134,7 @@ jobs: echo "Spark Connect server started" - name: Run pytest - run: | - coverage run -m pytest tests --runslow --constructors "pyspark[connect]" - coverage report --fail-under=95 --include="narwhals/_spark_like/*" + run: pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors "pyspark[connect]" - name: Stop Spark Connect server if: always() diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 39f59dbcd2..683f9ecfe7 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -6,6 +6,7 @@ on: env: PY_COLORS: 1 PYTEST_ADDOPTS: "--numprocesses=logical" + COVERAGE_PROCESS_START: pyproject.toml UV_SYSTEM_PYTHON: 1 jobs: pytest-39: @@ -30,9 +31,7 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: | - coverage run -m pytest tests --constructors=pandas,pyarrow,polars[eager],polars[lazy] - coverage report --fail-under=75 + run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] - name: install-test-plugin run: uv pip install -e test-plugin/. @@ -62,8 +61,7 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 - coverage report --fail-under=95 + pytest tests --cov=narwhals --cov=tests --runslow --cov-fail-under=95 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 pytest-full-coverage: strategy: @@ -94,9 +92,7 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: | - coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 - coverage report --fail-under=100 + run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=100 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 - name: Run doctests # reprs differ between versions, so we only run doctests on the latest Python if: matrix.python-version == '3.13' @@ -162,9 +158,7 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: | - coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe - coverage report --fail-under=50 + run: pytest tests --cov=narwhals --cov=tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --cov-fail-under=50 python-314t: strategy: @@ -190,6 +184,4 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: | - coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow - coverage report --fail-under=50 + run: pytest tests --cov=narwhals --cov=tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow --cov-fail-under=50 diff --git a/.github/workflows/random_ci_pytest.yml b/.github/workflows/random_ci_pytest.yml index e9fc21cc61..900985c9cc 100644 --- a/.github/workflows/random_ci_pytest.yml +++ b/.github/workflows/random_ci_pytest.yml @@ -6,6 +6,7 @@ on: env: PY_COLORS: 1 PYTEST_ADDOPTS: "--numprocesses=logical" + COVERAGE_PROCESS_START: pyproject.toml jobs: tox: @@ -36,5 +37,5 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --constructors=pandas,pyarrow,polars[eager],polars[lazy] - coverage report --fail-under=75 + pytest tests --cov=narwhals --cov=tests --cov-fail-under=75 \ + --constructors=pandas,pyarrow,polars[eager],polars[lazy] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec02c937cf..98a274e4ed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -147,9 +147,9 @@ If you add code that should be tested, please add tests. ### 7. Running tests -- To run tests, run `pytest`. To check coverage: `make test` +- To run tests, run `pytest`. To check coverage: `pytest --cov=narwhals` - To run tests on the doctests, use `pytest narwhals --doctest-modules` -- To run unit tests and doctests at the same time, run `pytest tests narwhals --doctest-modules` +- To run unit tests and doctests at the same time, run `pytest tests narwhals --cov=narwhals --doctest-modules` - To run tests multiprocessed, you may also want to use [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) (optional) - To choose which backends to run tests with you, you can use the `--constructors` flag: - To only run tests for pandas, Polars, and PyArrow, use `pytest --constructors=pandas,pyarrow,polars` diff --git a/pyproject.toml b/pyproject.toml index 99b7211ba0..df9fefe90e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ core = [ tests = [ "covdefaults", "pytest", + "pytest-cov", "pytest-env", "pytest-randomly", "pytest-xdist", @@ -304,6 +305,7 @@ env = [ [tool.coverage.run] plugins = ["covdefaults"] source = ["narwhals", "tests"] +parallel = true [tool.coverage.report] fail_under = 80 # This is just for local development, in CI we set it to 100 From 11a81838fab2ac93316f4f287d3e3e1a7c9492f1 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 17 Apr 2026 22:15:21 +0200 Subject: [PATCH 28/71] Act 1 --- narwhals/testing/constructors/_classes.py | 193 +++++++++++++++------- narwhals/testing/constructors/_name.py | 100 +++-------- tests/testing/constructors_test.py | 33 +++- 3 files changed, 185 insertions(+), 141 deletions(-) diff --git a/narwhals/testing/constructors/_classes.py b/narwhals/testing/constructors/_classes.py index fc8ccc2ba3..eef27e1e92 100644 --- a/narwhals/testing/constructors/_classes.py +++ b/narwhals/testing/constructors/_classes.py @@ -3,6 +3,10 @@ Each constructor wraps one backend library (pandas, Polars, DuckDB, ...) and knows how to turn a column-oriented `dict` into a native frame. +All static metadata for a backend lives on its constructor class, colocated +with the `__call__` implementation. Adding a constructor is a one-step +declaration — the `__init_subclass__` hook then auto-registers a singleton. + ## Adding a new constructor 1. Choose the right base class: @@ -10,18 +14,15 @@ * `ConstructorEagerBase`: if the backend returns an eager dataframe. * `ConstructorLazyBase`: if the backend returns a lazy frame. -2. Add a member to `ConstructorName` in `_name.py` and register the corresponding - `Implementation` mapping in `_NAME_TO_IMPL`. - -3. Define the class in this module. Specify: +2. Add a member to `ConstructorName` in `_name.py`. - * `requirements`: the packages that `importlib.util.find_spec` should check - * `legacy_name` (if relevant): the old `str(constructor)` value used in existing - tests as keyword arguments in the class header: +3. Define the class in this module and declare its metadata in the class + header as keyword arguments: ```py class MyBackendConstructor( ConstructorLazyBase, + implementation=Implementation.MY_BACKEND, requirements=("my_backend",), legacy_name="my_backend_lazy_constructor", ): @@ -33,8 +34,8 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ...: return my_backend.from_dict(obj) ``` -That is all. `__init_subclass__` on `ConstructorBase` will automatically register -a default singleton into `_registry`, record the *requirements*, and store the *legacy_name*. +That is all. `__init_subclass__` on `ConstructorBase` automatically registers +a default singleton into `_registry`. """ from __future__ import annotations @@ -46,8 +47,8 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ...: from functools import lru_cache from typing import TYPE_CHECKING, Any, ClassVar, Protocol, cast -from narwhals._utils import generate_temporary_column_name -from narwhals.testing.constructors._name import ConstructorName +from narwhals._utils import Implementation, generate_temporary_column_name +from narwhals.testing.constructors._name import ConstructorName, is_backend_available if TYPE_CHECKING: import ibis @@ -122,38 +123,62 @@ class ConstructorBase(Protocol): [`Data`][narwhals.testing.typing.Data]) into a native dataframe / lazy frame, plus a typed [`ConstructorName`][] that identifies the backend. - Subclasses are automatically registered when they set ``name`` as a - class variable and pass ``requirements`` / ``legacy_name`` as - keyword arguments in the class definition. + Subclasses declare their backend metadata (implementation, requirements, + legacy name, nullability, GPU need) as keyword arguments in the class + header. `__init_subclass__` stores those on the class and registers a + default singleton into `_registry`. """ _registry: ClassVar[dict[ConstructorName, ConstructorBase]] = {} - _requirements: ClassVar[dict[ConstructorName, tuple[str, ...]]] = {} - _legacy_names: ClassVar[dict[ConstructorName, str]] = {} name: ClassVar[ConstructorName] + implementation: ClassVar[Implementation] + requirements: ClassVar[tuple[str, ...]] = () + legacy_name: ClassVar[str] = "" + is_eager: ClassVar[bool] = False + is_non_nullable: ClassVar[bool] = False + needs_gpu: ClassVar[bool] = False def __init_subclass__( - cls, *, requirements: tuple[str, ...] = (), legacy_name: str = "", **kwargs: Any + cls, + *, + implementation: Implementation | None = None, + requirements: tuple[str, ...] = (), + legacy_name: str = "", + is_non_nullable: bool = False, + needs_gpu: bool = False, + **kwargs: Any, ) -> None: """Register concrete subclasses automatically. Arguments: - requirements: Package names that must be importable for - this constructor to be available (checked via - ``importlib.util.find_spec``). - legacy_name: Value returned by ``str(constructor)`` for - backward compatibility with test assertions that - match on the old naming scheme. - **kwargs: Forwarded to ``super().__init_subclass__``. + implementation: The [`Implementation`][] this constructor belongs to. + requirements: Package names that must be importable for this constructor + to be available (checked via `importlib.util.find_spec`). + legacy_name: Value returned by `str(constructor)` for backward compatibility + with test assertions that match on the old naming scheme. + is_non_nullable: Whether the backend lacks native null support. + needs_gpu: Whether the backend requires GPU hardware. + **kwargs: Forwarded to `super().__init_subclass__`. """ super().__init_subclass__(**kwargs) + if implementation is not None: + cls.implementation = implementation + + cls.requirements = requirements + cls.legacy_name = legacy_name + cls.is_non_nullable = is_non_nullable + cls.needs_gpu = needs_gpu + if "name" not in cls.__dict__: return - instance = cls() - ConstructorBase._registry[cls.name] = instance - ConstructorBase._requirements[cls.name] = requirements - ConstructorBase._legacy_names[cls.name] = legacy_name + if not hasattr(cls, "implementation"): + msg = ( + f"Constructor {cls.__name__} is missing `implementation` " + "kwarg in its class header." + ) + raise TypeError(msg) + ConstructorBase._registry[cls.name] = cls() def __call__(self, obj: Data, /, **kwds: Any) -> IntoFrame: """Build a native frame from `obj`.""" @@ -164,12 +189,26 @@ def identifier(self) -> str: """Instance-level string identifier for test IDs.""" return str(self.name) + @property + def is_lazy(self) -> bool: + """Whether this constructor produces a lazy native frame.""" + return not self.is_eager + + @property + def needs_pyarrow(self) -> bool: + """Whether this constructor requires `pyarrow` to be installed.""" + return "pyarrow" in self.requirements + + @property + def is_available(self) -> bool: + """Whether every package this constructor needs is importable.""" + return is_backend_available(*self.requirements) + def __str__(self) -> str: # NOTE: This is a temporary hack - # TODO(Unassigned): Remove once all the - # `"backend" in str(constructor)` statements in the - # test suite are properly replaced - return _LEGACY_NAME[self.name] + # TODO(Unassigned): Remove once all the `"backend" in str(constructor)` + # statements in the test suite are properly replaced + return self.legacy_name def __repr__(self) -> str: return f"{type(self).__name__}()" @@ -186,12 +225,16 @@ def __eq__(self, other: object) -> bool: class ConstructorEagerBase(ConstructorBase): """A constructor that returns an *eager* native dataframe.""" + is_eager: ClassVar[bool] = True + def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: ... class ConstructorLazyBase(ConstructorBase): """A constructor that returns a *lazy* native frame.""" + is_eager: ClassVar[bool] = False + def __call__(self, obj: Data, /, **kwds: Any) -> IntoLazyFrame: ... @@ -199,9 +242,13 @@ def __call__(self, obj: Data, /, **kwds: Any) -> IntoLazyFrame: ... class PandasConstructor( - ConstructorEagerBase, requirements=("pandas",), legacy_name="pandas_constructor" + ConstructorEagerBase, + implementation=Implementation.PANDAS, + requirements=("pandas",), + legacy_name="pandas_constructor", + is_non_nullable=True, ): - """Constructor backed by ``pandas.DataFrame`` with default NumPy dtypes.""" + """Constructor backed by `pandas.DataFrame` with default NumPy dtypes.""" name = ConstructorName.PANDAS @@ -213,10 +260,11 @@ def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: class PandasNullableConstructor( ConstructorEagerBase, + implementation=Implementation.PANDAS, requirements=("pandas",), legacy_name="pandas_nullable_constructor", ): - """Constructor backed by ``pandas.DataFrame`` with ``numpy_nullable`` dtypes.""" + """Constructor backed by `pandas.DataFrame` with `numpy_nullable` dtypes.""" name = ConstructorName.PANDAS_NULLABLE @@ -228,10 +276,11 @@ def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: class PandasPyArrowConstructor( ConstructorEagerBase, + implementation=Implementation.PANDAS, requirements=("pandas", "pyarrow"), legacy_name="pandas_pyarrow_constructor", ): - """Constructor backed by ``pandas.DataFrame`` with ``pyarrow`` dtypes.""" + """Constructor backed by `pandas.DataFrame` with `pyarrow` dtypes.""" name = ConstructorName.PANDAS_PYARROW @@ -243,10 +292,11 @@ def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: class PyArrowConstructor( ConstructorEagerBase, + implementation=Implementation.PYARROW, requirements=("pyarrow",), legacy_name="pyarrow_table_constructor", ): - """Constructor backed by ``pyarrow.Table``.""" + """Constructor backed by `pyarrow.Table`.""" name = ConstructorName.PYARROW @@ -257,9 +307,13 @@ def __call__(self, obj: Data, /, **kwds: Any) -> pa.Table: class ModinConstructor( - ConstructorEagerBase, requirements=("modin",), legacy_name="modin_constructor" + ConstructorEagerBase, + implementation=Implementation.MODIN, + requirements=("modin",), + legacy_name="modin_constructor", + is_non_nullable=True, ): # pragma: no cover - """Constructor backed by ``modin.pandas.DataFrame``.""" + """Constructor backed by `modin.pandas.DataFrame`.""" name = ConstructorName.MODIN @@ -272,10 +326,11 @@ def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: class ModinPyArrowConstructor( ConstructorEagerBase, + implementation=Implementation.MODIN, requirements=("modin", "pyarrow"), legacy_name="modin_pyarrow_constructor", ): - """Constructor backed by ``modin.pandas.DataFrame`` with ``pyarrow`` dtypes.""" + """Constructor backed by `modin.pandas.DataFrame` with `pyarrow` dtypes.""" name = ConstructorName.MODIN_PYARROW @@ -290,9 +345,13 @@ def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: class CudfConstructor( - ConstructorEagerBase, requirements=("cudf",), legacy_name="cudf_constructor" + ConstructorEagerBase, + implementation=Implementation.CUDF, + requirements=("cudf",), + legacy_name="cudf_constructor", + needs_gpu=True, ): # pragma: no cover - """Constructor backed by ``cudf.DataFrame`` (requires GPU).""" + """Constructor backed by `cudf.DataFrame` (requires GPU).""" name = ConstructorName.CUDF @@ -303,9 +362,12 @@ def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: class PolarsEagerConstructor( - ConstructorEagerBase, requirements=("polars",), legacy_name="polars_eager_constructor" + ConstructorEagerBase, + implementation=Implementation.POLARS, + requirements=("polars",), + legacy_name="polars_eager_constructor", ): - """Constructor backed by ``polars.DataFrame``.""" + """Constructor backed by `polars.DataFrame`.""" name = ConstructorName.POLARS_EAGER @@ -319,9 +381,12 @@ def __call__(self, obj: Data, /, **kwds: Any) -> pl.DataFrame: class PolarsLazyConstructor( - ConstructorLazyBase, requirements=("polars",), legacy_name="polars_lazy_constructor" + ConstructorLazyBase, + implementation=Implementation.POLARS, + requirements=("polars",), + legacy_name="polars_lazy_constructor", ): - """Constructor backed by ``polars.LazyFrame``.""" + """Constructor backed by `polars.LazyFrame`.""" name = ConstructorName.POLARS_LAZY @@ -332,12 +397,16 @@ def __call__(self, obj: Data, /, **kwds: Any) -> pl.LazyFrame: class DaskConstructor( - ConstructorLazyBase, requirements=("dask",), legacy_name="dask_lazy_p2_constructor" + ConstructorLazyBase, + implementation=Implementation.DASK, + requirements=("dask",), + legacy_name="dask_lazy_p2_constructor", + is_non_nullable=True, ): # pragma: no cover - """Constructor backed by ``dask.dataframe``. + """Constructor backed by `dask.dataframe`. Arguments: - npartitions: Number of Dask partitions (default ``1``). + npartitions: Number of Dask partitions (default `1`). """ name = ConstructorName.DASK @@ -370,10 +439,11 @@ def __eq__(self, other: object) -> bool: class DuckDBConstructor( ConstructorLazyBase, + implementation=Implementation.DUCKDB, requirements=("duckdb", "pyarrow"), legacy_name="duckdb_lazy_constructor", ): - """Constructor backed by DuckDB (via ``duckdb.sql``).""" + """Constructor backed by DuckDB (via `duckdb.sql`).""" name = ConstructorName.DUCKDB @@ -387,9 +457,12 @@ def __call__(self, obj: Data, /, **kwds: Any) -> NativeDuckDB: class PySparkConstructor( - ConstructorLazyBase, requirements=("pyspark",), legacy_name="pyspark_lazy_constructor" + ConstructorLazyBase, + implementation=Implementation.PYSPARK, + requirements=("pyspark",), + legacy_name="pyspark_lazy_constructor", ): # pragma: no cover - """Constructor backed by ``pyspark.sql.DataFrame``.""" + """Constructor backed by `pyspark.sql.DataFrame`.""" name = ConstructorName.PYSPARK @@ -408,7 +481,10 @@ def __call__(self, obj: Data, /, **kwds: Any) -> NativePySpark: class PySparkConnectConstructor( - PySparkConstructor, requirements=("pyspark",), legacy_name="pyspark_lazy_constructor" + PySparkConstructor, + implementation=Implementation.PYSPARK_CONNECT, + requirements=("pyspark",), + legacy_name="pyspark_lazy_constructor", ): # pragma: no cover """Constructor backed by PySpark Connect (Spark Connect protocol).""" @@ -417,10 +493,11 @@ class PySparkConnectConstructor( class SQLFrameConstructor( ConstructorLazyBase, + implementation=Implementation.SQLFRAME, requirements=("sqlframe", "duckdb"), legacy_name="sqlframe_pyspark_lazy_constructor", ): - """Constructor backed by ``sqlframe`` (DuckDB session).""" + """Constructor backed by `sqlframe` (DuckDB session).""" name = ConstructorName.SQLFRAME @@ -433,10 +510,11 @@ def __call__(self, obj: Data, /, **kwds: Any) -> NativeSQLFrame: class IbisConstructor( ConstructorLazyBase, + implementation=Implementation.IBIS, requirements=("ibis", "duckdb", "pyarrow"), legacy_name="ibis_lazy_constructor", ): - """Constructor backed by ``ibis`` (DuckDB backend).""" + """Constructor backed by `ibis` (DuckDB backend).""" name = ConstructorName.IBIS @@ -446,8 +524,3 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ibis.Table: table = pa.table(obj) # type:ignore[arg-type] table_name = str(uuid.uuid4()) return _ibis_backend().create_table(table_name, table, **kwds) - - -# TODO(Unassigned): Remove once all the `"backend" in str(constructor)` -# statements in the test suite are properly replaced. -_LEGACY_NAME = ConstructorBase._legacy_names diff --git a/narwhals/testing/constructors/_name.py b/narwhals/testing/constructors/_name.py index 8fa2d080e0..7118c28f97 100644 --- a/narwhals/testing/constructors/_name.py +++ b/narwhals/testing/constructors/_name.py @@ -1,7 +1,6 @@ from __future__ import annotations from enum import Enum -from functools import lru_cache from importlib.util import find_spec from typing import TYPE_CHECKING @@ -13,14 +12,11 @@ def is_backend_available(*packages: str) -> bool: - """Whether all backends required by `name` can be imported in this environment. + """Whether every package in `packages` can be imported in this environment. Examples: - >>> from narwhals.testing.constructors import ( - ... ConstructorName, - ... is_backend_available, - ... ) - >>> is_backend_available(ConstructorName.PANDAS) + >>> from narwhals.testing.constructors._name import is_backend_available + >>> is_backend_available("pandas") True """ return all(find_spec(pkg) is not None for pkg in packages) @@ -32,6 +28,11 @@ class ConstructorName(str, Enum): The string values are byte-identical to the identifiers accepted by the `--constructors` pytest CLI option (e.g. `pandas[pyarrow]`, `polars[lazy]`). + All static metadata (implementation, requirements, eager/lazy, nullability, + GPU need) lives on the registered constructor class in `_classes.py`. + Properties on this enum delegate to that class so there is one source of + truth for each backend. + Examples: >>> from narwhals.testing.constructors import ConstructorName >>> ConstructorName.PANDAS_PYARROW.value @@ -61,10 +62,17 @@ class ConstructorName(str, Enum): def __str__(self) -> str: return str(self.value) + @property + def constructor(self) -> ConstructorBase: + """Return the registered singleton constructor for this name.""" + from narwhals.testing.constructors._classes import ConstructorBase + + return ConstructorBase._registry[self] + @property def implementation(self) -> Implementation: """The [`Implementation`][] that this constructor belongs to.""" - return _name_to_impl()[self] + return self.constructor.implementation @property def is_pandas(self) -> bool: @@ -130,46 +138,32 @@ def is_spark_like(self) -> bool: @property def is_eager(self) -> bool: """Whether this constructor produces an eager native dataframe.""" - return self in { - ConstructorName.PANDAS, - ConstructorName.PANDAS_NULLABLE, - ConstructorName.PANDAS_PYARROW, - ConstructorName.PYARROW, - ConstructorName.MODIN, - ConstructorName.MODIN_PYARROW, - ConstructorName.CUDF, - ConstructorName.POLARS_EAGER, - } + return self.constructor.is_eager @property def is_lazy(self) -> bool: """Whether this constructor produces a lazy native frame.""" - return not self.is_eager + return self.constructor.is_lazy @property def needs_pyarrow(self) -> bool: """Whether this constructor requires `pyarrow` to be installed.""" - return self in { - ConstructorName.PYARROW, - ConstructorName.PANDAS_PYARROW, - ConstructorName.MODIN_PYARROW, - ConstructorName.DUCKDB, - ConstructorName.IBIS, - } + return self.constructor.needs_pyarrow @property def is_non_nullable(self) -> bool: """Whether this constructor uses a backend without native null support.""" - return self in { - ConstructorName.PANDAS, - ConstructorName.MODIN, - ConstructorName.DASK, - } + return self.constructor.is_non_nullable @property def needs_gpu(self) -> bool: """Whether this constructor requires GPU hardware.""" - return self is ConstructorName.CUDF + return self.constructor.needs_gpu + + @property + def is_available(self) -> bool: + """Whether every package required by this constructor is importable.""" + return self.constructor.is_available # TODO(Unassigned): remove 'no cover' flag once used in test suite @classmethod @@ -186,45 +180,3 @@ def from_pytest_request( ... ... """ return cls(str(request.node.callspec.id)) - - @property - def constructor(self) -> ConstructorBase: - """Return the registered singleton constructor for this name.""" - from narwhals.testing.constructors._classes import ConstructorBase - - return ConstructorBase._registry[self] - - @property - def is_available(self) -> bool: - """Whether every package required by this constructor is importable.""" - from narwhals.testing.constructors._classes import ConstructorBase - - return is_backend_available(*ConstructorBase._requirements[self]) - - -@lru_cache(maxsize=1) -def _name_to_impl() -> dict[ConstructorName, Implementation]: - """Lazily build the ConstructorName -> Implementation mapping. - - The import is deferred so that ``narwhals._utils`` is not loaded - at plugin-registration time (before coverage starts measuring). - """ - from narwhals._utils import Implementation - - return { - ConstructorName.PANDAS: Implementation.PANDAS, - ConstructorName.PANDAS_NULLABLE: Implementation.PANDAS, - ConstructorName.PANDAS_PYARROW: Implementation.PANDAS, - ConstructorName.PYARROW: Implementation.PYARROW, - ConstructorName.MODIN: Implementation.MODIN, - ConstructorName.MODIN_PYARROW: Implementation.MODIN, - ConstructorName.CUDF: Implementation.CUDF, - ConstructorName.POLARS_EAGER: Implementation.POLARS, - ConstructorName.POLARS_LAZY: Implementation.POLARS, - ConstructorName.DASK: Implementation.DASK, - ConstructorName.DUCKDB: Implementation.DUCKDB, - ConstructorName.PYSPARK: Implementation.PYSPARK, - ConstructorName.PYSPARK_CONNECT: Implementation.PYSPARK_CONNECT, - ConstructorName.SQLFRAME: Implementation.SQLFRAME, - ConstructorName.IBIS: Implementation.IBIS, - } diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index 7c19e0a895..916f1b5499 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -104,28 +104,47 @@ def test_constructor_dunder() -> None: def test_init_subclass_no_legacy_name() -> None: - class _Dummy(ConstructorBase, requirements=("polars",)): + class _Dummy( + ConstructorBase, implementation=Implementation.POLARS, requirements=("polars",) + ): name = ConstructorName.POLARS_EAGER def __call__(self, obj: object, /, **kwds: object) -> None: # type: ignore[override] ... # pragma: no cover - # re-registered POLARS_EAGER (overwriting the real one), but without adding a legacy_name entry for it. - assert ConstructorBase._registry[ConstructorName.POLARS_EAGER] == _Dummy() - assert ConstructorBase._legacy_names[ConstructorName.POLARS_EAGER] == "" + # re-registered POLARS_EAGER (overwriting the real one), but without a legacy_name for it. + registered = ConstructorBase._registry[ConstructorName.POLARS_EAGER] + assert registered == _Dummy() + assert registered.legacy_name == "" + assert registered.requirements == ("polars",) + assert registered.implementation is Implementation.POLARS # Restore the original legacy_name = "polars_eager_constructor" class PolarsEagerConstructor( - OriginalPolarsEagerConstructor, requirements=("polars",), legacy_name=legacy_name + OriginalPolarsEagerConstructor, + implementation=Implementation.POLARS, + requirements=("polars",), + legacy_name=legacy_name, ): name = ConstructorName.POLARS_EAGER original = PolarsEagerConstructor() - assert ConstructorBase._registry[ConstructorName.POLARS_EAGER] == original - assert ConstructorBase._legacy_names[ConstructorName.POLARS_EAGER] == legacy_name + restored = ConstructorBase._registry[ConstructorName.POLARS_EAGER] + assert restored == original + assert restored.legacy_name == legacy_name + + +def test_init_subclass_requires_implementation() -> None: + with pytest.raises(TypeError, match="missing `implementation`"): + + class _BadConstructor(ConstructorBase, requirements=("polars",)): + name = ConstructorName.POLARS_EAGER + + def __call__(self, obj: object, /, **kwds: object) -> None: # type: ignore[override] + ... # pragma: no cover def test_get_constructor() -> None: From 3acdbaccbdca24b1b0273c0aea0a1f06894b39ce Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 17 Apr 2026 22:31:43 +0200 Subject: [PATCH 29/71] fully remove ConstructorName --- .../_classes.py => constructors.py} | 321 ++++++++++++++---- narwhals/testing/constructors/__init__.py | 121 ------- narwhals/testing/constructors/_name.py | 182 ---------- narwhals/testing/pytest_plugin.py | 31 +- narwhals/testing/typing.py | 2 +- tests/expr_and_series/fill_nan_test.py | 4 +- tests/expr_and_series/is_close_test.py | 8 +- tests/expr_and_series/is_finite_test.py | 2 +- tests/expr_and_series/is_nan_test.py | 4 +- tests/hypothesis/getitem_test.py | 6 +- tests/ibis_test.py | 7 +- tests/series_only/hist_test.py | 37 +- tests/testing/constructors_test.py | 102 +++--- tests/translate/from_native_test.py | 2 +- 14 files changed, 339 insertions(+), 490 deletions(-) rename narwhals/testing/{constructors/_classes.py => constructors.py} (71%) delete mode 100644 narwhals/testing/constructors/__init__.py delete mode 100644 narwhals/testing/constructors/_name.py diff --git a/narwhals/testing/constructors/_classes.py b/narwhals/testing/constructors.py similarity index 71% rename from narwhals/testing/constructors/_classes.py rename to narwhals/testing/constructors.py index eef27e1e92..eda37a29d8 100644 --- a/narwhals/testing/constructors/_classes.py +++ b/narwhals/testing/constructors.py @@ -14,9 +14,7 @@ * `ConstructorEagerBase`: if the backend returns an eager dataframe. * `ConstructorLazyBase`: if the backend returns a lazy frame. -2. Add a member to `ConstructorName` in `_name.py`. - -3. Define the class in this module and declare its metadata in the class +2. Define the class in this module and declare its metadata in the class header as keyword arguments: ```py @@ -26,7 +24,7 @@ class MyBackendConstructor( requirements=("my_backend",), legacy_name="my_backend_lazy_constructor", ): - name = ConstructorName.MY_BACKEND + name = "my_backend" def __call__(self, obj: Data, /, **kwds: Any) -> ...: import my_backend @@ -35,7 +33,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ...: ``` That is all. `__init_subclass__` on `ConstructorBase` automatically registers -a default singleton into `_registry`. +a default singleton into `_registry`, keyed by the string `name`. """ from __future__ import annotations @@ -45,12 +43,14 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ...: import warnings from copy import deepcopy from functools import lru_cache +from importlib.util import find_spec from typing import TYPE_CHECKING, Any, ClassVar, Protocol, cast from narwhals._utils import Implementation, generate_temporary_column_name -from narwhals.testing.constructors._name import ConstructorName, is_backend_available if TYPE_CHECKING: + from collections.abc import Iterable + import ibis import pandas as pd import polars as pl @@ -64,56 +64,19 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ...: from narwhals.typing import IntoDataFrame, IntoFrame, IntoLazyFrame -def sqlframe_session() -> DuckDBSession: - """Return a fresh in-memory `sqlframe` DuckDB session.""" - from sqlframe.duckdb import DuckDBSession - - # NOTE: `__new__` override inferred by `pyright` only - # https://github.com/eakmanrq/sqlframe/blob/772b3a6bfe5a1ffd569b7749d84bea2f3a314510/sqlframe/base/session.py#L181-L184 - return cast("DuckDBSession", DuckDBSession()) # type: ignore[redundant-cast] - - -def pyspark_session() -> SparkSession: # pragma: no cover - """Return a singleton local `pyspark` (or pyspark[connect]) session.""" - if is_spark_connect := os.environ.get("SPARK_CONNECT", None): - from pyspark.sql.connect.session import SparkSession - else: - from pyspark.sql import SparkSession - builder = cast("SparkSession.Builder", SparkSession.builder).appName("unit-tests") - builder = ( - builder.remote(f"sc://localhost:{os.environ.get('SPARK_PORT', '15002')}") - if is_spark_connect - else builder.master("local[1]").config("spark.ui.enabled", "false") - ) - return ( - builder.config("spark.default.parallelism", "1") - .config("spark.sql.shuffle.partitions", "2") - .config("spark.sql.session.timeZone", "UTC") - .getOrCreate() - ) - - -@lru_cache(maxsize=1) -def _ibis_backend() -> IbisDuckDBBackend: # pragma: no cover - """Cached singleton in-memory ibis backend, so all tables share one database.""" - import ibis - - return ibis.duckdb.connect() - - -@lru_cache(maxsize=1) -def _pyspark_session_lazy() -> SparkSession: # pragma: no cover - """Cached pyspark session; created on first use, stopped at interpreter exit.""" - from atexit import register - - with warnings.catch_warnings(): - # The spark session seems to trigger a polars warning. - warnings.filterwarnings( - "ignore", r"Using fork\(\) can cause Polars", category=RuntimeWarning - ) - session = pyspark_session() - register(session.stop) - return session +__all__ = ( + "ALL_CONSTRUCTORS", + "ALL_CPU_CONSTRUCTORS", + "DEFAULT_CONSTRUCTORS", + "ConstructorBase", + "ConstructorEagerBase", + "available_constructors", + "get_constructor", + "is_backend_available", + "prepare_constructors", + "pyspark_session", + "sqlframe_session", +) class ConstructorBase(Protocol): @@ -121,7 +84,7 @@ class ConstructorBase(Protocol): A constructor is a callable that turns a column-oriented `dict` (typed as [`Data`][narwhals.testing.typing.Data]) into a native dataframe / lazy frame, - plus a typed [`ConstructorName`][] that identifies the backend. + plus a string `name` that identifies the backend (e.g. `"pandas[pyarrow]"`). Subclasses declare their backend metadata (implementation, requirements, legacy name, nullability, GPU need) as keyword arguments in the class @@ -129,9 +92,9 @@ class ConstructorBase(Protocol): default singleton into `_registry`. """ - _registry: ClassVar[dict[ConstructorName, ConstructorBase]] = {} + _registry: ClassVar[dict[str, ConstructorBase]] = {} - name: ClassVar[ConstructorName] + name: ClassVar[str] implementation: ClassVar[Implementation] requirements: ClassVar[tuple[str, ...]] = () legacy_name: ClassVar[str] = "" @@ -187,13 +150,74 @@ def __call__(self, obj: Data, /, **kwds: Any) -> IntoFrame: @property def identifier(self) -> str: """Instance-level string identifier for test IDs.""" - return str(self.name) + return self.name @property def is_lazy(self) -> bool: """Whether this constructor produces a lazy native frame.""" return not self.is_eager + @property + def is_pandas(self) -> bool: + """Whether this is one of the pandas constructors.""" + return self.implementation.is_pandas() + + @property + def is_modin(self) -> bool: + """Whether this is one of the modin constructors.""" + return self.implementation.is_modin() + + @property + def is_cudf(self) -> bool: + """Whether this is the cudf constructor.""" + return self.implementation.is_cudf() + + @property + def is_pandas_like(self) -> bool: + """Whether this constructor produces a pandas-like dataframe (pandas, modin, cudf).""" + return self.implementation.is_pandas_like() + + @property + def is_polars(self) -> bool: + """Whether this is one of the polars constructors.""" + return self.implementation.is_polars() + + @property + def is_pyarrow(self) -> bool: + """Whether this is the pyarrow table constructor.""" + return self.implementation.is_pyarrow() + + @property + def is_dask(self) -> bool: + """Whether this is the dask constructor.""" + return self.implementation.is_dask() + + @property + def is_duckdb(self) -> bool: + """Whether this is the duckdb constructor.""" + return self.implementation.is_duckdb() + + @property + def is_pyspark(self) -> bool: + """Whether this is one of the pyspark constructors.""" + impl = self.implementation + return impl.is_pyspark() or impl.is_pyspark_connect() + + @property + def is_sqlframe(self) -> bool: + """Whether this is the sqlframe constructor.""" + return self.implementation.is_sqlframe() + + @property + def is_ibis(self) -> bool: + """Whether this is the ibis constructor.""" + return self.implementation.is_ibis() + + @property + def is_spark_like(self) -> bool: + """Whether this constructor uses a spark-like backend (pyspark, sqlframe).""" + return self.implementation.is_spark_like() + @property def needs_pyarrow(self) -> bool: """Whether this constructor requires `pyarrow` to be installed.""" @@ -250,7 +274,7 @@ class PandasConstructor( ): """Constructor backed by `pandas.DataFrame` with default NumPy dtypes.""" - name = ConstructorName.PANDAS + name = "pandas" def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: import pandas as pd @@ -266,7 +290,7 @@ class PandasNullableConstructor( ): """Constructor backed by `pandas.DataFrame` with `numpy_nullable` dtypes.""" - name = ConstructorName.PANDAS_NULLABLE + name = "pandas[nullable]" def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: import pandas as pd @@ -282,7 +306,7 @@ class PandasPyArrowConstructor( ): """Constructor backed by `pandas.DataFrame` with `pyarrow` dtypes.""" - name = ConstructorName.PANDAS_PYARROW + name = "pandas[pyarrow]" def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: import pandas as pd @@ -298,7 +322,7 @@ class PyArrowConstructor( ): """Constructor backed by `pyarrow.Table`.""" - name = ConstructorName.PYARROW + name = "pyarrow" def __call__(self, obj: Data, /, **kwds: Any) -> pa.Table: import pyarrow as pa @@ -315,7 +339,7 @@ class ModinConstructor( ): # pragma: no cover """Constructor backed by `modin.pandas.DataFrame`.""" - name = ConstructorName.MODIN + name = "modin" def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: import modin.pandas as mpd @@ -332,7 +356,7 @@ class ModinPyArrowConstructor( ): """Constructor backed by `modin.pandas.DataFrame` with `pyarrow` dtypes.""" - name = ConstructorName.MODIN_PYARROW + name = "modin[pyarrow]" def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: import modin.pandas as mpd @@ -353,7 +377,7 @@ class CudfConstructor( ): # pragma: no cover """Constructor backed by `cudf.DataFrame` (requires GPU).""" - name = ConstructorName.CUDF + name = "cudf" def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: import cudf @@ -369,7 +393,7 @@ class PolarsEagerConstructor( ): """Constructor backed by `polars.DataFrame`.""" - name = ConstructorName.POLARS_EAGER + name = "polars[eager]" def __call__(self, obj: Data, /, **kwds: Any) -> pl.DataFrame: import polars as pl @@ -388,7 +412,7 @@ class PolarsLazyConstructor( ): """Constructor backed by `polars.LazyFrame`.""" - name = ConstructorName.POLARS_LAZY + name = "polars[lazy]" def __call__(self, obj: Data, /, **kwds: Any) -> pl.LazyFrame: import polars as pl @@ -409,7 +433,7 @@ class DaskConstructor( npartitions: Number of Dask partitions (default `1`). """ - name = ConstructorName.DASK + name = "dask" def __init__(self, npartitions: int = 2) -> None: self.npartitions = npartitions @@ -445,7 +469,7 @@ class DuckDBConstructor( ): """Constructor backed by DuckDB (via `duckdb.sql`).""" - name = ConstructorName.DUCKDB + name = "duckdb" def __call__(self, obj: Data, /, **kwds: Any) -> NativeDuckDB: import duckdb @@ -464,7 +488,7 @@ class PySparkConstructor( ): # pragma: no cover """Constructor backed by `pyspark.sql.DataFrame`.""" - name = ConstructorName.PYSPARK + name = "pyspark" def __call__(self, obj: Data, /, **kwds: Any) -> NativePySpark: session = _pyspark_session_lazy() @@ -488,7 +512,7 @@ class PySparkConnectConstructor( ): # pragma: no cover """Constructor backed by PySpark Connect (Spark Connect protocol).""" - name = ConstructorName.PYSPARK_CONNECT + name = "pyspark[connect]" class SQLFrameConstructor( @@ -499,7 +523,7 @@ class SQLFrameConstructor( ): """Constructor backed by `sqlframe` (DuckDB session).""" - name = ConstructorName.SQLFRAME + name = "sqlframe" def __call__(self, obj: Data, /, **kwds: Any) -> NativeSQLFrame: session = sqlframe_session() @@ -516,7 +540,7 @@ class IbisConstructor( ): """Constructor backed by `ibis` (DuckDB backend).""" - name = ConstructorName.IBIS + name = "ibis" def __call__(self, obj: Data, /, **kwds: Any) -> ibis.Table: import pyarrow as pa @@ -524,3 +548,150 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ibis.Table: table = pa.table(obj) # type:ignore[arg-type] table_name = str(uuid.uuid4()) return _ibis_backend().create_table(table_name, table, **kwds) + + +ALL_CONSTRUCTORS: dict[str, ConstructorBase] = ConstructorBase._registry +"""All registered constructors keyed by their string identifier.""" + +DEFAULT_CONSTRUCTORS: frozenset[str] = frozenset( + { + "pandas", + "pandas[pyarrow]", + "polars[eager]", + "pyarrow", + "duckdb", + "sqlframe", + "ibis", + } +) +"""Subset of constructors enabled by default for parametrised tests when the +user does not pass `--constructors` (mirrors the historical Narwhals defaults). +""" + +ALL_CPU_CONSTRUCTORS: frozenset[str] = frozenset( + name for name, c in ConstructorBase._registry.items() if not c.needs_gpu +) +"""All constructors that do not require GPU hardware.""" + + +def available_constructors() -> frozenset[str]: + """Return the names of every constructor whose backend is importable. + + Examples: + >>> from narwhals.testing.constructors import available_constructors + >>> "pandas" in available_constructors() + True + """ + return frozenset(name for name, c in ALL_CONSTRUCTORS.items() if c.is_available) + + +def get_constructor(name: str) -> ConstructorBase: + """Return the registered singleton constructor for `name`. + + Arguments: + name: The string identifier of a registered constructor + (e.g. `"pandas[pyarrow]"`). + + Raises: + ValueError: If `name` is not a registered constructor identifier. + + Examples: + >>> from narwhals.testing.constructors import get_constructor + >>> get_constructor("pandas") + PandasConstructor() + """ + try: + return ALL_CONSTRUCTORS[name] + except KeyError as exc: + valid = sorted(ALL_CONSTRUCTORS) + msg = f"Unknown constructor {name!r}. Expected one of: {valid}." + raise ValueError(msg) from exc + + +def prepare_constructors( + *, include: Iterable[str] | None = None, exclude: Iterable[str] | None = None +) -> list[ConstructorBase]: + """Return available constructors, optionally filtered. + + Arguments: + include: If given, only return constructors whose name is in this set. + exclude: If given, remove constructors whose name is in this set. + + Examples: + >>> from narwhals.testing.constructors import prepare_constructors + >>> constructors = prepare_constructors(include=["pandas", "polars[eager]"]) + """ + available = available_constructors() + candidates: list[ConstructorBase] = [ + c for name, c in ALL_CONSTRUCTORS.items() if name in available + ] + if include is not None: + inc = frozenset(include) + candidates = [c for c in candidates if c.name in inc] + if exclude is not None: + exc = frozenset(exclude) + candidates = [c for c in candidates if c.name not in exc] + return sorted(candidates, key=lambda c: c.name) + + +def is_backend_available(*packages: str) -> bool: + """Whether every package in `packages` can be imported in this environment. + + Examples: + >>> from narwhals.testing.constructors import is_backend_available + >>> is_backend_available("pandas") + True + """ + return all(find_spec(pkg) is not None for pkg in packages) + + +def sqlframe_session() -> DuckDBSession: + """Return a fresh in-memory `sqlframe` DuckDB session.""" + from sqlframe.duckdb import DuckDBSession + + # NOTE: `__new__` override inferred by `pyright` only + # https://github.com/eakmanrq/sqlframe/blob/772b3a6bfe5a1ffd569b7749d84bea2f3a314510/sqlframe/base/session.py#L181-L184 + return cast("DuckDBSession", DuckDBSession()) # type: ignore[redundant-cast] + + +def pyspark_session() -> SparkSession: # pragma: no cover + """Return a singleton local `pyspark` (or pyspark[connect]) session.""" + if is_spark_connect := os.environ.get("SPARK_CONNECT", None): + from pyspark.sql.connect.session import SparkSession + else: + from pyspark.sql import SparkSession + builder = cast("SparkSession.Builder", SparkSession.builder).appName("unit-tests") + builder = ( + builder.remote(f"sc://localhost:{os.environ.get('SPARK_PORT', '15002')}") + if is_spark_connect + else builder.master("local[1]").config("spark.ui.enabled", "false") + ) + return ( + builder.config("spark.default.parallelism", "1") + .config("spark.sql.shuffle.partitions", "2") + .config("spark.sql.session.timeZone", "UTC") + .getOrCreate() + ) + + +@lru_cache(maxsize=1) +def _ibis_backend() -> IbisDuckDBBackend: # pragma: no cover + """Cached singleton in-memory ibis backend, so all tables share one database.""" + import ibis + + return ibis.duckdb.connect() + + +@lru_cache(maxsize=1) +def _pyspark_session_lazy() -> SparkSession: # pragma: no cover + """Cached pyspark session; created on first use, stopped at interpreter exit.""" + from atexit import register + + with warnings.catch_warnings(): + # The spark session seems to trigger a polars warning. + warnings.filterwarnings( + "ignore", r"Using fork\(\) can cause Polars", category=RuntimeWarning + ) + session = pyspark_session() + register(session.stop) + return session diff --git a/narwhals/testing/constructors/__init__.py b/narwhals/testing/constructors/__init__.py deleted file mode 100644 index fb9d60754a..0000000000 --- a/narwhals/testing/constructors/__init__.py +++ /dev/null @@ -1,121 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from narwhals.testing.constructors._classes import ( - ConstructorBase, - ConstructorEagerBase, - pyspark_session, - sqlframe_session, -) -from narwhals.testing.constructors._name import ConstructorName - -if TYPE_CHECKING: - from collections.abc import Iterable - -__all__ = ( - "ALL_CONSTRUCTORS", - "ALL_CPU_CONSTRUCTORS", - "DEFAULT_CONSTRUCTORS", - "ConstructorBase", - "ConstructorEagerBase", - "ConstructorName", - "available_constructors", - "get_constructor", - "prepare_constructors", - "pyspark_session", - "sqlframe_session", -) - - -ALL_CONSTRUCTORS: dict[ConstructorName, ConstructorBase] = { - name: name.constructor for name in ConstructorName -} - -DEFAULT_CONSTRUCTORS: frozenset[ConstructorName] = frozenset( - { - ConstructorName.PANDAS, - ConstructorName.PANDAS_PYARROW, - ConstructorName.POLARS_EAGER, - ConstructorName.PYARROW, - ConstructorName.DUCKDB, - ConstructorName.SQLFRAME, - ConstructorName.IBIS, - } -) -"""Subset of constructors enabled by default for parametrised tests when the -user does not pass `--constructors` (mirrors the historical Narwhals defaults). -""" - -ALL_CPU_CONSTRUCTORS: frozenset[ConstructorName] = frozenset( - name for name in ConstructorName if not name.needs_gpu -) -"""All constructors that do not require GPU hardware.""" - - -def available_constructors() -> frozenset[ConstructorName]: - """Return every [`ConstructorName`][] whose backend is importable. - - Examples: - >>> from narwhals.testing.constructors import available_constructors - >>> ConstructorName.PANDAS in available_constructors() - True - """ - return frozenset(name for name in ConstructorName if name.is_available) - - -def get_constructor(name: ConstructorName | str) -> ConstructorBase: - """Return the registered singleton constructor for `name`. - - Arguments: - name: A [`ConstructorName`][] member or its string value - (e.g. `"pandas[pyarrow]"`). - - Raises: - ValueError: If `name` is not a registered constructor identifier. - - Examples: - >>> from narwhals.testing.constructors import get_constructor - >>> get_constructor("pandas") - PandasConstructor() - """ - try: - key = ConstructorName(name) if isinstance(name, str) else name - except ValueError as exc: - valid = sorted(c.value for c in ConstructorName) - msg = f"Unknown constructor {name!r}. Expected one of: {valid}." - raise ValueError(msg) from exc - return ALL_CONSTRUCTORS[key] - - -def prepare_constructors( - *, - include: Iterable[ConstructorName] | None = None, - exclude: Iterable[ConstructorName] | None = None, -) -> list[ConstructorBase]: - """Return available constructors, optionally filtered. - - Arguments: - include: If given, only return constructors whose name is in this set. - exclude: If given, remove constructors whose name is in this set. - - Examples: - >>> from narwhals.testing.constructors import ( - ... ConstructorName, - ... prepare_constructors, - ... ) - >>> constructors = prepare_constructors( - ... include=[ConstructorName.PANDAS, ConstructorName.POLARS_EAGER] - ... ) - """ - available = available_constructors() - candidates: list[ConstructorBase] = [ - ConstructorBase._registry[n] for n in ConstructorBase._registry if n in available - ] - if include is not None: - inc = frozenset(include) - candidates = [c for c in candidates if c.name in inc] - if exclude is not None: - exc = frozenset(exclude) - candidates = [c for c in candidates if c.name not in exc] - return sorted(candidates, key=lambda c: c.name.value) diff --git a/narwhals/testing/constructors/_name.py b/narwhals/testing/constructors/_name.py deleted file mode 100644 index 7118c28f97..0000000000 --- a/narwhals/testing/constructors/_name.py +++ /dev/null @@ -1,182 +0,0 @@ -from __future__ import annotations - -from enum import Enum -from importlib.util import find_spec -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - import pytest - - from narwhals._utils import Implementation - from narwhals.testing.constructors._classes import ConstructorBase - - -def is_backend_available(*packages: str) -> bool: - """Whether every package in `packages` can be imported in this environment. - - Examples: - >>> from narwhals.testing.constructors._name import is_backend_available - >>> is_backend_available("pandas") - True - """ - return all(find_spec(pkg) is not None for pkg in packages) - - -class ConstructorName(str, Enum): - """Typed identifier for each backend exposed by `narwhals.testing.constructors`. - - The string values are byte-identical to the identifiers accepted by the - `--constructors` pytest CLI option (e.g. `pandas[pyarrow]`, `polars[lazy]`). - - All static metadata (implementation, requirements, eager/lazy, nullability, - GPU need) lives on the registered constructor class in `_classes.py`. - Properties on this enum delegate to that class so there is one source of - truth for each backend. - - Examples: - >>> from narwhals.testing.constructors import ConstructorName - >>> ConstructorName.PANDAS_PYARROW.value - 'pandas[pyarrow]' - >>> ConstructorName.PANDAS_PYARROW.is_pandas_like - True - >>> ConstructorName.PANDAS_PYARROW.needs_pyarrow - True - """ - - PANDAS = "pandas" - PANDAS_NULLABLE = "pandas[nullable]" - PANDAS_PYARROW = "pandas[pyarrow]" - PYARROW = "pyarrow" - MODIN = "modin" - MODIN_PYARROW = "modin[pyarrow]" - CUDF = "cudf" - POLARS_EAGER = "polars[eager]" - POLARS_LAZY = "polars[lazy]" - DASK = "dask" - DUCKDB = "duckdb" - PYSPARK = "pyspark" - PYSPARK_CONNECT = "pyspark[connect]" - SQLFRAME = "sqlframe" - IBIS = "ibis" - - def __str__(self) -> str: - return str(self.value) - - @property - def constructor(self) -> ConstructorBase: - """Return the registered singleton constructor for this name.""" - from narwhals.testing.constructors._classes import ConstructorBase - - return ConstructorBase._registry[self] - - @property - def implementation(self) -> Implementation: - """The [`Implementation`][] that this constructor belongs to.""" - return self.constructor.implementation - - @property - def is_pandas(self) -> bool: - """Whether this is one of the pandas constructors.""" - return self.implementation.is_pandas() - - @property - def is_modin(self) -> bool: - """Whether this is one of the modin constructors.""" - return self.implementation.is_modin() - - @property - def is_cudf(self) -> bool: - """Whether this is the cudf constructor.""" - return self.implementation.is_cudf() - - @property - def is_pandas_like(self) -> bool: - """Whether this constructor produces a pandas-like dataframe (pandas, modin, cudf).""" - return self.implementation.is_pandas_like() - - @property - def is_polars(self) -> bool: - """Whether this is one of the polars constructors.""" - return self.implementation.is_polars() - - @property - def is_pyarrow(self) -> bool: - """Whether this is the pyarrow table constructor.""" - return self.implementation.is_pyarrow() - - @property - def is_dask(self) -> bool: - """Whether this is the dask constructor.""" - return self.implementation.is_dask() - - @property - def is_duckdb(self) -> bool: - """Whether this is the duckdb constructor.""" - return self.implementation.is_duckdb() - - @property - def is_pyspark(self) -> bool: - """Whether this is one of the pyspark constructors.""" - impl = self.implementation - return impl.is_pyspark() or impl.is_pyspark_connect() - - @property - def is_sqlframe(self) -> bool: - """Whether this is the sqlframe constructor.""" - return self.implementation.is_sqlframe() - - @property - def is_ibis(self) -> bool: - """Whether this is the ibis constructor.""" - return self.implementation.is_ibis() - - @property - def is_spark_like(self) -> bool: - """Whether this constructor uses a spark-like backend (pyspark, sqlframe).""" - return self.implementation.is_spark_like() - - @property - def is_eager(self) -> bool: - """Whether this constructor produces an eager native dataframe.""" - return self.constructor.is_eager - - @property - def is_lazy(self) -> bool: - """Whether this constructor produces a lazy native frame.""" - return self.constructor.is_lazy - - @property - def needs_pyarrow(self) -> bool: - """Whether this constructor requires `pyarrow` to be installed.""" - return self.constructor.needs_pyarrow - - @property - def is_non_nullable(self) -> bool: - """Whether this constructor uses a backend without native null support.""" - return self.constructor.is_non_nullable - - @property - def needs_gpu(self) -> bool: - """Whether this constructor requires GPU hardware.""" - return self.constructor.needs_gpu - - @property - def is_available(self) -> bool: - """Whether every package required by this constructor is importable.""" - return self.constructor.is_available - - # TODO(Unassigned): remove 'no cover' flag once used in test suite - @classmethod - def from_pytest_request( - cls, request: pytest.FixtureRequest - ) -> ConstructorName: # pragma: no cover - """Resolve the [`ConstructorName`][] from the current parametrised pytest request. - - Examples: - >>> import pytest - >>> def test_example(constructor, request): # doctest: +SKIP - ... name = ConstructorName.from_pytest_request(request) - ... if name.is_pandas_like: - ... ... - """ - return cls(str(request.node.callspec.id)) diff --git a/narwhals/testing/pytest_plugin.py b/narwhals/testing/pytest_plugin.py index 936ce08e64..f75d05b532 100644 --- a/narwhals/testing/pytest_plugin.py +++ b/narwhals/testing/pytest_plugin.py @@ -46,14 +46,14 @@ def _default_constructor_ids() -> list[str]: return env.split(",") from narwhals.testing.constructors import DEFAULT_CONSTRUCTORS, prepare_constructors - return [str(c.name) for c in prepare_constructors(include=DEFAULT_CONSTRUCTORS)] + return [c.name for c in prepare_constructors(include=DEFAULT_CONSTRUCTORS)] def pytest_addoption(parser: pytest.Parser) -> None: from narwhals.testing.constructors import DEFAULT_CONSTRUCTORS group = parser.getgroup("narwhals", "narwhals.testing") - defaults = ", ".join(f"'{c.value}'" for c in sorted(DEFAULT_CONSTRUCTORS)) + defaults = ", ".join(f"'{c}'" for c in sorted(DEFAULT_CONSTRUCTORS)) group.addoption( "--constructors", action="store", @@ -90,15 +90,9 @@ def pytest_addoption(parser: pytest.Parser) -> None: def _select_constructors( config: pytest.Config, ) -> list[ConstructorBase]: # pragma: no cover - from narwhals.testing.constructors import ( - ALL_CPU_CONSTRUCTORS, - ConstructorName, - prepare_constructors, - ) + from narwhals.testing.constructors import ALL_CPU_CONSTRUCTORS, prepare_constructors - _all_cpu_exclusions = frozenset( - {ConstructorName.MODIN, ConstructorName.PYSPARK_CONNECT} - ) + _all_cpu_exclusions = frozenset({"modin", "pyspark[connect]"}) if config.getoption("all_cpu_constructors"): selected = prepare_constructors( @@ -106,14 +100,11 @@ def _select_constructors( ) else: opt = cast("str", config.getoption("constructors")) - names = [ConstructorName(c) for c in opt.split(",") if c] + names = [c for c in opt.split(",") if c] selected = prepare_constructors(include=names) if _pandas_version() < _MIN_PANDAS_NULLABLE_VERSION: - _pandas_nullables = { - ConstructorName.PANDAS_NULLABLE, - ConstructorName.PANDAS_PYARROW, - } + _pandas_nullables = {"pandas[nullable]", "pandas[pyarrow]"} selected = [c for c in selected if c.name not in _pandas_nullables] return selected @@ -129,14 +120,14 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: selected = _select_constructors(metafunc.config) if "constructor_eager" in fixturenames: - params = [c for c in selected if c.name.is_eager] - ids = [str(c.name) for c in params] + params = [c for c in selected if c.is_eager] + ids = [c.name for c in params] metafunc.parametrize("constructor_eager", params, ids=ids) elif "constructor" in fixturenames: - metafunc.parametrize("constructor", selected, ids=[str(c.name) for c in selected]) + metafunc.parametrize("constructor", selected, ids=[c.name for c in selected]) elif "constructor_pandas_like" in fixturenames: - params = [c for c in selected if c.name.is_eager and c.name.is_pandas_like] - ids = [str(c.name) for c in params] + params = [c for c in selected if c.is_eager and c.is_pandas_like] + ids = [c.name for c in params] metafunc.parametrize("constructor_pandas_like", params, ids=ids) else: # pragma: no cover ... diff --git a/narwhals/testing/typing.py b/narwhals/testing/typing.py index a006a5d4c2..62b07873ce 100644 --- a/narwhals/testing/typing.py +++ b/narwhals/testing/typing.py @@ -5,7 +5,7 @@ if TYPE_CHECKING: from typing_extensions import TypeAlias - from narwhals.testing.constructors._classes import ( + from narwhals.testing.constructors import ( ConstructorBase, ConstructorEagerBase, ConstructorLazyBase, diff --git a/tests/expr_and_series/fill_nan_test.py b/tests/expr_and_series/fill_nan_test.py index dd63801ff0..5d3ddc8cfc 100644 --- a/tests/expr_and_series/fill_nan_test.py +++ b/tests/expr_and_series/fill_nan_test.py @@ -23,7 +23,7 @@ def test_fill_nan(request: pytest.FixtureRequest, constructor: Constructor) -> N assert_equal_data(result, expected) assert result.lazy().collect()["float_na"].null_count() == 2 result = df.select(nw.all().fill_nan(3.0)) - if constructor.name.is_non_nullable: + if constructor.is_non_nullable: # no nan vs null distinction expected = {"float": [-1.0, 1.0, 3.0], "float_na": [3.0, 1.0, 3.0]} assert result.lazy().collect()["float_na"].null_count() == 0 @@ -42,7 +42,7 @@ def test_fill_nan_series(constructor_eager: ConstructorEager) -> None: "float_na" ] result = s.fill_nan(999) - if constructor_eager.name.is_non_nullable: + if constructor_eager.is_non_nullable: # no nan vs null distinction assert_equal_data({"a": result}, {"a": [999.0, 1.0, 999.0]}) elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): diff --git a/tests/expr_and_series/is_close_test.py b/tests/expr_and_series/is_close_test.py index f580dd5eb4..6cfdb94657 100644 --- a/tests/expr_and_series/is_close_test.py +++ b/tests/expr_and_series/is_close_test.py @@ -114,7 +114,7 @@ def test_is_close_series_with_series( y = y.zip_with(y != NAN_PLACEHOLDER, y**0.5).zip_with(y != NULL_PLACEHOLDER, nulls) result = x.is_close(y, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal) - if constructor_eager.name.is_non_nullable: + if constructor_eager.is_non_nullable: expected = [v if v is not None else nans_equal for v in expected] elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): expected = [ @@ -142,7 +142,7 @@ def test_is_close_series_with_scalar( y = y.zip_with(y != NAN_PLACEHOLDER, y**0.5).zip_with(y != NULL_PLACEHOLDER, nulls) result = y.is_close(other, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal) - if constructor_eager.name.is_non_nullable: + if constructor_eager.is_non_nullable: expected = [v if v is not None else False for v in expected] elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): expected = [ @@ -187,7 +187,7 @@ def test_is_close_expr_with_expr( ) .sort("idx") ) - if constructor.name.is_non_nullable: + if constructor.is_non_nullable: expected = [v if v is not None else nans_equal for v in expected] elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): expected = [ @@ -228,7 +228,7 @@ def test_is_close_expr_with_scalar( ) .sort("idx") ) - if constructor.name.is_non_nullable: + if constructor.is_non_nullable: expected = [v if v is not None else False for v in expected] elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): expected = [ diff --git a/tests/expr_and_series/is_finite_test.py b/tests/expr_and_series/is_finite_test.py index d9c3d68965..b2a36d5ed7 100644 --- a/tests/expr_and_series/is_finite_test.py +++ b/tests/expr_and_series/is_finite_test.py @@ -64,7 +64,7 @@ def test_is_finite_column_with_null(constructor: Constructor, data: list[float]) result = df.select(nw.col("a").is_finite()) expected: dict[str, list[Any]] - if constructor.name.is_non_nullable: + if constructor.is_non_nullable: # Null values are coerced to NaN for non-nullable datatypes expected = {"a": [True, True, False]} else: diff --git a/tests/expr_and_series/is_nan_test.py b/tests/expr_and_series/is_nan_test.py index 632710ae39..948b800a9b 100644 --- a/tests/expr_and_series/is_nan_test.py +++ b/tests/expr_and_series/is_nan_test.py @@ -20,7 +20,7 @@ def test_nan(constructor: Constructor) -> None: ) expected: dict[str, list[Any]] - if constructor.name.is_non_nullable: + if constructor.is_non_nullable: # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], @@ -57,7 +57,7 @@ def test_nan_series(constructor_eager: ConstructorEager) -> None: "float_na": df["float_na"].is_nan(), } expected: dict[str, list[Any]] - if constructor_eager.name.is_non_nullable: + if constructor_eager.is_non_nullable: # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], diff --git a/tests/hypothesis/getitem_test.py b/tests/hypothesis/getitem_test.py index a85f7d57f8..9ce1ba38b1 100644 --- a/tests/hypothesis/getitem_test.py +++ b/tests/hypothesis/getitem_test.py @@ -7,7 +7,7 @@ from hypothesis import assume, given import narwhals as nw -from narwhals.testing.constructors._classes import PandasConstructor, PyArrowConstructor +from narwhals.testing.constructors import PandasConstructor, PyArrowConstructor from tests.utils import assert_equal_data if TYPE_CHECKING: @@ -126,7 +126,7 @@ def test_getitem(pandas_or_pyarrow_constructor: ConstructorBase, selector: Any) # NotImplementedError: Slicing with step is not supported on PyArrow tables assume( not ( - pandas_or_pyarrow_constructor.name.is_pyarrow + pandas_or_pyarrow_constructor.is_pyarrow and isinstance(selector, slice) and selector.step is not None ) @@ -135,7 +135,7 @@ def test_getitem(pandas_or_pyarrow_constructor: ConstructorBase, selector: Any) # NotImplementedError: Slicing with step is not supported on PyArrow tables assume( not ( - pandas_or_pyarrow_constructor.name.is_pyarrow + pandas_or_pyarrow_constructor.is_pyarrow and isinstance(selector, tuple) and ( (isinstance(selector[0], slice) and selector[0].step is not None) diff --git a/tests/ibis_test.py b/tests/ibis_test.py index 06926f821d..f537ef1c99 100644 --- a/tests/ibis_test.py +++ b/tests/ibis_test.py @@ -3,11 +3,12 @@ import pytest import narwhals as nw -from narwhals.testing.constructors import ConstructorName +from narwhals.testing.constructors import get_constructor def test_from_native() -> None: - if not (name := ConstructorName.IBIS).is_available: + ibis_constructor = get_constructor("ibis") + if not ibis_constructor.is_available: pytest.skip() - df = nw.from_native(name.constructor({"a": [1, 2, 3], "b": [4, 5, 6]})) + df = nw.from_native(ibis_constructor({"a": [1, 2, 3], "b": [4, 5, 6]})) assert df.columns == ["a", "b"] diff --git a/tests/series_only/hist_test.py b/tests/series_only/hist_test.py index 5b42eae346..18468cc783 100644 --- a/tests/series_only/hist_test.py +++ b/tests/series_only/hist_test.py @@ -3,7 +3,7 @@ from __future__ import annotations from random import Random -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any import hypothesis.strategies as st import pytest @@ -11,7 +11,7 @@ import narwhals as nw from narwhals.exceptions import ComputeError -from narwhals.testing.constructors import ConstructorName +from narwhals.testing.constructors import get_constructor from tests.utils import POLARS_VERSION, ConstructorEager, assert_equal_data if TYPE_CHECKING: @@ -44,15 +44,13 @@ param_include_breakpoint = pytest.mark.parametrize( "include_breakpoint", [True, False], ids=["breakpoint-True", "breakpoint-False"] ) -param_name = pytest.mark.parametrize( - "name", - [ConstructorName.PANDAS, ConstructorName.POLARS_EAGER, ConstructorName.PYARROW], -) +param_name = pytest.mark.parametrize("name", ["pandas", "polars[eager]", "pyarrow"]) -def maybe_name_to_constructor(name: ConstructorName) -> ConstructorEager: - if name.is_available: - return cast("ConstructorEager", name.constructor) +def maybe_name_to_constructor(name: str) -> ConstructorEager: + constructor = get_constructor(name) + if constructor.is_available: + return constructor # type: ignore[return-value] pytest.skip() @@ -77,11 +75,7 @@ def maybe_name_to_constructor(name: ConstructorName) -> ConstructorEager: ) @param_name def test_hist_bin( - name: ConstructorName, - bins: list[float], - expected: Sequence[float], - *, - include_breakpoint: bool, + name: str, bins: list[float], expected: Sequence[float], *, include_breakpoint: bool ) -> None: constructor_eager = maybe_name_to_constructor(name) df = nw.from_native(constructor_eager(data)).with_columns( @@ -128,7 +122,7 @@ def test_hist_bin( @param_include_breakpoint @param_name def test_hist_count( - name: ConstructorName, *, params: dict[str, Any], include_breakpoint: bool + name: str, *, params: dict[str, Any], include_breakpoint: bool ) -> None: constructor_eager = maybe_name_to_constructor(name) df = nw.from_native(constructor_eager(data)).with_columns( @@ -172,7 +166,7 @@ def test_hist_count( @param_name -def test_hist_count_no_spread(name: ConstructorName) -> None: +def test_hist_count_no_spread(name: str) -> None: constructor_eager = maybe_name_to_constructor(name) data = {"all_zero": [0, 0, 0], "all_non_zero": [5, 5, 5]} df = nw.from_native(constructor_eager(data)) @@ -204,7 +198,7 @@ def test_hist_bin_and_bin_count() -> None: @param_include_breakpoint @param_name -def test_hist_no_data(name: ConstructorName, *, include_breakpoint: bool) -> None: +def test_hist_no_data(name: str, *, include_breakpoint: bool) -> None: constructor_eager = maybe_name_to_constructor(name) s = nw.from_native(constructor_eager({"values": []})).select( nw.col("values").cast(nw.Float64) @@ -226,7 +220,7 @@ def test_hist_no_data(name: ConstructorName, *, include_breakpoint: bool) -> Non @param_name -def test_hist_small_bins(name: ConstructorName) -> None: +def test_hist_small_bins(name: str) -> None: constructor_eager = maybe_name_to_constructor(name) s = nw.from_native(constructor_eager({"values": [1, 2, 3]})) result = s["values"].hist(bins=None, bin_count=None) @@ -279,7 +273,7 @@ def test_hist_non_monotonic(constructor_eager: ConstructorEager) -> None: @pytest.mark.filterwarnings("ignore:invalid value encountered in cast:RuntimeWarning") @pytest.mark.slow def test_hist_bin_hypotheis( - name: ConstructorName, data: list[float], bin_deltas: list[float] + name: str, data: list[float], bin_deltas: list[float] ) -> None: constructor_eager = maybe_name_to_constructor(name) pytest.importorskip("polars") @@ -320,10 +314,7 @@ def test_hist_bin_hypotheis( @param_name @pytest.mark.slow def test_hist_count_hypothesis( - name: ConstructorName, - data: list[float], - bin_count: int, - request: pytest.FixtureRequest, + name: str, data: list[float], bin_count: int, request: pytest.FixtureRequest ) -> None: pytest.importorskip("polars") import polars as pl diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index 916f1b5499..a43a6543cf 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -7,23 +7,20 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.testing.constructors import ( - ConstructorName, - get_constructor, - prepare_constructors, -) -from narwhals.testing.constructors._classes import ( ConstructorBase, DaskConstructor, PandasConstructor, PolarsEagerConstructor as OriginalPolarsEagerConstructor, + get_constructor, + prepare_constructors, ) if TYPE_CHECKING: from typing_extensions import TypeAlias PropertyName: TypeAlias = str - ReturnTrue: TypeAlias = set[ConstructorName] - ReturnFalse: TypeAlias = set[ConstructorName] + TrueNames: TypeAlias = set[str] + FalseNames: TypeAlias = set[str] def test_dask_npartitions_distinct() -> None: @@ -37,64 +34,66 @@ def test_dask_repr() -> None: def test_eager_returns_eager_frame() -> None: - cn = ConstructorName.PANDAS - if not cn.is_available: + c = get_constructor("pandas") + if not c.is_available: pytest.skip() - df = nw.from_native(cn.constructor({"x": [1, 2, 3]})) + df = nw.from_native(c({"x": [1, 2, 3]})) assert isinstance(df, nw.DataFrame) def test_lazy_returns_lazy_frame() -> None: - cn = ConstructorName.POLARS_LAZY - if not cn.is_available: + c = get_constructor("polars[lazy]") + if not c.is_available: pytest.skip() - lf = nw.from_native(cn.constructor({"x": [1, 2, 3]})) + lf = nw.from_native(c({"x": [1, 2, 3]})) assert isinstance(lf, nw.LazyFrame) -CN = ConstructorName - - -_IS_PROPERTY_CASES: list[tuple[PropertyName, ReturnTrue, ReturnFalse]] = [ - ("is_pandas", {CN.PANDAS, CN.PANDAS_NULLABLE, CN.PANDAS_PYARROW}, {CN.POLARS_EAGER}), - ("is_modin", {CN.MODIN, CN.MODIN_PYARROW}, {CN.PANDAS}), - ("is_cudf", {CN.CUDF}, {CN.PANDAS}), - ("is_pandas_like", {CN.PANDAS, CN.MODIN, CN.CUDF}, {CN.POLARS_EAGER}), - ("is_polars", {CN.POLARS_EAGER, CN.POLARS_LAZY}, {CN.PANDAS}), - ("is_pyarrow", {CN.PYARROW}, {CN.PANDAS}), - ("is_dask", {CN.DASK}, {CN.PANDAS}), - ("is_duckdb", {CN.DUCKDB}, {CN.PANDAS}), - ("is_pyspark", {CN.PYSPARK, CN.PYSPARK_CONNECT}, {CN.PANDAS}), - ("is_sqlframe", {CN.SQLFRAME}, {CN.PANDAS}), - ("is_ibis", {CN.IBIS}, {CN.PANDAS}), - ("is_spark_like", {CN.PYSPARK, CN.SQLFRAME, CN.PYSPARK_CONNECT}, {CN.PANDAS}), - ("is_lazy", {CN.POLARS_LAZY, CN.DASK, CN.DUCKDB}, {CN.PANDAS}), - ("needs_pyarrow", {CN.PYARROW, CN.DUCKDB, CN.IBIS}, {CN.PANDAS}), - ("is_non_nullable", {CN.PANDAS, CN.MODIN, CN.DASK}, {CN.POLARS_EAGER}), +_IS_PROPERTY_CASES: list[tuple[PropertyName, TrueNames, FalseNames]] = [ + ("is_pandas", {"pandas", "pandas[nullable]", "pandas[pyarrow]"}, {"polars[eager]"}), + ("is_modin", {"modin", "modin[pyarrow]"}, {"pandas"}), + ("is_cudf", {"cudf"}, {"pandas"}), + ("is_pandas_like", {"pandas", "modin", "cudf"}, {"polars[eager]"}), + ("is_polars", {"polars[eager]", "polars[lazy]"}, {"pandas"}), + ("is_pyarrow", {"pyarrow"}, {"pandas"}), + ("is_dask", {"dask"}, {"pandas"}), + ("is_duckdb", {"duckdb"}, {"pandas"}), + ("is_pyspark", {"pyspark", "pyspark[connect]"}, {"pandas"}), + ("is_sqlframe", {"sqlframe"}, {"pandas"}), + ("is_ibis", {"ibis"}, {"pandas"}), + ("is_spark_like", {"pyspark", "sqlframe", "pyspark[connect]"}, {"pandas"}), + ("is_lazy", {"polars[lazy]", "dask", "duckdb"}, {"pandas"}), + ("needs_pyarrow", {"pyarrow", "duckdb", "ibis"}, {"pandas"}), + ("is_non_nullable", {"pandas", "modin", "dask"}, {"polars[eager]"}), ] @pytest.mark.parametrize(("prop", "true_names", "false_names"), _IS_PROPERTY_CASES) -def test_constructor_name_is_properties( - prop: str, true_names: set[ConstructorName], false_names: set[ConstructorName] +def test_constructor_is_properties( + prop: str, true_names: TrueNames, false_names: FalseNames ) -> None: for name in true_names: - assert getattr(name, prop), f"{name}.{prop} should be True" + c = get_constructor(name) + assert getattr(c, prop), f"{name}.{prop} should be True" for name in false_names: - assert not getattr(name, prop), f"{name}.{prop} should be False" + c = get_constructor(name) + assert not getattr(c, prop), f"{name}.{prop} should be False" -def test_constructor_name_implementation() -> None: - assert CN.PANDAS.implementation is Implementation.PANDAS - assert CN.PANDAS_PYARROW.implementation is Implementation.PANDAS - assert CN.POLARS_EAGER.implementation is Implementation.POLARS - assert CN.PYSPARK_CONNECT.implementation is Implementation.PYSPARK_CONNECT +def test_constructor_implementation() -> None: + assert get_constructor("pandas").implementation is Implementation.PANDAS + assert get_constructor("pandas[pyarrow]").implementation is Implementation.PANDAS + assert get_constructor("polars[eager]").implementation is Implementation.POLARS + assert ( + get_constructor("pyspark[connect]").implementation + is Implementation.PYSPARK_CONNECT + ) def test_constructor_dunder() -> None: - c1 = ConstructorName.PANDAS.constructor + c1 = get_constructor("pandas") c2 = PandasConstructor() assert c1.identifier == "pandas" assert c1 == c2 @@ -107,13 +106,13 @@ def test_init_subclass_no_legacy_name() -> None: class _Dummy( ConstructorBase, implementation=Implementation.POLARS, requirements=("polars",) ): - name = ConstructorName.POLARS_EAGER + name = "polars[eager]" def __call__(self, obj: object, /, **kwds: object) -> None: # type: ignore[override] ... # pragma: no cover - # re-registered POLARS_EAGER (overwriting the real one), but without a legacy_name for it. - registered = ConstructorBase._registry[ConstructorName.POLARS_EAGER] + # re-registered polars[eager] (overwriting the real one), but without a legacy_name for it. + registered = ConstructorBase._registry["polars[eager]"] assert registered == _Dummy() assert registered.legacy_name == "" assert registered.requirements == ("polars",) @@ -128,11 +127,11 @@ class PolarsEagerConstructor( requirements=("polars",), legacy_name=legacy_name, ): - name = ConstructorName.POLARS_EAGER + name = "polars[eager]" original = PolarsEagerConstructor() - restored = ConstructorBase._registry[ConstructorName.POLARS_EAGER] + restored = ConstructorBase._registry["polars[eager]"] assert restored == original assert restored.legacy_name == legacy_name @@ -141,15 +140,14 @@ def test_init_subclass_requires_implementation() -> None: with pytest.raises(TypeError, match="missing `implementation`"): class _BadConstructor(ConstructorBase, requirements=("polars",)): - name = ConstructorName.POLARS_EAGER + name = "polars[eager]" def __call__(self, obj: object, /, **kwds: object) -> None: # type: ignore[override] ... # pragma: no cover def test_get_constructor() -> None: - expected = ConstructorName.PANDAS_PYARROW.constructor - assert get_constructor("pandas[pyarrow]") == expected + assert get_constructor("pandas[pyarrow]") == get_constructor("pandas[pyarrow]") def test_get_constructor_invalid_name() -> None: @@ -158,6 +156,6 @@ def test_get_constructor_invalid_name() -> None: def test_prepare_constructors_exclude_only() -> None: - result = prepare_constructors(exclude=[ConstructorName.PANDAS]) + result = prepare_constructors(exclude=["pandas"]) names = {c.name for c in result} - assert ConstructorName.PANDAS not in names + assert "pandas" not in names diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index f5b8d635d4..f91454b153 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -30,7 +30,7 @@ import narwhals as nw from narwhals._utils import Version -from narwhals.testing.constructors._classes import SQLFrameConstructor +from narwhals.testing.constructors import SQLFrameConstructor from tests.utils import Constructor, maybe_get_modin_df if TYPE_CHECKING: From 90d90c028ef3bb1cf845f5da94eaf79f226df5cb Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 17 Apr 2026 23:38:21 +0200 Subject: [PATCH 30/71] remove pytest-cov, coverage: run -> combine -> report --- .github/workflows/extremes.yml | 20 +++++++++++++++----- .github/workflows/pytest-pyspark.yml | 10 ++++++++-- .github/workflows/pytest.yml | 24 +++++++++++++++++++----- .github/workflows/random_ci_pytest.yml | 5 +++-- CONTRIBUTING.md | 2 +- Makefile | 3 ++- pyproject.toml | 1 - 7 files changed, 48 insertions(+), 17 deletions(-) diff --git a/.github/workflows/extremes.yml b/.github/workflows/extremes.yml index 296ce0efaf..f18ee5a697 100644 --- a/.github/workflows/extremes.yml +++ b/.github/workflows/extremes.yml @@ -45,7 +45,10 @@ jobs: echo "$DEPS" | grep 'scikit-learn==1.1.0' echo "$DEPS" | grep 'duckdb==1.1' - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb + run: | + coverage run -m pytest tests --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb + coverage combine + coverage report --fail-under=50 pretty_old_versions: strategy: @@ -83,7 +86,10 @@ jobs: echo "$DEPS" | grep 'scikit-learn==1.1.0' echo "$DEPS" | grep 'duckdb==1.2' - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb + run: | + coverage run -m pytest tests --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb + coverage combine + coverage report --fail-under=50 not_so_old_versions: strategy: @@ -120,7 +126,10 @@ jobs: echo "$DEPS" | grep 'dask==2024.10' echo "$DEPS" | grep 'duckdb==1.3' - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb + run: | + coverage run -m pytest tests --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb + coverage combine + coverage report --fail-under=50 nightlies: strategy: @@ -176,5 +185,6 @@ jobs: echo "$DEPS" | grep 'dask.*@' - name: Run pytest run: | - pytest tests --cov=narwhals --cov=tests --cov-fail-under=50 --runslow \ - --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb + coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb + coverage combine + coverage report --fail-under=50 diff --git a/.github/workflows/pytest-pyspark.yml b/.github/workflows/pytest-pyspark.yml index 0c003b8929..125310e5cf 100644 --- a/.github/workflows/pytest-pyspark.yml +++ b/.github/workflows/pytest-pyspark.yml @@ -41,7 +41,10 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors pyspark + run: | + coverage run -m pytest tests --runslow --constructors pyspark + coverage combine + coverage report --fail-under=95 --include "narwhals/_spark_like/*" pytest-pyspark-min-version-constructor: @@ -134,7 +137,10 @@ jobs: echo "Spark Connect server started" - name: Run pytest - run: pytest tests --cov=narwhals/_spark_like --cov-fail-under=95 --runslow --constructors "pyspark[connect]" + run: | + coverage run -m pytest tests --runslow --constructors "pyspark[connect]" + coverage combine + coverage report --fail-under=95 --include="narwhals/_spark_like/*" - name: Stop Spark Connect server if: always() diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 683f9ecfe7..fd4e92207c 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -31,7 +31,10 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] + run: | + coverage run -m pytest tests --constructors=pandas,pyarrow,polars[eager],polars[lazy] + coverage combine + coverage report --fail-under=75 - name: install-test-plugin run: uv pip install -e test-plugin/. @@ -61,7 +64,9 @@ jobs: run: uv pip freeze - name: Run pytest run: | - pytest tests --cov=narwhals --cov=tests --runslow --cov-fail-under=95 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 + coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 + coverage combine + coverage report --fail-under=95 pytest-full-coverage: strategy: @@ -92,7 +97,10 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=100 --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 + run: | + coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 + coverage combine + coverage report --fail-under=100 - name: Run doctests # reprs differ between versions, so we only run doctests on the latest Python if: matrix.python-version == '3.13' @@ -158,7 +166,10 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --cov-fail-under=50 + run: | + coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 + coverage combine + coverage report --fail-under=100 python-314t: strategy: @@ -184,4 +195,7 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --cov=narwhals --cov=tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow --cov-fail-under=50 + run: | + coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow + coverage combine + coverage report --fail-under=50 diff --git a/.github/workflows/random_ci_pytest.yml b/.github/workflows/random_ci_pytest.yml index 900985c9cc..8531c0e639 100644 --- a/.github/workflows/random_ci_pytest.yml +++ b/.github/workflows/random_ci_pytest.yml @@ -37,5 +37,6 @@ jobs: run: uv pip freeze - name: Run pytest run: | - pytest tests --cov=narwhals --cov=tests --cov-fail-under=75 \ - --constructors=pandas,pyarrow,polars[eager],polars[lazy] + coverage run -m pytest tests --constructors=pandas,pyarrow,polars[eager],polars[lazy] + coverage combine + coverage report --fail-under=75 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 98a274e4ed..3dc3129c66 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -149,7 +149,7 @@ If you add code that should be tested, please add tests. - To run tests, run `pytest`. To check coverage: `pytest --cov=narwhals` - To run tests on the doctests, use `pytest narwhals --doctest-modules` -- To run unit tests and doctests at the same time, run `pytest tests narwhals --cov=narwhals --doctest-modules` +- To run unit tests and doctests at the same time, run `pytest tests narwhals --doctest-modules` - To run tests multiprocessed, you may also want to use [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) (optional) - To choose which backends to run tests with you, you can use the `--constructors` flag: - To only run tests for pandas, Polars, and PyArrow, use `pytest --constructors=pandas,pyarrow,polars` diff --git a/Makefile b/Makefile index 824f91f746..4010bd8a74 100644 --- a/Makefile +++ b/Makefile @@ -47,5 +47,6 @@ test: ## Run unittest --editable .[ibis,modin,pyspark] \ --group core \ --group tests - $(VENV_BIN)/uv run --no-sync coverage run -m pytest tests --all-cpu-constructors + $(VENV_BIN)/uv run --no-sync coverage run -m pytest tests --all-cpu-constructors --numprocesses=logical + $(VENV_BIN)/uv run --no-sync coverage combine $(VENV_BIN)/uv run --no-sync coverage report --fail-under=95 diff --git a/pyproject.toml b/pyproject.toml index df9fefe90e..0406a068d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,6 @@ core = [ tests = [ "covdefaults", "pytest", - "pytest-cov", "pytest-env", "pytest-randomly", "pytest-xdist", From b0fbd1bc9cd631963064ee9e53e38e991c48f733 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 00:11:36 +0200 Subject: [PATCH 31/71] add patch=subprocess option --- pyproject.toml | 1 + tests/expr_and_series/nth_test.py | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0406a068d3..7699d02826 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -302,6 +302,7 @@ env = [ ] [tool.coverage.run] +patch = ["subprocess"] plugins = ["covdefaults"] source = ["narwhals", "tests"] parallel = true diff --git a/tests/expr_and_series/nth_test.py b/tests/expr_and_series/nth_test.py index 1249f7f2e2..86f9bfe2eb 100644 --- a/tests/expr_and_series/nth_test.py +++ b/tests/expr_and_series/nth_test.py @@ -1,17 +1,14 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw from tests.utils import POLARS_VERSION, Constructor, assert_equal_data -if TYPE_CHECKING: - from collections.abc import Mapping - -data: Mapping[str, Any] = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8.0, 9.0]} +data: dict[str, list[Any]] = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8.0, 9.0]} @pytest.mark.parametrize( From edb0263199eba9e85ed70af9a234823c9ddd0d26 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 00:19:42 +0200 Subject: [PATCH 32/71] pragma: no cover constructors as current main --- narwhals/testing/constructors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index eda37a29d8..f92f268d87 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -353,7 +353,7 @@ class ModinPyArrowConstructor( implementation=Implementation.MODIN, requirements=("modin", "pyarrow"), legacy_name="modin_pyarrow_constructor", -): +): # pragma: no cover """Constructor backed by `modin.pandas.DataFrame` with `pyarrow` dtypes.""" name = "modin[pyarrow]" @@ -537,7 +537,7 @@ class IbisConstructor( implementation=Implementation.IBIS, requirements=("ibis", "duckdb", "pyarrow"), legacy_name="ibis_lazy_constructor", -): +): # pragma: no cover """Constructor backed by `ibis` (DuckDB backend).""" name = "ibis" From 06a69616670f03931541f4238f17809b0dfb1e8b Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 15:15:19 +0200 Subject: [PATCH 33/71] add patches to coveragepy --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7699d02826..75685c4b29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -302,7 +302,7 @@ env = [ ] [tool.coverage.run] -patch = ["subprocess"] +patch = ["execv", "fork", "subprocess"] plugins = ["covdefaults"] source = ["narwhals", "tests"] parallel = true From 024640aeb3de0197dec12c1a5c1aa784237e9ff3 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 15:15:41 +0200 Subject: [PATCH 34/71] remove mapping hint in tests --- tests/frame/interchange_native_namespace_test.py | 11 ++++------- tests/frame/interchange_select_test.py | 7 ++----- tests/frame/interchange_to_arrow_test.py | 7 ++----- tests/frame/to_polars_test.py | 7 +++---- tests/hypothesis/getitem_test.py | 11 ++++------- tests/hypothesis/join_test.py | 2 +- tests/read_scan_test.py | 2 +- 7 files changed, 17 insertions(+), 30 deletions(-) diff --git a/tests/frame/interchange_native_namespace_test.py b/tests/frame/interchange_native_namespace_test.py index 79a92ef6c9..0face73928 100644 --- a/tests/frame/interchange_native_namespace_test.py +++ b/tests/frame/interchange_native_namespace_test.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest @@ -9,10 +9,7 @@ pytest.importorskip("polars") import polars as pl -if TYPE_CHECKING: - from collections.abc import Mapping - -data: Mapping[str, Any] = {"a": [1, 2, 3], "b": [4.5, 6.7, 8.9], "z": ["x", "y", "w"]} +data: dict[str, list[Any]] = {"a": [1, 2, 3], "b": [4.5, 6.7, 8.9], "z": ["x", "y", "w"]} def test_interchange() -> None: @@ -60,9 +57,9 @@ def test_duckdb() -> None: pytest.importorskip("duckdb") import duckdb - df_pl = pl.DataFrame(data) # noqa: F841 + _df_pl = pl.DataFrame(data) - rel = duckdb.sql("select * from df_pl") + rel = duckdb.sql("select * from _df_pl") df = nw_v1.from_native(rel, eager_or_interchange_only=True) series = df["a"] diff --git a/tests/frame/interchange_select_test.py b/tests/frame/interchange_select_test.py index a927ba18c6..90279f0296 100644 --- a/tests/frame/interchange_select_test.py +++ b/tests/frame/interchange_select_test.py @@ -1,16 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals as nw import narwhals.stable.v1 as nw_v1 -if TYPE_CHECKING: - from collections.abc import Mapping - -data: Mapping[str, Any] = {"a": [1, 2, 3], "b": [4.0, 5.0, 6.1], "z": ["x", "y", "z"]} +data: dict[str, list[Any]] = {"a": [1, 2, 3], "b": [4.0, 5.0, 6.1], "z": ["x", "y", "z"]} class InterchangeDataFrame: diff --git a/tests/frame/interchange_to_arrow_test.py b/tests/frame/interchange_to_arrow_test.py index 2277d498ea..e8604f816d 100644 --- a/tests/frame/interchange_to_arrow_test.py +++ b/tests/frame/interchange_to_arrow_test.py @@ -1,15 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest import narwhals.stable.v1 as nw_v1 -if TYPE_CHECKING: - from collections.abc import Mapping - -data: Mapping[str, Any] = {"a": [1, 2, 3], "b": [4.0, 5.0, 6.1], "z": ["x", "y", "z"]} +data: dict[str, list[Any]] = {"a": [1, 2, 3], "b": [4.0, 5.0, 6.1], "z": ["x", "y", "z"]} pytest.importorskip("polars") pytest.importorskip("pyarrow") diff --git a/tests/frame/to_polars_test.py b/tests/frame/to_polars_test.py index 60ca653f32..89d4a65b2a 100644 --- a/tests/frame/to_polars_test.py +++ b/tests/frame/to_polars_test.py @@ -1,14 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import pytest import narwhals as nw if TYPE_CHECKING: - from collections.abc import Mapping - + from narwhals.testing.typing import Data from tests.utils import ConstructorEager pytest.importorskip("polars") @@ -20,7 +19,7 @@ def test_convert_polars(constructor_eager: ConstructorEager) -> None: pytest.importorskip("pyarrow") from polars.testing import assert_frame_equal - data: Mapping[str, Any] = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8.0, 9.0]} + data: Data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8.0, 9.0]} df_raw = constructor_eager(data) result = nw.from_native(df_raw).to_polars() diff --git a/tests/hypothesis/getitem_test.py b/tests/hypothesis/getitem_test.py index 9ce1ba38b1..e001d55d21 100644 --- a/tests/hypothesis/getitem_test.py +++ b/tests/hypothesis/getitem_test.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import TYPE_CHECKING, Any, cast import hypothesis.strategies as st import pytest @@ -13,8 +13,7 @@ if TYPE_CHECKING: from collections.abc import Sequence - from narwhals.testing.typing import ConstructorBase - from narwhals.typing import IntoDataFrame + from narwhals.testing.typing import ConstructorEager pytest.importorskip("pandas") pytest.importorskip("polars") @@ -22,9 +21,7 @@ @pytest.fixture(params=[PandasConstructor(), PyArrowConstructor()], scope="module") -def pandas_or_pyarrow_constructor( - request: pytest.FixtureRequest, -) -> Callable[[Any], IntoDataFrame]: +def pandas_or_pyarrow_constructor(request: pytest.FixtureRequest) -> ConstructorEager: return request.param # type: ignore[no-any-return] @@ -118,7 +115,7 @@ def tuple_selector(draw: st.DrawFn) -> tuple[Any, Any]: @given(selector=st.one_of(single_selector, tuple_selector())) @pytest.mark.slow -def test_getitem(pandas_or_pyarrow_constructor: ConstructorBase, selector: Any) -> None: +def test_getitem(pandas_or_pyarrow_constructor: ConstructorEager, selector: Any) -> None: """Compare __getitem__ against polars.""" # TODO(PR - clean up): documenting current differences # These assume(...) lines each filter out a known difference. diff --git a/tests/hypothesis/join_test.py b/tests/hypothesis/join_test.py index 037854a861..c380ee73a6 100644 --- a/tests/hypothesis/join_test.py +++ b/tests/hypothesis/join_test.py @@ -44,7 +44,7 @@ def test_join( # pragma: no cover floats: st.SearchStrategy[list[float]], cols: st.SearchStrategy[list[str]], ) -> None: - data: Mapping[str, Any] = {"a": integers, "b": other_integers, "c": floats} + data: dict[str, Any] = {"a": integers, "b": other_integers, "c": floats} join_cols = cast("list[str]", cols) df_polars = pl.DataFrame(data) diff --git a/tests/read_scan_test.py b/tests/read_scan_test.py index aeee5229d7..0e0c94d994 100644 --- a/tests/read_scan_test.py +++ b/tests/read_scan_test.py @@ -27,7 +27,7 @@ IOSourceKind: TypeAlias = Literal["str", "Path", "PathLike"] -data: Mapping[str, Any] = {"a": [1, 2, 3], "b": [4.5, 6.7, 8.9], "z": ["x", "y", "w"]} +data: dict[str, list[Any]] = {"a": [1, 2, 3], "b": [4.5, 6.7, 8.9], "z": ["x", "y", "w"]} skipif_pandas_lt_1_5 = pytest.mark.skipif( PANDAS_VERSION < (1, 5), reason="too old for pyarrow" ) From 7d127f7ea8e62273582d503b9d4a17af1e8decef Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 15:28:05 +0200 Subject: [PATCH 35/71] rename base classes --- narwhals/testing/constructors.py | 63 +++++++++++++++--------------- narwhals/testing/pytest_plugin.py | 4 +- narwhals/testing/typing.py | 25 +++++------- tests/testing/constructors_test.py | 10 ++--- tests/testing/plugin_test.py | 12 +++--- tests/utils.py | 5 ++- 6 files changed, 57 insertions(+), 62 deletions(-) diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index f92f268d87..e8a0149cea 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -11,15 +11,15 @@ 1. Choose the right base class: - * `ConstructorEagerBase`: if the backend returns an eager dataframe. - * `ConstructorLazyBase`: if the backend returns a lazy frame. + * `EagerFrameConstructor`: if the backend returns an eager dataframe. + * `LazyFrameConstructor`: if the backend returns a lazy frame. 2. Define the class in this module and declare its metadata in the class header as keyword arguments: ```py class MyBackendConstructor( - ConstructorLazyBase, + LazyFrameConstructor, implementation=Implementation.MY_BACKEND, requirements=("my_backend",), legacy_name="my_backend_lazy_constructor", @@ -32,7 +32,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ...: return my_backend.from_dict(obj) ``` -That is all. `__init_subclass__` on `ConstructorBase` automatically registers +That is all. `__init_subclass__` on `FrameConstructor` automatically registers a default singleton into `_registry`, keyed by the string `name`. """ @@ -68,8 +68,8 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ...: "ALL_CONSTRUCTORS", "ALL_CPU_CONSTRUCTORS", "DEFAULT_CONSTRUCTORS", - "ConstructorBase", - "ConstructorEagerBase", + "EagerFrameConstructor", + "FrameConstructor", "available_constructors", "get_constructor", "is_backend_available", @@ -79,7 +79,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ...: ) -class ConstructorBase(Protocol): +class FrameConstructor(Protocol): """Abstract base for any constructor exposed by `narwhals.testing`. A constructor is a callable that turns a column-oriented `dict` (typed as @@ -92,7 +92,7 @@ class ConstructorBase(Protocol): default singleton into `_registry`. """ - _registry: ClassVar[dict[str, ConstructorBase]] = {} + _registry: ClassVar[dict[str, FrameConstructor]] = {} name: ClassVar[str] implementation: ClassVar[Implementation] @@ -141,7 +141,7 @@ def __init_subclass__( "kwarg in its class header." ) raise TypeError(msg) - ConstructorBase._registry[cls.name] = cls() + FrameConstructor._registry[cls.name] = cls() def __call__(self, obj: Data, /, **kwds: Any) -> IntoFrame: """Build a native frame from `obj`.""" @@ -242,11 +242,12 @@ def __hash__(self) -> int: def __eq__(self, other: object) -> bool: return ( - type(self) is type(other) and self.name == cast("ConstructorBase", other).name + type(self) is type(other) + and self.name == cast("FrameConstructor", other).name ) -class ConstructorEagerBase(ConstructorBase): +class EagerFrameConstructor(FrameConstructor): """A constructor that returns an *eager* native dataframe.""" is_eager: ClassVar[bool] = True @@ -254,7 +255,7 @@ class ConstructorEagerBase(ConstructorBase): def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: ... -class ConstructorLazyBase(ConstructorBase): +class LazyFrameConstructor(FrameConstructor): """A constructor that returns a *lazy* native frame.""" is_eager: ClassVar[bool] = False @@ -266,7 +267,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> IntoLazyFrame: ... class PandasConstructor( - ConstructorEagerBase, + EagerFrameConstructor, implementation=Implementation.PANDAS, requirements=("pandas",), legacy_name="pandas_constructor", @@ -283,7 +284,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: class PandasNullableConstructor( - ConstructorEagerBase, + EagerFrameConstructor, implementation=Implementation.PANDAS, requirements=("pandas",), legacy_name="pandas_nullable_constructor", @@ -299,7 +300,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: class PandasPyArrowConstructor( - ConstructorEagerBase, + EagerFrameConstructor, implementation=Implementation.PANDAS, requirements=("pandas", "pyarrow"), legacy_name="pandas_pyarrow_constructor", @@ -315,7 +316,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: class PyArrowConstructor( - ConstructorEagerBase, + EagerFrameConstructor, implementation=Implementation.PYARROW, requirements=("pyarrow",), legacy_name="pyarrow_table_constructor", @@ -331,7 +332,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> pa.Table: class ModinConstructor( - ConstructorEagerBase, + EagerFrameConstructor, implementation=Implementation.MODIN, requirements=("modin",), legacy_name="modin_constructor", @@ -349,7 +350,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: class ModinPyArrowConstructor( - ConstructorEagerBase, + EagerFrameConstructor, implementation=Implementation.MODIN, requirements=("modin", "pyarrow"), legacy_name="modin_pyarrow_constructor", @@ -369,7 +370,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: class CudfConstructor( - ConstructorEagerBase, + EagerFrameConstructor, implementation=Implementation.CUDF, requirements=("cudf",), legacy_name="cudf_constructor", @@ -386,7 +387,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: class PolarsEagerConstructor( - ConstructorEagerBase, + EagerFrameConstructor, implementation=Implementation.POLARS, requirements=("polars",), legacy_name="polars_eager_constructor", @@ -405,7 +406,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> pl.DataFrame: class PolarsLazyConstructor( - ConstructorLazyBase, + LazyFrameConstructor, implementation=Implementation.POLARS, requirements=("polars",), legacy_name="polars_lazy_constructor", @@ -421,7 +422,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> pl.LazyFrame: class DaskConstructor( - ConstructorLazyBase, + LazyFrameConstructor, implementation=Implementation.DASK, requirements=("dask",), legacy_name="dask_lazy_p2_constructor", @@ -462,7 +463,7 @@ def __eq__(self, other: object) -> bool: class DuckDBConstructor( - ConstructorLazyBase, + LazyFrameConstructor, implementation=Implementation.DUCKDB, requirements=("duckdb", "pyarrow"), legacy_name="duckdb_lazy_constructor", @@ -481,7 +482,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> NativeDuckDB: class PySparkConstructor( - ConstructorLazyBase, + LazyFrameConstructor, implementation=Implementation.PYSPARK, requirements=("pyspark",), legacy_name="pyspark_lazy_constructor", @@ -516,7 +517,7 @@ class PySparkConnectConstructor( class SQLFrameConstructor( - ConstructorLazyBase, + LazyFrameConstructor, implementation=Implementation.SQLFRAME, requirements=("sqlframe", "duckdb"), legacy_name="sqlframe_pyspark_lazy_constructor", @@ -533,7 +534,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> NativeSQLFrame: class IbisConstructor( - ConstructorLazyBase, + LazyFrameConstructor, implementation=Implementation.IBIS, requirements=("ibis", "duckdb", "pyarrow"), legacy_name="ibis_lazy_constructor", @@ -550,7 +551,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ibis.Table: return _ibis_backend().create_table(table_name, table, **kwds) -ALL_CONSTRUCTORS: dict[str, ConstructorBase] = ConstructorBase._registry +ALL_CONSTRUCTORS: dict[str, FrameConstructor] = FrameConstructor._registry """All registered constructors keyed by their string identifier.""" DEFAULT_CONSTRUCTORS: frozenset[str] = frozenset( @@ -569,7 +570,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ibis.Table: """ ALL_CPU_CONSTRUCTORS: frozenset[str] = frozenset( - name for name, c in ConstructorBase._registry.items() if not c.needs_gpu + name for name, c in FrameConstructor._registry.items() if not c.needs_gpu ) """All constructors that do not require GPU hardware.""" @@ -585,7 +586,7 @@ def available_constructors() -> frozenset[str]: return frozenset(name for name, c in ALL_CONSTRUCTORS.items() if c.is_available) -def get_constructor(name: str) -> ConstructorBase: +def get_constructor(name: str) -> FrameConstructor: """Return the registered singleton constructor for `name`. Arguments: @@ -610,7 +611,7 @@ def get_constructor(name: str) -> ConstructorBase: def prepare_constructors( *, include: Iterable[str] | None = None, exclude: Iterable[str] | None = None -) -> list[ConstructorBase]: +) -> list[FrameConstructor]: """Return available constructors, optionally filtered. Arguments: @@ -622,7 +623,7 @@ def prepare_constructors( >>> constructors = prepare_constructors(include=["pandas", "polars[eager]"]) """ available = available_constructors() - candidates: list[ConstructorBase] = [ + candidates: list[FrameConstructor] = [ c for name, c in ALL_CONSTRUCTORS.items() if name in available ] if include is not None: diff --git a/narwhals/testing/pytest_plugin.py b/narwhals/testing/pytest_plugin.py index f75d05b532..9d08fa380e 100644 --- a/narwhals/testing/pytest_plugin.py +++ b/narwhals/testing/pytest_plugin.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: import pytest - from narwhals.testing.constructors import ConstructorBase + from narwhals.testing.constructors import FrameConstructor _MIN_PANDAS_NULLABLE_VERSION: tuple[int, ...] = (2, 0, 0) @@ -89,7 +89,7 @@ def pytest_addoption(parser: pytest.Parser) -> None: def _select_constructors( config: pytest.Config, -) -> list[ConstructorBase]: # pragma: no cover +) -> list[FrameConstructor]: # pragma: no cover from narwhals.testing.constructors import ALL_CPU_CONSTRUCTORS, prepare_constructors _all_cpu_exclusions = frozenset({"modin", "pyspark[connect]"}) diff --git a/narwhals/testing/typing.py b/narwhals/testing/typing.py index 62b07873ce..19aabb9e3b 100644 --- a/narwhals/testing/typing.py +++ b/narwhals/testing/typing.py @@ -2,25 +2,18 @@ from typing import TYPE_CHECKING, Any +from narwhals.testing.constructors import ( + EagerFrameConstructor, + FrameConstructor, + LazyFrameConstructor, +) + if TYPE_CHECKING: from typing_extensions import TypeAlias - from narwhals.testing.constructors import ( - ConstructorBase, - ConstructorEagerBase, - ConstructorLazyBase, - ) - -Data: TypeAlias = dict[str, Any] # TODO(Unassined): This should have a better annotation -"""A column-oriented mapping used as input to a [`Constructor`][].""" -Constructor: TypeAlias = "ConstructorBase" -"""Any constructor (eager or lazy): callable that returns a native frame.""" +__all__ = ("Data", "EagerFrameConstructor", "FrameConstructor", "LazyFrameConstructor") -ConstructorEager: TypeAlias = "ConstructorEagerBase" -"""A constructor that returns an eager native dataframe.""" -ConstructorLazy: TypeAlias = "ConstructorLazyBase" -"""A constructor that returns a lazy native frame.""" - -__all__ = ["Constructor", "ConstructorEager", "ConstructorLazy", "Data"] +Data: TypeAlias = dict[str, Any] # TODO(Unassined): This should have a better annotation +"""A column-oriented mapping used as input to a [`Constructor`][].""" diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index a43a6543cf..a0720d75ca 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -7,8 +7,8 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.testing.constructors import ( - ConstructorBase, DaskConstructor, + FrameConstructor, PandasConstructor, PolarsEagerConstructor as OriginalPolarsEagerConstructor, get_constructor, @@ -104,7 +104,7 @@ def test_constructor_dunder() -> None: def test_init_subclass_no_legacy_name() -> None: class _Dummy( - ConstructorBase, implementation=Implementation.POLARS, requirements=("polars",) + FrameConstructor, implementation=Implementation.POLARS, requirements=("polars",) ): name = "polars[eager]" @@ -112,7 +112,7 @@ def __call__(self, obj: object, /, **kwds: object) -> None: # type: ignore[over ... # pragma: no cover # re-registered polars[eager] (overwriting the real one), but without a legacy_name for it. - registered = ConstructorBase._registry["polars[eager]"] + registered = FrameConstructor._registry["polars[eager]"] assert registered == _Dummy() assert registered.legacy_name == "" assert registered.requirements == ("polars",) @@ -131,7 +131,7 @@ class PolarsEagerConstructor( original = PolarsEagerConstructor() - restored = ConstructorBase._registry["polars[eager]"] + restored = FrameConstructor._registry["polars[eager]"] assert restored == original assert restored.legacy_name == legacy_name @@ -139,7 +139,7 @@ class PolarsEagerConstructor( def test_init_subclass_requires_implementation() -> None: with pytest.raises(TypeError, match="missing `implementation`"): - class _BadConstructor(ConstructorBase, requirements=("polars",)): + class _BadConstructor(FrameConstructor, requirements=("polars",)): name = "polars[eager]" def __call__(self, obj: object, /, **kwds: object) -> None: # type: ignore[override] diff --git a/tests/testing/plugin_test.py b/tests/testing/plugin_test.py index 7023ae4f41..2205ac0f20 100644 --- a/tests/testing/plugin_test.py +++ b/tests/testing/plugin_test.py @@ -15,9 +15,9 @@ def test_constructor_eager_fixture_runs_for_each_backend( pytester.makeconftest("") pytester.makepyfile(""" import narwhals as nw - from narwhals.testing.typing import ConstructorEager + from narwhals.testing.typing import EagerFrameConstructor - def test_shape(constructor_eager: ConstructorEager) -> None: + def test_shape(constructor_eager: EagerFrameConstructor) -> None: df = nw.from_native(constructor_eager({"x": [1, 2, 3]}), eager_only=True) assert df.shape == (3, 1) """) @@ -42,9 +42,9 @@ def test_constructor_fixture_includes_lazy_backends(pytester: pytest.Pytester) - pytester.makeconftest("") pytester.makepyfile(""" import narwhals as nw - from narwhals.testing.typing import Constructor + from narwhals.testing.typing import FrameConstructor - def test_columns(constructor: Constructor) -> None: + def test_columns(constructor: FrameConstructor) -> None: df = nw.from_native(constructor({"x": [1, 2, 3]})) assert df.collect_schema().names() == ["x"] """) @@ -57,9 +57,9 @@ def test_columns(constructor: Constructor) -> None: def test_external_constructor_disables_parametrisation(pytester: pytest.Pytester) -> None: pytester.makeconftest("") pytester.makepyfile(""" - from narwhals.testing.typing import ConstructorEager + from narwhals.testing.typing import EagerFrameConstructor - def test_unparam(constructor_eager: ConstructorEager) -> None: + def test_unparam(constructor_eager: EagerFrameConstructor) -> None: pass """) result = pytester.runpytest_subprocess("--use-external-constructor") diff --git a/tests/utils.py b/tests/utils.py index 86202b465b..d58794d609 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -14,8 +14,9 @@ from narwhals._utils import Implementation, parse_version, zip_strict from narwhals.dependencies import get_pandas from narwhals.testing.typing import ( - Constructor as Constructor, # noqa: PLC0414,TC001 - ConstructorEager as ConstructorEager, # noqa: PLC0414,TC001 + # TODO(FBruzzesi): Remove these aliases once all the test suite migrates to *FrameConstructor's + EagerFrameConstructor as ConstructorEager, # noqa: TC001 + FrameConstructor as Constructor, # noqa: TC001 ) from narwhals.translate import from_native From 76b5851278eecc33f49840f32b770588fcf6759b Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 19:04:56 +0200 Subject: [PATCH 36/71] rm execv patch flag --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 83e7de2540..733ababe2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -305,7 +305,7 @@ env = [ ] [tool.coverage.run] -patch = ["execv", "fork", "subprocess"] +patch = ["fork", "subprocess"] plugins = ["covdefaults"] source = ["narwhals", "tests"] parallel = true From e1749ac1d67edf7b92993b164fd40656bff7d8c4 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 19:55:10 +0200 Subject: [PATCH 37/71] addressing typing issues part 1 --- narwhals/testing/constructors.py | 44 +++++++------------------ tests/frame/join_test.py | 20 ++++------- tests/hypothesis/getitem_test.py | 16 ++++++--- tests/testing/constructors_test.py | 51 ++--------------------------- tests/translate/from_native_test.py | 8 ++--- tests/utils.py | 9 +++-- 6 files changed, 41 insertions(+), 107 deletions(-) diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index e8a0149cea..cf838d3098 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -145,7 +145,7 @@ def __init_subclass__( def __call__(self, obj: Data, /, **kwds: Any) -> IntoFrame: """Build a native frame from `obj`.""" - ... + raise NotImplementedError @property def identifier(self) -> str: @@ -252,7 +252,8 @@ class EagerFrameConstructor(FrameConstructor): is_eager: ClassVar[bool] = True - def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: ... + def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: + raise NotImplementedError class LazyFrameConstructor(FrameConstructor): @@ -260,7 +261,8 @@ class LazyFrameConstructor(FrameConstructor): is_eager: ClassVar[bool] = False - def __call__(self, obj: Data, /, **kwds: Any) -> IntoLazyFrame: ... + def __call__(self, obj: Data, /, **kwds: Any) -> IntoLazyFrame: + raise NotImplementedError # Eager constructors @@ -328,7 +330,7 @@ class PyArrowConstructor( def __call__(self, obj: Data, /, **kwds: Any) -> pa.Table: import pyarrow as pa - return pa.table(obj, **kwds) # type:ignore[arg-type] + return pa.table(obj, **kwds) class ModinConstructor( @@ -428,38 +430,14 @@ class DaskConstructor( legacy_name="dask_lazy_p2_constructor", is_non_nullable=True, ): # pragma: no cover - """Constructor backed by `dask.dataframe`. - - Arguments: - npartitions: Number of Dask partitions (default `1`). - """ + """Constructor backed by `dask.dataframe`.""" name = "dask" - def __init__(self, npartitions: int = 2) -> None: - self.npartitions = npartitions - - def __call__(self, obj: Data, /, **kwds: Any) -> NativeDask: + def __call__(self, obj: Data, /, npartitions: int = 2, **kwds: Any) -> NativeDask: import dask.dataframe as dd - return cast("NativeDask", dd.from_dict(obj, npartitions=self.npartitions, **kwds)) - - @property - def identifier(self) -> str: - """Identifier that encodes the number of partitions.""" - return f"dask[p{self.npartitions}]" - - def __repr__(self) -> str: - return f"{type(self).__name__}(npartitions={self.npartitions})" - - def __hash__(self) -> int: - return hash((type(self), self.name, self.npartitions)) - - def __eq__(self, other: object) -> bool: - return ( - type(self) is type(other) - and self.npartitions == cast("DaskConstructor", other).npartitions - ) + return cast("NativeDask", dd.from_dict(obj, npartitions=npartitions, **kwds)) class DuckDBConstructor( @@ -477,7 +455,7 @@ def __call__(self, obj: Data, /, **kwds: Any) -> NativeDuckDB: import pyarrow as pa duckdb.sql("""set timezone = 'UTC'""") - _df = pa.table(obj, **kwds) # type:ignore[arg-type] + _df = pa.table(obj, **kwds) return duckdb.sql("select * from _df") @@ -546,7 +524,7 @@ class IbisConstructor( def __call__(self, obj: Data, /, **kwds: Any) -> ibis.Table: import pyarrow as pa - table = pa.table(obj) # type:ignore[arg-type] + table = pa.table(obj) table_name = str(uuid.uuid4()) return _ibis_backend().create_table(table_name, table, **kwds) diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py index 42d52adafc..a259d57c17 100644 --- a/tests/frame/join_test.py +++ b/tests/frame/join_test.py @@ -754,13 +754,9 @@ def test_joinasof_by_exceptions( message: str, ) -> None: data = {ON: [1, 3, 2], BY: [4, 4, 6], "zor ro": [7.0, 8.0, 9.0]} - df = nw.from_native(constructor(data)) - if isinstance(df, nw.LazyFrame): - with pytest.raises(ValueError, match=message): - df.join_asof(df, on=on, by_left=by_left, by_right=by_right, by=by) - else: - with pytest.raises(ValueError, match=message): - df.join_asof(df, on=on, by_left=by_left, by_right=by_right, by=by) + df = from_native_lazy(constructor(data)) + with pytest.raises(ValueError, match=message): + df.join_asof(df, on=on, by_left=by_left, by_right=by_right, by=by).collect() def test_join_duplicate_column_names( @@ -777,7 +773,7 @@ def test_join_duplicate_column_names( ): request.applymarker(pytest.mark.xfail) data = {"a": [1, 2, 3, 4, 5], "b": [6, 6, 6, 6, 6]} - df = nw.from_native(constructor(data)) + df = from_native_lazy(constructor(data)) if any( x in str(constructor) for x in ("pandas", "pandas[pyarrow]", "pandas[nullable]", "dask") @@ -796,12 +792,8 @@ def test_join_duplicate_column_names( request.applymarker(pytest.mark.xfail) else: exception = nw.exceptions.DuplicateError - if isinstance(df, nw.LazyFrame): - with pytest.raises(exception): - df.join(df, on=["a"]).join(df, on=["a"]).collect() - else: - with pytest.raises(exception): - df.join(df, on=["a"]).join(df, on=["a"]) + with pytest.raises(exception): + df.join(df, on=["a"]).join(df, on=["a"]).collect() def test_join_same_laziness(constructor: Constructor) -> None: diff --git a/tests/hypothesis/getitem_test.py b/tests/hypothesis/getitem_test.py index e001d55d21..60419f48a2 100644 --- a/tests/hypothesis/getitem_test.py +++ b/tests/hypothesis/getitem_test.py @@ -7,21 +7,25 @@ from hypothesis import assume, given import narwhals as nw -from narwhals.testing.constructors import PandasConstructor, PyArrowConstructor +from narwhals.testing.constructors import get_constructor from tests.utils import assert_equal_data if TYPE_CHECKING: from collections.abc import Sequence - from narwhals.testing.typing import ConstructorEager + from narwhals.testing.typing import EagerFrameConstructor pytest.importorskip("pandas") pytest.importorskip("polars") import polars as pl -@pytest.fixture(params=[PandasConstructor(), PyArrowConstructor()], scope="module") -def pandas_or_pyarrow_constructor(request: pytest.FixtureRequest) -> ConstructorEager: +@pytest.fixture( + params=[get_constructor("pandas"), get_constructor("pyarrow")], scope="module" +) +def pandas_or_pyarrow_constructor( + request: pytest.FixtureRequest, +) -> EagerFrameConstructor: return request.param # type: ignore[no-any-return] @@ -115,7 +119,9 @@ def tuple_selector(draw: st.DrawFn) -> tuple[Any, Any]: @given(selector=st.one_of(single_selector, tuple_selector())) @pytest.mark.slow -def test_getitem(pandas_or_pyarrow_constructor: ConstructorEager, selector: Any) -> None: +def test_getitem( + pandas_or_pyarrow_constructor: EagerFrameConstructor, selector: Any +) -> None: """Compare __getitem__ against polars.""" # TODO(PR - clean up): documenting current differences # These assume(...) lines each filter out a known difference. diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index a0720d75ca..422dbd8372 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -7,10 +7,7 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.testing.constructors import ( - DaskConstructor, FrameConstructor, - PandasConstructor, - PolarsEagerConstructor as OriginalPolarsEagerConstructor, get_constructor, prepare_constructors, ) @@ -23,16 +20,6 @@ FalseNames: TypeAlias = set[str] -def test_dask_npartitions_distinct() -> None: - dp1, dp2 = DaskConstructor(npartitions=1), DaskConstructor(npartitions=2) - assert dp1 != dp2 - assert hash(dp1) != hash(dp2) - - -def test_dask_repr() -> None: - assert repr(DaskConstructor(npartitions=3)) == "DaskConstructor(npartitions=3)" - - def test_eager_returns_eager_frame() -> None: c = get_constructor("pandas") if not c.is_available: @@ -94,48 +81,14 @@ def test_constructor_implementation() -> None: def test_constructor_dunder() -> None: c1 = get_constructor("pandas") - c2 = PandasConstructor() + c2 = get_constructor("pandas") assert c1.identifier == "pandas" assert c1 == c2 assert hash(c1) == hash(c2) - assert c1 != OriginalPolarsEagerConstructor() + assert c1 != get_constructor("polars[eager]") assert c1 != "not a constructor" -def test_init_subclass_no_legacy_name() -> None: - class _Dummy( - FrameConstructor, implementation=Implementation.POLARS, requirements=("polars",) - ): - name = "polars[eager]" - - def __call__(self, obj: object, /, **kwds: object) -> None: # type: ignore[override] - ... # pragma: no cover - - # re-registered polars[eager] (overwriting the real one), but without a legacy_name for it. - registered = FrameConstructor._registry["polars[eager]"] - assert registered == _Dummy() - assert registered.legacy_name == "" - assert registered.requirements == ("polars",) - assert registered.implementation is Implementation.POLARS - - # Restore the original - legacy_name = "polars_eager_constructor" - - class PolarsEagerConstructor( - OriginalPolarsEagerConstructor, - implementation=Implementation.POLARS, - requirements=("polars",), - legacy_name=legacy_name, - ): - name = "polars[eager]" - - original = PolarsEagerConstructor() - - restored = FrameConstructor._registry["polars[eager]"] - assert restored == original - assert restored.legacy_name == legacy_name - - def test_init_subclass_requires_implementation() -> None: with pytest.raises(TypeError, match="missing `implementation`"): diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index f91454b153..21bd7ce7b9 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -30,7 +30,7 @@ import narwhals as nw from narwhals._utils import Version -from narwhals.testing.constructors import SQLFrameConstructor +from narwhals.testing.constructors import get_constructor from tests.utils import Constructor, maybe_get_modin_df if TYPE_CHECKING: @@ -294,10 +294,10 @@ def test_eager_only_lazy_dask(eager_only: Any, context: Any) -> None: def test_series_only_sqlframe() -> None: # pragma: no cover pytest.importorskip("sqlframe") - df = SQLFrameConstructor()(data) + df = get_constructor("sqlframe")(data) with pytest.raises(TypeError, match="Cannot only use `series_only`"): - nw.from_native(df, series_only=True) # pyright: ignore[reportArgumentType, reportCallIssue] + nw.from_native(df, series_only=True) # type: ignore[call-overload] @pytest.mark.parametrize( @@ -315,7 +315,7 @@ def test_series_only_sqlframe() -> None: # pragma: no cover ) def test_eager_only_sqlframe(eager_only: Any, context: Any) -> None: # pragma: no cover pytest.importorskip("sqlframe") - df = SQLFrameConstructor()(data) + df = get_constructor("sqlframe")(data) with context: res = nw.from_native(df, eager_only=eager_only) diff --git a/tests/utils.py b/tests/utils.py index d58794d609..c51ee91d00 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -15,8 +15,8 @@ from narwhals.dependencies import get_pandas from narwhals.testing.typing import ( # TODO(FBruzzesi): Remove these aliases once all the test suite migrates to *FrameConstructor's - EagerFrameConstructor as ConstructorEager, # noqa: TC001 - FrameConstructor as Constructor, # noqa: TC001 + EagerFrameConstructor as ConstructorEager, + FrameConstructor as Constructor, ) from narwhals.translate import from_native @@ -28,6 +28,11 @@ from narwhals.typing import Frame, TimeUnit +# TODO(FBruzzesi): Remove these aliases once all the test suite migrates to *FrameConstructor's +# NOTE: Explicitly exported otherwise mypy will raise an [attr-defined] error for each file +# importing them from `tests.utils` rather than `narwhals.testing.typing` directly. +__all__ = ("Constructor", "ConstructorEager") + def get_module_version_as_tuple(module_name: str) -> tuple[int, ...]: try: From df1afc37c7b1b5d827d51d303959a065dc81d923 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 20:22:03 +0200 Subject: [PATCH 38/71] add NativeIbis to v1 IntoDataFrame --- narwhals/stable/v1/typing.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/narwhals/stable/v1/typing.py b/narwhals/stable/v1/typing.py index a154a1a3f8..bc0a1ca26a 100644 --- a/narwhals/stable/v1/typing.py +++ b/narwhals/stable/v1/typing.py @@ -8,7 +8,12 @@ if TYPE_CHECKING: from typing_extensions import TypeAlias - from narwhals._native import NativeDataFrame, NativeDuckDB, NativeLazyFrame + from narwhals._native import ( + NativeDataFrame, + NativeDuckDB, + NativeIbis, + NativeLazyFrame, + ) from narwhals.stable.v1 import DataFrame, Expr, LazyFrame, Series class DataFrameLike(Protocol): @@ -25,7 +30,9 @@ def __dataframe__(self, *args: Any, **kwargs: Any) -> Any: ... `nw.Expr`, e.g. `df.select('a')`. """ -IntoDataFrame: TypeAlias = Union["NativeDataFrame", "DataFrameLike", "NativeDuckDB"] +IntoDataFrame: TypeAlias = Union[ + "NativeDataFrame", "DataFrameLike", "NativeDuckDB", "NativeIbis" +] """Anything which can be converted to a Narwhals DataFrame. Use this if your function accepts a narwhalifiable object but doesn't care about its backend. From 21befeec4e902f4912d035dc4443c51c06ec6b42 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 20:29:53 +0200 Subject: [PATCH 39/71] match polars signature in assert_frame_equal to fix typing --- narwhals/testing/asserts/frame.py | 19 +++++++++++-------- tests/testing/assert_frame_equal_test.py | 3 +-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/narwhals/testing/asserts/frame.py b/narwhals/testing/asserts/frame.py index 64eec42abc..9386abad13 100644 --- a/narwhals/testing/asserts/frame.py +++ b/narwhals/testing/asserts/frame.py @@ -13,7 +13,6 @@ if TYPE_CHECKING: from narwhals._typing import Arrow, IntoBackend, Pandas, Polars - from narwhals.typing import DataFrameT, LazyFrameT GUARANTEES_ROW_ORDER = { Implementation.PANDAS, @@ -26,8 +25,8 @@ def assert_frame_equal( - left: DataFrameT | LazyFrameT, - right: DataFrameT | LazyFrameT, + left: DataFrame[Any] | LazyFrame[Any], + right: DataFrame[Any] | LazyFrame[Any], *, check_row_order: bool = True, check_column_order: bool = True, @@ -145,8 +144,8 @@ def assert_frame_equal( def _check_correct_input_type( # noqa: RET503 - left: DataFrameT | LazyFrameT, - right: DataFrameT | LazyFrameT, + left: DataFrame[Any] | LazyFrame[Any], + right: DataFrame[Any] | LazyFrame[Any], backend: IntoBackend[Polars | Pandas | Arrow] | None, ) -> tuple[DataFrame[Any], DataFrame[Any]]: # Adapted from https://github.com/pola-rs/polars/blob/afdbf3056d1228cf493901e45f536b0905cec8ea/py-polars/src/polars/testing/asserts/frame.py#L15-L17 @@ -165,8 +164,8 @@ def _check_correct_input_type( # noqa: RET503 def _assert_dataframe_equal( - left: DataFrameT, - right: DataFrameT, + left: DataFrame[Any], + right: DataFrame[Any], impl: Implementation, *, check_row_order: bool, @@ -232,7 +231,11 @@ def _assert_dataframe_equal( def _check_schema_equal( - left: DataFrameT, right: DataFrameT, *, check_dtypes: bool, check_column_order: bool + left: DataFrame[Any], + right: DataFrame[Any], + *, + check_dtypes: bool, + check_column_order: bool, ) -> None: """Compares DataFrame schema based on specified criteria. diff --git a/tests/testing/assert_frame_equal_test.py b/tests/testing/assert_frame_equal_test.py index c1b3b4e357..9b0f813b2f 100644 --- a/tests/testing/assert_frame_equal_test.py +++ b/tests/testing/assert_frame_equal_test.py @@ -42,8 +42,7 @@ def test_implementation_mismatch() -> None: with _assertion_error("implementation mismatch"): assert_frame_equal( - nw.from_native(pd.DataFrame({"a": [1]})), - nw.from_native(pa.table({"a": [1]})), # type: ignore[type-var] # pyright: ignore[reportArgumentType] + nw.from_native(pd.DataFrame({"a": [1]})), nw.from_native(pa.table({"a": [1]})) ) From 96b6a4435bc4c71dbf12bec4d8779f52350d7a04 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 21:23:05 +0200 Subject: [PATCH 40/71] try hack to define coverage patch flags dynamically --- .github/workflows/pytest.yml | 10 ++++++++++ pyproject.toml | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index fd4e92207c..3fdac67f06 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -31,6 +31,11 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest + env: + # coverage's execv/fork patches raise on Windows; collapse to `subprocess` + # there (coverage dedupes) and keep the default values on Linux. + COVERAGE_PATCH_EXECV: ${{ matrix.os == 'windows-latest' && 'subprocess' || 'execv' }} + COVERAGE_PATCH_FORK: ${{ matrix.os == 'windows-latest' && 'subprocess' || 'fork' }} run: | coverage run -m pytest tests --constructors=pandas,pyarrow,polars[eager],polars[lazy] coverage combine @@ -44,6 +49,11 @@ jobs: python-version: ["3.10", "3.12"] os: [windows-latest] runs-on: ${{ matrix.os }} + env: + # coverage's execv/fork patches raise on Windows; collapse them to `subprocess` + # in the pyproject `patch` list (coverage dedupes). + COVERAGE_PATCH_EXECV: subprocess + COVERAGE_PATCH_FORK: subprocess steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 diff --git a/pyproject.toml b/pyproject.toml index 733ababe2d..5c4ba21b7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -305,7 +305,9 @@ env = [ ] [tool.coverage.run] -patch = ["fork", "subprocess"] +# execv and fork patches are unsupported on Windows (coverage raises), so Windows +# CI jobs set these env vars to "subprocess" — coverage dedupes the final list. +patch = ["${COVERAGE_PATCH_EXECV-execv}", "${COVERAGE_PATCH_FORK-fork}", "subprocess"] plugins = ["covdefaults"] source = ["narwhals", "tests"] parallel = true From 57a5f18a94ef5de81604173a8e6ceeb9bb87ba44 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 21:33:36 +0200 Subject: [PATCH 41/71] port fix from #3555 --- tests/hypothesis/join_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/hypothesis/join_test.py b/tests/hypothesis/join_test.py index c380ee73a6..fe40218c9a 100644 --- a/tests/hypothesis/join_test.py +++ b/tests/hypothesis/join_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +import math from typing import TYPE_CHECKING, Any, cast import pytest @@ -44,6 +45,9 @@ def test_join( # pragma: no cover floats: st.SearchStrategy[list[float]], cols: st.SearchStrategy[list[str]], ) -> None: + # See https://github.com/narwhals-dev/narwhals/issues/3554 + # for why we need to assume that all float values are finite + assume(all(math.isfinite(f) for f in cast("list[float]", floats))) data: dict[str, Any] = {"a": integers, "b": other_integers, "c": floats} join_cols = cast("list[str]", cols) From ffc216651d38255c3386972098a4e9c2e35ef514 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 21:47:53 +0200 Subject: [PATCH 42/71] fix: pytest on py3.14 ported wrong min coverage --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 3fdac67f06..a908544fee 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -179,7 +179,7 @@ jobs: run: | coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 coverage combine - coverage report --fail-under=100 + coverage report --fail-under=50 python-314t: strategy: From ffbf61aed670f423a5bcbe011960ce55eb32cbac Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 21:48:21 +0200 Subject: [PATCH 43/71] Run join_test with eager frames as well --- tests/frame/join_test.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py index a259d57c17..37cd933b04 100644 --- a/tests/frame/join_test.py +++ b/tests/frame/join_test.py @@ -525,6 +525,16 @@ def test_joinasof_numeric( assert_equal_data(result.sort(by="antananarivo"), expected) assert_equal_data(result_on.sort(by="antananarivo"), expected) + df_eager, df_right_eager = df.collect(), df_right.collect() + result_eager = df_eager.join_asof( + df_right_eager, left_on="antananarivo", right_on="antananarivo", strategy=strategy + ) + result_on_eager = df_eager.join_asof( + df_right_eager, on="antananarivo", strategy=strategy + ) + assert_equal_data(result_eager.sort(by="antananarivo"), expected) + assert_equal_data(result_on_eager.sort(by="antananarivo"), expected) + @pytest.mark.parametrize( ("strategy", "expected"), @@ -795,6 +805,10 @@ def test_join_duplicate_column_names( with pytest.raises(exception): df.join(df, on=["a"]).join(df, on=["a"]).collect() + df_eager = df.collect() + with pytest.raises(exception): + df_eager.join(df_eager, on=["a"]).join(df_eager, on=["a"]) + def test_join_same_laziness(constructor: Constructor) -> None: pytest.importorskip("polars") From 804256416e73c40f63ebb5d0c579e0b290ac492f Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 22:07:45 +0200 Subject: [PATCH 44/71] do not collect too early --- tests/frame/join_test.py | 59 ++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py index 37cd933b04..12ae512308 100644 --- a/tests/frame/join_test.py +++ b/tests/frame/join_test.py @@ -512,29 +512,33 @@ def test_joinasof_numeric( ("pandas_pyarrow" in str(constructor)) or ("pandas_nullable" in str(constructor)) ): request.applymarker(pytest.mark.xfail) - df = from_native_lazy( - constructor({"antananarivo": [1, 5, 10], "val": ["a", "b", "c"]}) - ).sort("antananarivo") - df_right = from_native_lazy( - constructor({"antananarivo": [1, 2, 3, 6, 7], "val": [1, 2, 3, 6, 7]}) - ).sort("antananarivo") - result = df.join_asof( - df_right, left_on="antananarivo", right_on="antananarivo", strategy=strategy - ) - result_on = df.join_asof(df_right, on="antananarivo", strategy=strategy) + + data_left = {"antananarivo": [1, 5, 10], "val": ["a", "b", "c"]} + data_right = {"antananarivo": [1, 2, 3, 6, 7], "val": [1, 2, 3, 6, 7]} + left_lf = from_native_lazy(constructor(data_left)).sort("antananarivo") + right_lf = from_native_lazy(constructor(data_right)).sort("antananarivo") + + result: nw.DataFrame[Any] | nw.LazyFrame[Any] + result_on: nw.DataFrame[Any] | nw.LazyFrame[Any] + if constructor.is_lazy: + result = left_lf.join_asof( + right_lf, left_on="antananarivo", right_on="antananarivo", strategy=strategy + ) + result_on = left_lf.join_asof(right_lf, on="antananarivo", strategy=strategy) + + elif constructor.is_eager: + left_df, right_df = left_lf.collect(), right_lf.collect() + result = left_df.join_asof( + right_df, left_on="antananarivo", right_on="antananarivo", strategy=strategy + ) + result_on = left_df.join_asof(right_df, on="antananarivo", strategy=strategy) + else: # pragma: no cover + msg = "Unreachable" + raise AssertionError(msg) + assert_equal_data(result.sort(by="antananarivo"), expected) assert_equal_data(result_on.sort(by="antananarivo"), expected) - df_eager, df_right_eager = df.collect(), df_right.collect() - result_eager = df_eager.join_asof( - df_right_eager, left_on="antananarivo", right_on="antananarivo", strategy=strategy - ) - result_on_eager = df_eager.join_asof( - df_right_eager, on="antananarivo", strategy=strategy - ) - assert_equal_data(result_eager.sort(by="antananarivo"), expected) - assert_equal_data(result_on_eager.sort(by="antananarivo"), expected) - @pytest.mark.parametrize( ("strategy", "expected"), @@ -764,9 +768,18 @@ def test_joinasof_by_exceptions( message: str, ) -> None: data = {ON: [1, 3, 2], BY: [4, 4, 6], "zor ro": [7.0, 8.0, 9.0]} - df = from_native_lazy(constructor(data)) - with pytest.raises(ValueError, match=message): - df.join_asof(df, on=on, by_left=by_left, by_right=by_right, by=by).collect() + frame = from_native_lazy(constructor(data)) + + if constructor.is_lazy: + with pytest.raises(ValueError, match=message): + frame.join_asof(frame, on=on, by_left=by_left, by_right=by_right, by=by) + elif constructor.is_eager: + with pytest.raises(ValueError, match=message): + frame.collect().join_asof( + frame.collect(), on=on, by_left=by_left, by_right=by_right, by=by + ) + else: # pragma: no cover + ... def test_join_duplicate_column_names( From 25126ea9b2994c54c10b55c99e8062e98306e218 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 22:17:56 +0200 Subject: [PATCH 45/71] fixup join test part 2 --- tests/frame/join_test.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py index 12ae512308..e0228294e2 100644 --- a/tests/frame/join_test.py +++ b/tests/frame/join_test.py @@ -526,15 +526,12 @@ def test_joinasof_numeric( ) result_on = left_lf.join_asof(right_lf, on="antananarivo", strategy=strategy) - elif constructor.is_eager: + else: left_df, right_df = left_lf.collect(), right_lf.collect() result = left_df.join_asof( right_df, left_on="antananarivo", right_on="antananarivo", strategy=strategy ) result_on = left_df.join_asof(right_df, on="antananarivo", strategy=strategy) - else: # pragma: no cover - msg = "Unreachable" - raise AssertionError(msg) assert_equal_data(result.sort(by="antananarivo"), expected) assert_equal_data(result_on.sort(by="antananarivo"), expected) @@ -773,13 +770,11 @@ def test_joinasof_by_exceptions( if constructor.is_lazy: with pytest.raises(ValueError, match=message): frame.join_asof(frame, on=on, by_left=by_left, by_right=by_right, by=by) - elif constructor.is_eager: + else: with pytest.raises(ValueError, match=message): frame.collect().join_asof( frame.collect(), on=on, by_left=by_left, by_right=by_right, by=by ) - else: # pragma: no cover - ... def test_join_duplicate_column_names( @@ -815,12 +810,13 @@ def test_join_duplicate_column_names( request.applymarker(pytest.mark.xfail) else: exception = nw.exceptions.DuplicateError - with pytest.raises(exception): - df.join(df, on=["a"]).join(df, on=["a"]).collect() - df_eager = df.collect() - with pytest.raises(exception): - df_eager.join(df_eager, on=["a"]).join(df_eager, on=["a"]) + if constructor.is_lazy: + with pytest.raises(exception): + df.join(df, on=["a"]).join(df, on=["a"]).collect() + else: + with pytest.raises(exception): + df.join(df, on=["a"]).join(df, on=["a"]) def test_join_same_laziness(constructor: Constructor) -> None: From 024135a51187aea95ec90c2b867a4cbe4631ec98 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 18 Apr 2026 23:00:06 +0200 Subject: [PATCH 46/71] test_join_duplicate_column_names fix --- tests/frame/join_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py index e0228294e2..7ca4754b2d 100644 --- a/tests/frame/join_test.py +++ b/tests/frame/join_test.py @@ -791,7 +791,7 @@ def test_join_duplicate_column_names( ): request.applymarker(pytest.mark.xfail) data = {"a": [1, 2, 3, 4, 5], "b": [6, 6, 6, 6, 6]} - df = from_native_lazy(constructor(data)) + lf = from_native_lazy(constructor(data)) if any( x in str(constructor) for x in ("pandas", "pandas[pyarrow]", "pandas[nullable]", "dask") @@ -813,8 +813,9 @@ def test_join_duplicate_column_names( if constructor.is_lazy: with pytest.raises(exception): - df.join(df, on=["a"]).join(df, on=["a"]).collect() + lf.join(lf, on=["a"]).join(lf, on=["a"]).collect() else: + df = lf.collect() with pytest.raises(exception): df.join(df, on=["a"]).join(df, on=["a"]) From e1d09e5436c8cbf759da97a6ca9fc2ef4d905bbb Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 21 Apr 2026 21:57:34 +0200 Subject: [PATCH 47/71] Adopt registry pattern, tackle a few other feedbacks --- narwhals/testing/__init__.py | 3 +- narwhals/testing/constructors.py | 613 ++++++++++++------------ narwhals/testing/pytest_plugin.py | 9 +- narwhals/testing/typing.py | 19 +- tests/expr_and_series/fill_nan_test.py | 4 +- tests/expr_and_series/is_close_test.py | 8 +- tests/expr_and_series/is_finite_test.py | 2 +- tests/expr_and_series/is_nan_test.py | 4 +- tests/series_only/hist_test.py | 24 +- tests/testing/constructors_test.py | 17 +- 10 files changed, 347 insertions(+), 356 deletions(-) diff --git a/narwhals/testing/__init__.py b/narwhals/testing/__init__.py index 649463383f..6eb8c0b0d0 100644 --- a/narwhals/testing/__init__.py +++ b/narwhals/testing/__init__.py @@ -2,5 +2,6 @@ from narwhals.testing.asserts.frame import assert_frame_equal from narwhals.testing.asserts.series import assert_series_equal +from narwhals.testing.constructors import frame_constructor -__all__ = ("assert_frame_equal", "assert_series_equal") +__all__ = ("assert_frame_equal", "assert_series_equal", "frame_constructor") diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index cf838d3098..9a642cda4c 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -1,39 +1,29 @@ -"""Concrete constructor classes and auto-registration machinery. +"""Constructor registry for `narwhals.testing`. -Each constructor wraps one backend library (pandas, Polars, DuckDB, ...) -and knows how to turn a column-oriented `dict` into a native frame. +Each constructor wraps one backend library (pandas, Polars, DuckDB, ...) and +knows how to turn a column-oriented `dict` into a native frame. -All static metadata for a backend lives on its constructor class, colocated -with the `__call__` implementation. Adding a constructor is a one-step -declaration — the `__init_subclass__` hook then auto-registers a singleton. +Registration is explicit: wrap a plain builder function with +`@frame_constructor.register(...)`. The decorator instantiates a +[`narwhals.testing.frame_constructor`][] with the declared metadata and stores +it in the shared `_registry`. ## Adding a new constructor -1. Choose the right base class: +```py +from narwhals.testing import frame_constructor - * `EagerFrameConstructor`: if the backend returns an eager dataframe. - * `LazyFrameConstructor`: if the backend returns a lazy frame. -2. Define the class in this module and declare its metadata in the class - header as keyword arguments: - - ```py - class MyBackendConstructor( - LazyFrameConstructor, - implementation=Implementation.MY_BACKEND, - requirements=("my_backend",), - legacy_name="my_backend_lazy_constructor", - ): - name = "my_backend" - - def __call__(self, obj: Data, /, **kwds: Any) -> ...: - import my_backend - - return my_backend.from_dict(obj) - ``` +@frame_constructor.register( + name="my_backend", + implementation=Implementation.MY_BACKEND, + requirements=("my_backend",), +) +def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: + import my_backend -That is all. `__init_subclass__` on `FrameConstructor` automatically registers -a default singleton into `_registry`, keyed by the string `name`. + return my_backend.from_dict(obj) +``` """ from __future__ import annotations @@ -44,7 +34,17 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ...: from copy import deepcopy from functools import lru_cache from importlib.util import find_spec -from typing import TYPE_CHECKING, Any, ClassVar, Protocol, cast +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Generic, + Literal, + TypeVar, + cast, + overload, +) from narwhals._utils import Implementation, generate_temporary_column_name @@ -58,19 +58,19 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ...: from ibis.backends.duckdb import Backend as IbisDuckDBBackend from pyspark.sql import SparkSession from sqlframe.duckdb import DuckDBSession + from typing_extensions import Concatenate, ParamSpec, TypeAlias from narwhals._native import NativeDask, NativeDuckDB, NativePySpark, NativeSQLFrame from narwhals.testing.typing import Data from narwhals.typing import IntoDataFrame, IntoFrame, IntoLazyFrame + PS = ParamSpec("PS") + __all__ = ( - "ALL_CONSTRUCTORS", - "ALL_CPU_CONSTRUCTORS", - "DEFAULT_CONSTRUCTORS", - "EagerFrameConstructor", - "FrameConstructor", "available_constructors", + "available_cpu_constructors", + "frame_constructor", "get_constructor", "is_backend_available", "prepare_constructors", @@ -78,74 +78,93 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ...: "sqlframe_session", ) +T_co = TypeVar("T_co", covariant=True, bound="IntoFrame", default="IntoFrame") +R = TypeVar("R", bound="IntoFrame") -class FrameConstructor(Protocol): - """Abstract base for any constructor exposed by `narwhals.testing`. - A constructor is a callable that turns a column-oriented `dict` (typed as - [`Data`][narwhals.testing.typing.Data]) into a native dataframe / lazy frame, - plus a string `name` that identifies the backend (e.g. `"pandas[pyarrow]"`). +class frame_constructor(Generic[T_co]): # noqa: N801 + """Callable wrapper around a backend frame builder. - Subclasses declare their backend metadata (implementation, requirements, - legacy name, nullability, GPU need) as keyword arguments in the class - header. `__init_subclass__` stores those on the class and registers a - default singleton into `_registry`. - """ + Turns a column-oriented `dict` (typed as [`Data`][narwhals.testing.typing.Data]) + into a native frame. Metadata (implementation, requirements, eager/lazy, + nullability, GPU need) lives on the instance, alongside the wrapped + `func`. Equality and hashing are keyed on `(type, name)`, so two lookups + of the same registered constructor compare equal. - _registry: ClassVar[dict[str, FrameConstructor]] = {} + Warning: + Instances should be created via [`narwhals.testing.constructors.frame_constructor.register`][], + which is the only supported entry point. - name: ClassVar[str] - implementation: ClassVar[Implementation] - requirements: ClassVar[tuple[str, ...]] = () - legacy_name: ClassVar[str] = "" - is_eager: ClassVar[bool] = False - is_non_nullable: ClassVar[bool] = False - needs_gpu: ClassVar[bool] = False + Direct instantiation is allowed but **does not** register the instance. + """ - def __init_subclass__( - cls, + _registry: ClassVar[dict[str, frame_constructor[IntoFrame]]] = {} + + def __init__( + self, + func: Callable[Concatenate[Data, PS], T_co], + /, *, - implementation: Implementation | None = None, + name: str, + implementation: Implementation, requirements: tuple[str, ...] = (), - legacy_name: str = "", - is_non_nullable: bool = False, + is_eager: bool = False, + is_nullable: bool = False, needs_gpu: bool = False, - **kwargs: Any, ) -> None: - """Register concrete subclasses automatically. + self.func = func + self.name = name + self.implementation = implementation + self.requirements = requirements + self.is_eager = is_eager + self.is_nullable = is_nullable + self.needs_gpu = needs_gpu + + @classmethod + def register( + cls, + *, + name: str, + implementation: Implementation, + requirements: tuple[str, ...] = (), + is_eager: bool = False, + is_nullable: bool = True, + needs_gpu: bool = False, + ) -> Callable[[Callable[Concatenate[Data, PS], R]], frame_constructor[R]]: + """Decorator: register `func` as the constructor named `name`. Arguments: + name: The string identifier of the constructor (e.g. `"pandas[pyarrow]"`). implementation: The [`Implementation`][] this constructor belongs to. requirements: Package names that must be importable for this constructor to be available (checked via `importlib.util.find_spec`). - legacy_name: Value returned by `str(constructor)` for backward compatibility - with test assertions that match on the old naming scheme. - is_non_nullable: Whether the backend lacks native null support. + is_eager: Whether the backend returns an eager dataframe. + is_nullable: Whether the backend has native null support. needs_gpu: Whether the backend requires GPU hardware. - **kwargs: Forwarded to `super().__init_subclass__`. + + Returns: + A decorator that replaces `func` with a `frame_constructor` + instance registered into the shared `_registry`. """ - super().__init_subclass__(**kwargs) - if implementation is not None: - cls.implementation = implementation - - cls.requirements = requirements - cls.legacy_name = legacy_name - cls.is_non_nullable = is_non_nullable - cls.needs_gpu = needs_gpu - - if "name" not in cls.__dict__: - return - if not hasattr(cls, "implementation"): - msg = ( - f"Constructor {cls.__name__} is missing `implementation` " - "kwarg in its class header." + + def decorator(func: Callable[Concatenate[Data, PS], R]) -> frame_constructor[R]: + inst: frame_constructor[R] = frame_constructor( + func, + name=name, + implementation=implementation, + requirements=requirements, + is_eager=is_eager, + is_nullable=is_nullable, + needs_gpu=needs_gpu, ) - raise TypeError(msg) - FrameConstructor._registry[cls.name] = cls() + cls._registry[name] = inst + return inst + + return decorator - def __call__(self, obj: Data, /, **kwds: Any) -> IntoFrame: - """Build a native frame from `obj`.""" - raise NotImplementedError + def __call__(self, obj: Data, /, **kwds: Any) -> T_co: + """Build a native frame from `obj` by delegating to the wrapped function.""" + return self.func(obj, **kwds) @property def identifier(self) -> str: @@ -230,12 +249,12 @@ def is_available(self) -> bool: def __str__(self) -> str: # NOTE: This is a temporary hack - # TODO(Unassigned): Remove once all the `"backend" in str(constructor)` + # TODO(FBruzzesi): Remove once all the `"backend" in str(constructor)` # statements in the test suite are properly replaced - return self.legacy_name + return self.func.__name__ def __repr__(self) -> str: - return f"{type(self).__name__}()" + return f"{type(self).__name__}(name={self.name!r})" def __hash__(self) -> int: return hash((type(self), self.name)) @@ -243,294 +262,213 @@ def __hash__(self) -> int: def __eq__(self, other: object) -> bool: return ( type(self) is type(other) - and self.name == cast("FrameConstructor", other).name + and self.name == cast("frame_constructor[Any]", other).name ) -class EagerFrameConstructor(FrameConstructor): - """A constructor that returns an *eager* native dataframe.""" - - is_eager: ClassVar[bool] = True - - def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: - raise NotImplementedError - - -class LazyFrameConstructor(FrameConstructor): - """A constructor that returns a *lazy* native frame.""" - - is_eager: ClassVar[bool] = False - - def __call__(self, obj: Data, /, **kwds: Any) -> IntoLazyFrame: - raise NotImplementedError - - # Eager constructors -class PandasConstructor( - EagerFrameConstructor, +@frame_constructor.register( + name="pandas", implementation=Implementation.PANDAS, requirements=("pandas",), - legacy_name="pandas_constructor", - is_non_nullable=True, -): - """Constructor backed by `pandas.DataFrame` with default NumPy dtypes.""" - - name = "pandas" - - def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: - import pandas as pd + is_eager=True, + is_nullable=False, +) +def pandas_constructor(obj: Data, /, **kwds: Any) -> pd.DataFrame: + import pandas as pd - return pd.DataFrame(obj, **kwds) + return pd.DataFrame(obj, **kwds) -class PandasNullableConstructor( - EagerFrameConstructor, +@frame_constructor.register( + name="pandas[nullable]", implementation=Implementation.PANDAS, requirements=("pandas",), - legacy_name="pandas_nullable_constructor", -): - """Constructor backed by `pandas.DataFrame` with `numpy_nullable` dtypes.""" - - name = "pandas[nullable]" - - def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: - import pandas as pd + is_eager=True, +) +def pandas_nullable_constructor(obj: Data, /, **kwds: Any) -> pd.DataFrame: + import pandas as pd - return pd.DataFrame(obj, **kwds).convert_dtypes(dtype_backend="numpy_nullable") + return pd.DataFrame(obj, **kwds).convert_dtypes(dtype_backend="numpy_nullable") -class PandasPyArrowConstructor( - EagerFrameConstructor, +@frame_constructor.register( + name="pandas[pyarrow]", implementation=Implementation.PANDAS, requirements=("pandas", "pyarrow"), - legacy_name="pandas_pyarrow_constructor", -): - """Constructor backed by `pandas.DataFrame` with `pyarrow` dtypes.""" - - name = "pandas[pyarrow]" - - def __call__(self, obj: Data, /, **kwds: Any) -> pd.DataFrame: - import pandas as pd + is_eager=True, +) +def pandas_pyarrow_constructor(obj: Data, /, **kwds: Any) -> pd.DataFrame: + import pandas as pd - return pd.DataFrame(obj, **kwds).convert_dtypes(dtype_backend="pyarrow") + return pd.DataFrame(obj, **kwds).convert_dtypes(dtype_backend="pyarrow") -class PyArrowConstructor( - EagerFrameConstructor, +@frame_constructor.register( + name="pyarrow", implementation=Implementation.PYARROW, requirements=("pyarrow",), - legacy_name="pyarrow_table_constructor", -): - """Constructor backed by `pyarrow.Table`.""" - - name = "pyarrow" - - def __call__(self, obj: Data, /, **kwds: Any) -> pa.Table: - import pyarrow as pa + is_eager=True, +) +def pyarrow_table_constructor(obj: Data, /, **kwds: Any) -> pa.Table: + import pyarrow as pa - return pa.table(obj, **kwds) + return pa.table(obj, **kwds) -class ModinConstructor( - EagerFrameConstructor, +@frame_constructor.register( + name="modin", implementation=Implementation.MODIN, requirements=("modin",), - legacy_name="modin_constructor", - is_non_nullable=True, -): # pragma: no cover - """Constructor backed by `modin.pandas.DataFrame`.""" - - name = "modin" - - def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: - import modin.pandas as mpd - import pandas as pd + is_eager=True, + is_nullable=False, +) +def modin_constructor(obj: Data, /, **kwds: Any) -> IntoDataFrame: # pragma: no cover + import modin.pandas as mpd + import pandas as pd - return cast("IntoDataFrame", mpd.DataFrame(pd.DataFrame(obj, **kwds))) + return cast("IntoDataFrame", mpd.DataFrame(pd.DataFrame(obj, **kwds))) -class ModinPyArrowConstructor( - EagerFrameConstructor, +@frame_constructor.register( + name="modin[pyarrow]", implementation=Implementation.MODIN, requirements=("modin", "pyarrow"), - legacy_name="modin_pyarrow_constructor", -): # pragma: no cover - """Constructor backed by `modin.pandas.DataFrame` with `pyarrow` dtypes.""" - - name = "modin[pyarrow]" - - def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: - import modin.pandas as mpd - import pandas as pd + is_eager=True, +) +def modin_pyarrow_constructor( + obj: Data, /, **kwds: Any +) -> IntoDataFrame: # pragma: no cover + import modin.pandas as mpd + import pandas as pd - df = mpd.DataFrame(pd.DataFrame(obj, **kwds)).convert_dtypes( - dtype_backend="pyarrow" - ) - return cast("IntoDataFrame", df) + df = mpd.DataFrame(pd.DataFrame(obj, **kwds)).convert_dtypes(dtype_backend="pyarrow") + return cast("IntoDataFrame", df) -class CudfConstructor( - EagerFrameConstructor, +@frame_constructor.register( + name="cudf", implementation=Implementation.CUDF, requirements=("cudf",), - legacy_name="cudf_constructor", + is_eager=True, needs_gpu=True, -): # pragma: no cover - """Constructor backed by `cudf.DataFrame` (requires GPU).""" - - name = "cudf" - - def __call__(self, obj: Data, /, **kwds: Any) -> IntoDataFrame: - import cudf +) +def cudf_constructor(obj: Data, /, **kwds: Any) -> IntoDataFrame: # pragma: no cover + import cudf - return cast("IntoDataFrame", cudf.DataFrame(obj, **kwds)) + return cast("IntoDataFrame", cudf.DataFrame(obj, **kwds)) -class PolarsEagerConstructor( - EagerFrameConstructor, +@frame_constructor.register( + name="polars[eager]", implementation=Implementation.POLARS, requirements=("polars",), - legacy_name="polars_eager_constructor", -): - """Constructor backed by `polars.DataFrame`.""" - - name = "polars[eager]" - - def __call__(self, obj: Data, /, **kwds: Any) -> pl.DataFrame: - import polars as pl + is_eager=True, +) +def polars_eager_constructor(obj: Data, /, **kwds: Any) -> pl.DataFrame: + import polars as pl - return pl.DataFrame(obj, **kwds) + return pl.DataFrame(obj, **kwds) # Lazy constructors -class PolarsLazyConstructor( - LazyFrameConstructor, - implementation=Implementation.POLARS, - requirements=("polars",), - legacy_name="polars_lazy_constructor", -): - """Constructor backed by `polars.LazyFrame`.""" - - name = "polars[lazy]" - - def __call__(self, obj: Data, /, **kwds: Any) -> pl.LazyFrame: - import polars as pl +@frame_constructor.register( + name="polars[lazy]", implementation=Implementation.POLARS, requirements=("polars",) +) +def polars_lazy_constructor(obj: Data, /, **kwds: Any) -> pl.LazyFrame: + import polars as pl - return pl.LazyFrame(obj, **kwds) + return pl.LazyFrame(obj, **kwds) -class DaskConstructor( - LazyFrameConstructor, +@frame_constructor.register( + name="dask", implementation=Implementation.DASK, requirements=("dask",), - legacy_name="dask_lazy_p2_constructor", - is_non_nullable=True, -): # pragma: no cover - """Constructor backed by `dask.dataframe`.""" - - name = "dask" - - def __call__(self, obj: Data, /, npartitions: int = 2, **kwds: Any) -> NativeDask: - import dask.dataframe as dd + is_nullable=False, +) +def dask_lazy_p2_constructor( + obj: Data, /, npartitions: int = 2, **kwds: Any +) -> NativeDask: # pragma: no cover + import dask.dataframe as dd - return cast("NativeDask", dd.from_dict(obj, npartitions=npartitions, **kwds)) + return cast("NativeDask", dd.from_dict(obj, npartitions=npartitions, **kwds)) -class DuckDBConstructor( - LazyFrameConstructor, +@frame_constructor.register( + name="duckdb", implementation=Implementation.DUCKDB, requirements=("duckdb", "pyarrow"), - legacy_name="duckdb_lazy_constructor", -): - """Constructor backed by DuckDB (via `duckdb.sql`).""" - - name = "duckdb" - - def __call__(self, obj: Data, /, **kwds: Any) -> NativeDuckDB: - import duckdb - import pyarrow as pa +) +def duckdb_lazy_constructor(obj: Data, /, **kwds: Any) -> NativeDuckDB: + import duckdb + import pyarrow as pa - duckdb.sql("""set timezone = 'UTC'""") - _df = pa.table(obj, **kwds) - return duckdb.sql("select * from _df") + duckdb.sql("""set timezone = 'UTC'""") + _df = pa.table(obj, **kwds) + return duckdb.sql("select * from _df") + + +def _pyspark_build(obj: Data, /, **kwds: Any) -> NativePySpark: # pragma: no cover + session = _pyspark_session_lazy() + _obj = deepcopy(obj) + index_col_name = generate_temporary_column_name(n_bytes=8, columns=list(_obj)) + _obj[index_col_name] = list(range(len(_obj[next(iter(_obj))]))) + result = ( + session.createDataFrame([*zip(*_obj.values())], schema=[*_obj.keys()], **kwds) + .repartition(2) + .orderBy(index_col_name) + .drop(index_col_name) + ) + return cast("NativePySpark", result) -class PySparkConstructor( - LazyFrameConstructor, - implementation=Implementation.PYSPARK, - requirements=("pyspark",), - legacy_name="pyspark_lazy_constructor", -): # pragma: no cover - """Constructor backed by `pyspark.sql.DataFrame`.""" - - name = "pyspark" - - def __call__(self, obj: Data, /, **kwds: Any) -> NativePySpark: - session = _pyspark_session_lazy() - _obj = deepcopy(obj) - index_col_name = generate_temporary_column_name(n_bytes=8, columns=list(_obj)) - _obj[index_col_name] = list(range(len(_obj[next(iter(_obj))]))) - result = ( - session.createDataFrame([*zip(*_obj.values())], schema=[*_obj.keys()], **kwds) - .repartition(2) - .orderBy(index_col_name) - .drop(index_col_name) - ) - return cast("NativePySpark", result) +@frame_constructor.register( + name="pyspark", implementation=Implementation.PYSPARK, requirements=("pyspark",) +) +def pyspark_lazy_constructor( + obj: Data, /, **kwds: Any +) -> NativePySpark: # pragma: no cover + return _pyspark_build(obj, **kwds) -class PySparkConnectConstructor( - PySparkConstructor, +@frame_constructor.register( + name="pyspark[connect]", implementation=Implementation.PYSPARK_CONNECT, requirements=("pyspark",), - legacy_name="pyspark_lazy_constructor", -): # pragma: no cover - """Constructor backed by PySpark Connect (Spark Connect protocol).""" - - name = "pyspark[connect]" +) +def pyspark_connect_lazy_constructor( + obj: Data, /, **kwds: Any +) -> NativePySpark: # pragma: no cover + return _pyspark_build(obj, **kwds) -class SQLFrameConstructor( - LazyFrameConstructor, +@frame_constructor.register( + name="sqlframe", implementation=Implementation.SQLFRAME, requirements=("sqlframe", "duckdb"), - legacy_name="sqlframe_pyspark_lazy_constructor", -): - """Constructor backed by `sqlframe` (DuckDB session).""" - - name = "sqlframe" - - def __call__(self, obj: Data, /, **kwds: Any) -> NativeSQLFrame: - session = sqlframe_session() - return session.createDataFrame( - [*zip(*obj.values())], schema=[*obj.keys()], **kwds - ) +) +def sqlframe_pyspark_lazy_constructor(obj: Data, /, **kwds: Any) -> NativeSQLFrame: + session = sqlframe_session() + return session.createDataFrame([*zip(*obj.values())], schema=[*obj.keys()], **kwds) -class IbisConstructor( - LazyFrameConstructor, +@frame_constructor.register( + name="ibis", implementation=Implementation.IBIS, requirements=("ibis", "duckdb", "pyarrow"), - legacy_name="ibis_lazy_constructor", -): # pragma: no cover - """Constructor backed by `ibis` (DuckDB backend).""" - - name = "ibis" - - def __call__(self, obj: Data, /, **kwds: Any) -> ibis.Table: - import pyarrow as pa - - table = pa.table(obj) - table_name = str(uuid.uuid4()) - return _ibis_backend().create_table(table_name, table, **kwds) +) +def ibis_lazy_constructor(obj: Data, /, **kwds: Any) -> ibis.Table: # pragma: no cover + import pyarrow as pa + table = pa.table(obj) + table_name = str(uuid.uuid4()) + return _ibis_backend().create_table(table_name, table, **kwds) -ALL_CONSTRUCTORS: dict[str, FrameConstructor] = FrameConstructor._registry -"""All registered constructors keyed by their string identifier.""" DEFAULT_CONSTRUCTORS: frozenset[str] = frozenset( { @@ -547,11 +485,6 @@ def __call__(self, obj: Data, /, **kwds: Any) -> ibis.Table: user does not pass `--constructors` (mirrors the historical Narwhals defaults). """ -ALL_CPU_CONSTRUCTORS: frozenset[str] = frozenset( - name for name, c in FrameConstructor._registry.items() if not c.needs_gpu -) -"""All constructors that do not require GPU hardware.""" - def available_constructors() -> frozenset[str]: """Return the names of every constructor whose backend is importable. @@ -561,11 +494,51 @@ def available_constructors() -> frozenset[str]: >>> "pandas" in available_constructors() True """ - return frozenset(name for name, c in ALL_CONSTRUCTORS.items() if c.is_available) + return frozenset( + name for name, c in frame_constructor._registry.items() if c.is_available + ) -def get_constructor(name: str) -> FrameConstructor: - """Return the registered singleton constructor for `name`. +def available_cpu_constructors() -> frozenset[str]: + """Return the names of every CPU constructor whose backend is importable. + + Examples: + >>> from narwhals.testing.constructors import available_cpu_constructors + >>> "pandas" in available_cpu_constructors() + True + """ + return frozenset( + name + for name, c in frame_constructor._registry.items() + if c.is_available and not c.needs_gpu + ) + + +EagerName: TypeAlias = Literal[ + "pandas", + "pandas[nullable]", + "pandas[pyarrow]", + "modin", + "modin[pyarrow]", + "cudf", + "polars[eager]", + "pyarrow", +] +LazyName: TypeAlias = Literal[ + "polars[lazy]", "dask", "duckdb", "pyspark", "pyspark[connect]", "sqlframe", "ibis" +] + + +@overload +def get_constructor(name: EagerName) -> frame_constructor[IntoDataFrame]: ... +@overload +def get_constructor(name: LazyName) -> frame_constructor[IntoLazyFrame]: ... +@overload +def get_constructor(name: str) -> frame_constructor[IntoFrame]: ... + + +def get_constructor(name: str) -> frame_constructor[IntoFrame]: + """Return the registered constructor for `name`. Arguments: name: The string identifier of a registered constructor @@ -577,21 +550,24 @@ def get_constructor(name: str) -> FrameConstructor: Examples: >>> from narwhals.testing.constructors import get_constructor >>> get_constructor("pandas") - PandasConstructor() + frame_constructor(name='pandas') """ try: - return ALL_CONSTRUCTORS[name] + return frame_constructor._registry[name] except KeyError as exc: - valid = sorted(ALL_CONSTRUCTORS) + valid = sorted(frame_constructor._registry) msg = f"Unknown constructor {name!r}. Expected one of: {valid}." raise ValueError(msg) from exc def prepare_constructors( *, include: Iterable[str] | None = None, exclude: Iterable[str] | None = None -) -> list[FrameConstructor]: +) -> list[frame_constructor[Any]]: """Return available constructors, optionally filtered. + Note: + `exclude` is given precedence in the selection. + Arguments: include: If given, only return constructors whose name is in this set. exclude: If given, remove constructors whose name is in this set. @@ -601,15 +577,18 @@ def prepare_constructors( >>> constructors = prepare_constructors(include=["pandas", "polars[eager]"]) """ available = available_constructors() - candidates: list[FrameConstructor] = [ - c for name, c in ALL_CONSTRUCTORS.items() if name in available + candidates: list[frame_constructor[Any]] = [ + c for name, c in frame_constructor._registry.items() if name in available ] - if include is not None: - inc = frozenset(include) - candidates = [c for c in candidates if c.name in inc] - if exclude is not None: - exc = frozenset(exclude) - candidates = [c for c in candidates if c.name not in exc] + + include_set = frozenset(include) if include is not None else set() + exclude_set = frozenset(exclude) if exclude is not None else set() + + if unknown := (include_set.union(exclude_set).difference(available)): + msg = f"The following names are not known constructors: {sorted(unknown)}" + raise ValueError(msg) + + candidates = [c for c in candidates if c.name in include_set.difference(exclude_set)] return sorted(candidates, key=lambda c: c.name) diff --git a/narwhals/testing/pytest_plugin.py b/narwhals/testing/pytest_plugin.py index 9d08fa380e..fdc6795a76 100644 --- a/narwhals/testing/pytest_plugin.py +++ b/narwhals/testing/pytest_plugin.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: import pytest - from narwhals.testing.constructors import FrameConstructor + from narwhals.testing.typing import FrameConstructor _MIN_PANDAS_NULLABLE_VERSION: tuple[int, ...] = (2, 0, 0) @@ -90,13 +90,16 @@ def pytest_addoption(parser: pytest.Parser) -> None: def _select_constructors( config: pytest.Config, ) -> list[FrameConstructor]: # pragma: no cover - from narwhals.testing.constructors import ALL_CPU_CONSTRUCTORS, prepare_constructors + from narwhals.testing.constructors import ( + available_cpu_constructors, + prepare_constructors, + ) _all_cpu_exclusions = frozenset({"modin", "pyspark[connect]"}) if config.getoption("all_cpu_constructors"): selected = prepare_constructors( - include=ALL_CPU_CONSTRUCTORS, exclude=_all_cpu_exclusions + include=available_cpu_constructors(), exclude=_all_cpu_exclusions ) else: opt = cast("str", config.getoption("constructors")) diff --git a/narwhals/testing/typing.py b/narwhals/testing/typing.py index 19aabb9e3b..44049d1e5e 100644 --- a/narwhals/testing/typing.py +++ b/narwhals/testing/typing.py @@ -2,18 +2,23 @@ from typing import TYPE_CHECKING, Any -from narwhals.testing.constructors import ( - EagerFrameConstructor, - FrameConstructor, - LazyFrameConstructor, -) - if TYPE_CHECKING: from typing_extensions import TypeAlias + from narwhals.testing.constructors import frame_constructor + from narwhals.typing import IntoDataFrame, IntoFrame, IntoLazyFrame + __all__ = ("Data", "EagerFrameConstructor", "FrameConstructor", "LazyFrameConstructor") +FrameConstructor: TypeAlias = "frame_constructor[IntoFrame]" + +EagerFrameConstructor: TypeAlias = "frame_constructor[IntoDataFrame]" +"""Type alias for a constructor that returns an eager native dataframe.""" + +LazyFrameConstructor: TypeAlias = "frame_constructor[IntoLazyFrame]" +"""Type alias for a constructor that returns a lazy native frame.""" + Data: TypeAlias = dict[str, Any] # TODO(Unassined): This should have a better annotation -"""A column-oriented mapping used as input to a [`Constructor`][].""" +"""A column-oriented mapping used as input to a frame constructor.""" diff --git a/tests/expr_and_series/fill_nan_test.py b/tests/expr_and_series/fill_nan_test.py index 5d3ddc8cfc..1835d6c1f1 100644 --- a/tests/expr_and_series/fill_nan_test.py +++ b/tests/expr_and_series/fill_nan_test.py @@ -23,7 +23,7 @@ def test_fill_nan(request: pytest.FixtureRequest, constructor: Constructor) -> N assert_equal_data(result, expected) assert result.lazy().collect()["float_na"].null_count() == 2 result = df.select(nw.all().fill_nan(3.0)) - if constructor.is_non_nullable: + if not constructor.is_nullable: # no nan vs null distinction expected = {"float": [-1.0, 1.0, 3.0], "float_na": [3.0, 1.0, 3.0]} assert result.lazy().collect()["float_na"].null_count() == 0 @@ -42,7 +42,7 @@ def test_fill_nan_series(constructor_eager: ConstructorEager) -> None: "float_na" ] result = s.fill_nan(999) - if constructor_eager.is_non_nullable: + if not constructor_eager.is_nullable: # no nan vs null distinction assert_equal_data({"a": result}, {"a": [999.0, 1.0, 999.0]}) elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): diff --git a/tests/expr_and_series/is_close_test.py b/tests/expr_and_series/is_close_test.py index 6cfdb94657..01579e3f3f 100644 --- a/tests/expr_and_series/is_close_test.py +++ b/tests/expr_and_series/is_close_test.py @@ -114,7 +114,7 @@ def test_is_close_series_with_series( y = y.zip_with(y != NAN_PLACEHOLDER, y**0.5).zip_with(y != NULL_PLACEHOLDER, nulls) result = x.is_close(y, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal) - if constructor_eager.is_non_nullable: + if not constructor_eager.is_nullable: expected = [v if v is not None else nans_equal for v in expected] elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): expected = [ @@ -142,7 +142,7 @@ def test_is_close_series_with_scalar( y = y.zip_with(y != NAN_PLACEHOLDER, y**0.5).zip_with(y != NULL_PLACEHOLDER, nulls) result = y.is_close(other, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal) - if constructor_eager.is_non_nullable: + if not constructor_eager.is_nullable: expected = [v if v is not None else False for v in expected] elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): expected = [ @@ -187,7 +187,7 @@ def test_is_close_expr_with_expr( ) .sort("idx") ) - if constructor.is_non_nullable: + if not constructor.is_nullable: expected = [v if v is not None else nans_equal for v in expected] elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): expected = [ @@ -228,7 +228,7 @@ def test_is_close_expr_with_scalar( ) .sort("idx") ) - if constructor.is_non_nullable: + if not constructor.is_nullable: expected = [v if v is not None else False for v in expected] elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): expected = [ diff --git a/tests/expr_and_series/is_finite_test.py b/tests/expr_and_series/is_finite_test.py index b2a36d5ed7..f55b106593 100644 --- a/tests/expr_and_series/is_finite_test.py +++ b/tests/expr_and_series/is_finite_test.py @@ -64,7 +64,7 @@ def test_is_finite_column_with_null(constructor: Constructor, data: list[float]) result = df.select(nw.col("a").is_finite()) expected: dict[str, list[Any]] - if constructor.is_non_nullable: + if not constructor.is_nullable: # Null values are coerced to NaN for non-nullable datatypes expected = {"a": [True, True, False]} else: diff --git a/tests/expr_and_series/is_nan_test.py b/tests/expr_and_series/is_nan_test.py index 948b800a9b..9dce78c535 100644 --- a/tests/expr_and_series/is_nan_test.py +++ b/tests/expr_and_series/is_nan_test.py @@ -20,7 +20,7 @@ def test_nan(constructor: Constructor) -> None: ) expected: dict[str, list[Any]] - if constructor.is_non_nullable: + if not constructor.is_nullable: # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], @@ -57,7 +57,7 @@ def test_nan_series(constructor_eager: ConstructorEager) -> None: "float_na": df["float_na"].is_nan(), } expected: dict[str, list[Any]] - if constructor_eager.is_non_nullable: + if not constructor_eager.is_nullable: # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], diff --git a/tests/series_only/hist_test.py b/tests/series_only/hist_test.py index 18468cc783..56cb68ccb4 100644 --- a/tests/series_only/hist_test.py +++ b/tests/series_only/hist_test.py @@ -17,6 +17,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + from narwhals.testing.constructors import EagerName + rnd = Random(0) # noqa: S311 data: dict[str, Any] = { @@ -47,10 +49,10 @@ param_name = pytest.mark.parametrize("name", ["pandas", "polars[eager]", "pyarrow"]) -def maybe_name_to_constructor(name: str) -> ConstructorEager: +def maybe_name_to_constructor(name: EagerName) -> ConstructorEager: constructor = get_constructor(name) if constructor.is_available: - return constructor # type: ignore[return-value] + return constructor pytest.skip() @@ -75,7 +77,11 @@ def maybe_name_to_constructor(name: str) -> ConstructorEager: ) @param_name def test_hist_bin( - name: str, bins: list[float], expected: Sequence[float], *, include_breakpoint: bool + name: EagerName, + bins: list[float], + expected: Sequence[float], + *, + include_breakpoint: bool, ) -> None: constructor_eager = maybe_name_to_constructor(name) df = nw.from_native(constructor_eager(data)).with_columns( @@ -122,7 +128,7 @@ def test_hist_bin( @param_include_breakpoint @param_name def test_hist_count( - name: str, *, params: dict[str, Any], include_breakpoint: bool + name: EagerName, *, params: dict[str, Any], include_breakpoint: bool ) -> None: constructor_eager = maybe_name_to_constructor(name) df = nw.from_native(constructor_eager(data)).with_columns( @@ -166,7 +172,7 @@ def test_hist_count( @param_name -def test_hist_count_no_spread(name: str) -> None: +def test_hist_count_no_spread(name: EagerName) -> None: constructor_eager = maybe_name_to_constructor(name) data = {"all_zero": [0, 0, 0], "all_non_zero": [5, 5, 5]} df = nw.from_native(constructor_eager(data)) @@ -198,7 +204,7 @@ def test_hist_bin_and_bin_count() -> None: @param_include_breakpoint @param_name -def test_hist_no_data(name: str, *, include_breakpoint: bool) -> None: +def test_hist_no_data(name: EagerName, *, include_breakpoint: bool) -> None: constructor_eager = maybe_name_to_constructor(name) s = nw.from_native(constructor_eager({"values": []})).select( nw.col("values").cast(nw.Float64) @@ -220,7 +226,7 @@ def test_hist_no_data(name: str, *, include_breakpoint: bool) -> None: @param_name -def test_hist_small_bins(name: str) -> None: +def test_hist_small_bins(name: EagerName) -> None: constructor_eager = maybe_name_to_constructor(name) s = nw.from_native(constructor_eager({"values": [1, 2, 3]})) result = s["values"].hist(bins=None, bin_count=None) @@ -273,7 +279,7 @@ def test_hist_non_monotonic(constructor_eager: ConstructorEager) -> None: @pytest.mark.filterwarnings("ignore:invalid value encountered in cast:RuntimeWarning") @pytest.mark.slow def test_hist_bin_hypotheis( - name: str, data: list[float], bin_deltas: list[float] + name: EagerName, data: list[float], bin_deltas: list[float] ) -> None: constructor_eager = maybe_name_to_constructor(name) pytest.importorskip("polars") @@ -314,7 +320,7 @@ def test_hist_bin_hypotheis( @param_name @pytest.mark.slow def test_hist_count_hypothesis( - name: str, data: list[float], bin_count: int, request: pytest.FixtureRequest + name: EagerName, data: list[float], bin_count: int, request: pytest.FixtureRequest ) -> None: pytest.importorskip("polars") import polars as pl diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index 422dbd8372..c812637953 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -7,7 +7,7 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.testing.constructors import ( - FrameConstructor, + frame_constructor, get_constructor, prepare_constructors, ) @@ -53,7 +53,7 @@ def test_lazy_returns_lazy_frame() -> None: ("is_spark_like", {"pyspark", "sqlframe", "pyspark[connect]"}, {"pandas"}), ("is_lazy", {"polars[lazy]", "dask", "duckdb"}, {"pandas"}), ("needs_pyarrow", {"pyarrow", "duckdb", "ibis"}, {"pandas"}), - ("is_non_nullable", {"pandas", "modin", "dask"}, {"polars[eager]"}), + ("is_nullable", {"polars[eager]"}, {"pandas", "modin", "dask"}), ] @@ -89,14 +89,11 @@ def test_constructor_dunder() -> None: assert c1 != "not a constructor" -def test_init_subclass_requires_implementation() -> None: - with pytest.raises(TypeError, match="missing `implementation`"): - - class _BadConstructor(FrameConstructor, requirements=("polars",)): - name = "polars[eager]" - - def __call__(self, obj: object, /, **kwds: object) -> None: # type: ignore[override] - ... # pragma: no cover +def test_register_requires_implementation() -> None: + with pytest.raises(TypeError, match="implementation"): + frame_constructor.register( # type: ignore[call-arg] + name="_bad", requirements=("polars",) + ) def test_get_constructor() -> None: From e97f33124cbee39f1bc93c7c9575a8ada3dd33d0 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 21 Apr 2026 22:26:52 +0200 Subject: [PATCH 48/71] fixup typing and prepare_constructors regression --- narwhals/testing/constructors.py | 25 +++++++++++++++---------- tests/testing/constructors_test.py | 29 +++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index 9a642cda4c..0595179bea 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -58,14 +58,12 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: from ibis.backends.duckdb import Backend as IbisDuckDBBackend from pyspark.sql import SparkSession from sqlframe.duckdb import DuckDBSession - from typing_extensions import Concatenate, ParamSpec, TypeAlias + from typing_extensions import Concatenate, TypeAlias from narwhals._native import NativeDask, NativeDuckDB, NativePySpark, NativeSQLFrame from narwhals.testing.typing import Data from narwhals.typing import IntoDataFrame, IntoFrame, IntoLazyFrame - PS = ParamSpec("PS") - __all__ = ( "available_constructors", @@ -102,7 +100,7 @@ class frame_constructor(Generic[T_co]): # noqa: N801 def __init__( self, - func: Callable[Concatenate[Data, PS], T_co], + func: Callable[Concatenate[Data, ...], T_co], /, *, name: str, @@ -130,7 +128,7 @@ def register( is_eager: bool = False, is_nullable: bool = True, needs_gpu: bool = False, - ) -> Callable[[Callable[Concatenate[Data, PS], R]], frame_constructor[R]]: + ) -> Callable[[Callable[Concatenate[Data, ...], R]], frame_constructor[R]]: """Decorator: register `func` as the constructor named `name`. Arguments: @@ -147,7 +145,7 @@ def register( instance registered into the shared `_registry`. """ - def decorator(func: Callable[Concatenate[Data, PS], R]) -> frame_constructor[R]: + def decorator(func: Callable[Concatenate[Data, ...], R]) -> frame_constructor[R]: inst: frame_constructor[R] = frame_constructor( func, name=name, @@ -562,7 +560,7 @@ def get_constructor(name: str) -> frame_constructor[IntoFrame]: def prepare_constructors( *, include: Iterable[str] | None = None, exclude: Iterable[str] | None = None -) -> list[frame_constructor[Any]]: +) -> list[frame_constructor[IntoFrame]]: """Return available constructors, optionally filtered. Note: @@ -581,14 +579,21 @@ def prepare_constructors( c for name, c in frame_constructor._registry.items() if name in available ] - include_set = frozenset(include) if include is not None else set() - exclude_set = frozenset(exclude) if exclude is not None else set() + include_set: frozenset[str] = ( + frozenset(include) if include is not None else frozenset() + ) + exclude_set: frozenset[str] = ( + frozenset(exclude) if exclude is not None else frozenset() + ) if unknown := (include_set.union(exclude_set).difference(available)): msg = f"The following names are not known constructors: {sorted(unknown)}" raise ValueError(msg) - candidates = [c for c in candidates if c.name in include_set.difference(exclude_set)] + if include is not None: + candidates = [c for c in candidates if c.name in include_set] + if exclude is not None: + candidates = [c for c in candidates if c.name not in exclude_set] return sorted(candidates, key=lambda c: c.name) diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index c812637953..5a6cdaa340 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -7,6 +7,7 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.testing.constructors import ( + available_constructors, frame_constructor, get_constructor, prepare_constructors, @@ -105,7 +106,27 @@ def test_get_constructor_invalid_name() -> None: get_constructor("not_a_backend") -def test_prepare_constructors_exclude_only() -> None: - result = prepare_constructors(exclude=["pandas"]) - names = {c.name for c in result} - assert "pandas" not in names +@pytest.mark.parametrize( + ("include", "exclude", "expected"), + [ + (None, None, available_constructors()), + (None, ["pandas"], available_constructors() - {"pandas"}), + (["pandas", "polars[eager]"], None, {"pandas", "polars[eager]"}), + (["pandas", "polars[eager]"], ["pandas"], {"polars[eager]"}), + ([], None, frozenset()), + ], +) +def test_prepare_constructors( + include: list[str] | None, exclude: list[str] | None, expected: frozenset[str] +) -> None: + for name in (*(include or ()), *(exclude or ())): + if not get_constructor(name).is_available: + pytest.skip(f"{name} not installed") + result = prepare_constructors(include=include, exclude=exclude) + assert {c.name for c in result} == expected + + +@pytest.mark.parametrize("kwarg", ["include", "exclude"]) +def test_prepare_constructors_unknown_name_raises(kwarg: str) -> None: + with pytest.raises(ValueError, match="not known constructors"): + prepare_constructors(**{kwarg: ["not_a_backend"]}) From e76e50d5c8d97683b6cb39d3efecaaa1f7e43e3c Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 21 Apr 2026 22:33:05 +0200 Subject: [PATCH 49/71] fix __eq__ --- narwhals/testing/constructors.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index 0595179bea..7b4261920b 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -3,10 +3,9 @@ Each constructor wraps one backend library (pandas, Polars, DuckDB, ...) and knows how to turn a column-oriented `dict` into a native frame. -Registration is explicit: wrap a plain builder function with -`@frame_constructor.register(...)`. The decorator instantiates a -[`narwhals.testing.frame_constructor`][] with the declared metadata and stores -it in the shared `_registry`. +Registration is explicit: wrap a plain builder function with `@frame_constructor.register(...)`. +The decorator instantiates a [`narwhals.testing.frame_constructor`][] with the +declared metadata and stores it in the shared `_registry`. ## Adding a new constructor @@ -258,10 +257,7 @@ def __hash__(self) -> int: return hash((type(self), self.name)) def __eq__(self, other: object) -> bool: - return ( - type(self) is type(other) - and self.name == cast("frame_constructor[Any]", other).name - ) + return isinstance(other, frame_constructor) and self.name == other.name # Eager constructors From b1da6a7a92d814174b56bd198f1cada6797b25c9 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 21 Apr 2026 22:36:50 +0200 Subject: [PATCH 50/71] match is_nullable default --- narwhals/testing/constructors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index 7b4261920b..668743c609 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -106,7 +106,7 @@ def __init__( implementation: Implementation, requirements: tuple[str, ...] = (), is_eager: bool = False, - is_nullable: bool = False, + is_nullable: bool = True, needs_gpu: bool = False, ) -> None: self.func = func From bb6b04233b46f0fbf6ea9f1454eee21f5eeb769d Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 21 Apr 2026 22:39:17 +0200 Subject: [PATCH 51/71] fixup typevar default --- narwhals/testing/constructors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index 668743c609..79d21bf36d 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -75,7 +75,7 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: "sqlframe_session", ) -T_co = TypeVar("T_co", covariant=True, bound="IntoFrame", default="IntoFrame") +T_co = TypeVar("T_co", covariant=True, bound="IntoFrame") R = TypeVar("R", bound="IntoFrame") From 32018b368a7f2b7f98d16a856ca6a66344c045e1 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 21 Apr 2026 23:10:39 +0200 Subject: [PATCH 52/71] Fixup default backends parsing --- narwhals/testing/pytest_plugin.py | 8 ++++++-- narwhals/testing/typing.py | 6 +++--- tests/hypothesis/getitem_test.py | 8 +++----- tests/testing/plugin_test.py | 8 ++++---- tests/utils.py | 2 +- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/narwhals/testing/pytest_plugin.py b/narwhals/testing/pytest_plugin.py index fdc6795a76..418bb4e201 100644 --- a/narwhals/testing/pytest_plugin.py +++ b/narwhals/testing/pytest_plugin.py @@ -44,9 +44,13 @@ def _default_constructor_ids() -> list[str]: """ if env := os.environ.get("NARWHALS_DEFAULT_CONSTRUCTORS"): # pragma: no cover return env.split(",") - from narwhals.testing.constructors import DEFAULT_CONSTRUCTORS, prepare_constructors + from narwhals.testing.constructors import DEFAULT_CONSTRUCTORS, frame_constructor - return [c.name for c in prepare_constructors(include=DEFAULT_CONSTRUCTORS)] + return [ + name + for name, constructor in frame_constructor._registry.items() + if constructor.is_available and name in DEFAULT_CONSTRUCTORS + ] def pytest_addoption(parser: pytest.Parser) -> None: diff --git a/narwhals/testing/typing.py b/narwhals/testing/typing.py index 44049d1e5e..f03e946887 100644 --- a/narwhals/testing/typing.py +++ b/narwhals/testing/typing.py @@ -9,16 +9,16 @@ from narwhals.typing import IntoDataFrame, IntoFrame, IntoLazyFrame -__all__ = ("Data", "EagerFrameConstructor", "FrameConstructor", "LazyFrameConstructor") +__all__ = ("Data", "DataFrameConstructor", "FrameConstructor", "LazyFrameConstructor") FrameConstructor: TypeAlias = "frame_constructor[IntoFrame]" +"""Type alias for a constructor that returns a native eager or lazy frame.""" -EagerFrameConstructor: TypeAlias = "frame_constructor[IntoDataFrame]" +DataFrameConstructor: TypeAlias = "frame_constructor[IntoDataFrame]" """Type alias for a constructor that returns an eager native dataframe.""" LazyFrameConstructor: TypeAlias = "frame_constructor[IntoLazyFrame]" """Type alias for a constructor that returns a lazy native frame.""" - Data: TypeAlias = dict[str, Any] # TODO(Unassined): This should have a better annotation """A column-oriented mapping used as input to a frame constructor.""" diff --git a/tests/hypothesis/getitem_test.py b/tests/hypothesis/getitem_test.py index 60419f48a2..c303e80299 100644 --- a/tests/hypothesis/getitem_test.py +++ b/tests/hypothesis/getitem_test.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from collections.abc import Sequence - from narwhals.testing.typing import EagerFrameConstructor + from narwhals.testing.typing import DataFrameConstructor pytest.importorskip("pandas") pytest.importorskip("polars") @@ -23,9 +23,7 @@ @pytest.fixture( params=[get_constructor("pandas"), get_constructor("pyarrow")], scope="module" ) -def pandas_or_pyarrow_constructor( - request: pytest.FixtureRequest, -) -> EagerFrameConstructor: +def pandas_or_pyarrow_constructor(request: pytest.FixtureRequest) -> DataFrameConstructor: return request.param # type: ignore[no-any-return] @@ -120,7 +118,7 @@ def tuple_selector(draw: st.DrawFn) -> tuple[Any, Any]: @given(selector=st.one_of(single_selector, tuple_selector())) @pytest.mark.slow def test_getitem( - pandas_or_pyarrow_constructor: EagerFrameConstructor, selector: Any + pandas_or_pyarrow_constructor: DataFrameConstructor, selector: Any ) -> None: """Compare __getitem__ against polars.""" # TODO(PR - clean up): documenting current differences diff --git a/tests/testing/plugin_test.py b/tests/testing/plugin_test.py index 2205ac0f20..051b7707fe 100644 --- a/tests/testing/plugin_test.py +++ b/tests/testing/plugin_test.py @@ -15,9 +15,9 @@ def test_constructor_eager_fixture_runs_for_each_backend( pytester.makeconftest("") pytester.makepyfile(""" import narwhals as nw - from narwhals.testing.typing import EagerFrameConstructor + from narwhals.testing.typing import DataFrameConstructor - def test_shape(constructor_eager: EagerFrameConstructor) -> None: + def test_shape(constructor_eager: DataFrameConstructor) -> None: df = nw.from_native(constructor_eager({"x": [1, 2, 3]}), eager_only=True) assert df.shape == (3, 1) """) @@ -57,9 +57,9 @@ def test_columns(constructor: FrameConstructor) -> None: def test_external_constructor_disables_parametrisation(pytester: pytest.Pytester) -> None: pytester.makeconftest("") pytester.makepyfile(""" - from narwhals.testing.typing import EagerFrameConstructor + from narwhals.testing.typing import DataFrameConstructor - def test_unparam(constructor_eager: EagerFrameConstructor) -> None: + def test_unparam(constructor_eager: DataFrameConstructor) -> None: pass """) result = pytester.runpytest_subprocess("--use-external-constructor") diff --git a/tests/utils.py b/tests/utils.py index c51ee91d00..3281951896 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -15,7 +15,7 @@ from narwhals.dependencies import get_pandas from narwhals.testing.typing import ( # TODO(FBruzzesi): Remove these aliases once all the test suite migrates to *FrameConstructor's - EagerFrameConstructor as ConstructorEager, + DataFrameConstructor as ConstructorEager, FrameConstructor as Constructor, ) from narwhals.translate import from_native From 37c1edb691de1019f038c3c8291197175e24b928 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 21 Apr 2026 23:21:32 +0200 Subject: [PATCH 53/71] no cover + GHA --- .github/workflows/pytest.yml | 2 +- narwhals/testing/constructors.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index a908544fee..d043edc61e 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -177,7 +177,7 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 + coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 coverage combine coverage report --fail-under=50 diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index 79d21bf36d..17fa574edc 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -493,7 +493,7 @@ def available_constructors() -> frozenset[str]: ) -def available_cpu_constructors() -> frozenset[str]: +def available_cpu_constructors() -> frozenset[str]: # pragma: no cover """Return the names of every CPU constructor whose backend is importable. Examples: From 9712559aadec357185bd27bbd38dad3906d88e92 Mon Sep 17 00:00:00 2001 From: Francesco Bruzzesi <42817048+FBruzzesi@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:09:30 +0200 Subject: [PATCH 54/71] refactor: Rename exposed fixtures and pytest options (#3556) --- .github/workflows/extremes.yml | 8 +- .github/workflows/pytest-ibis.yml | 2 +- .github/workflows/pytest-modin.yml | 2 +- .github/workflows/pytest-pyspark.yml | 6 +- .github/workflows/pytest.yml | 16 ++-- .github/workflows/random_ci_pytest.yml | 2 +- Makefile | 2 +- docs/api-reference/testing.md | 43 +++++---- narwhals/testing/constructors.py | 50 +++++------ narwhals/testing/pytest_plugin.py | 106 +++++++++++------------ tests/conftest.py | 22 +++++ tests/expr_and_series/arithmetic_test.py | 38 ++++---- tests/hypothesis/getitem_test.py | 5 +- tests/ibis_test.py | 4 +- tests/series_only/hist_test.py | 4 +- tests/testing/constructors_test.py | 62 ++++++------- tests/testing/plugin_test.py | 16 ++-- tests/translate/from_native_test.py | 6 +- utils/sort_api_reference.py | 2 +- 19 files changed, 208 insertions(+), 188 deletions(-) diff --git a/.github/workflows/extremes.yml b/.github/workflows/extremes.yml index f18ee5a697..8d5d4a007b 100644 --- a/.github/workflows/extremes.yml +++ b/.github/workflows/extremes.yml @@ -46,7 +46,7 @@ jobs: echo "$DEPS" | grep 'duckdb==1.1' - name: Run pytest run: | - coverage run -m pytest tests --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb + coverage run -m pytest tests --runslow --nw-backends=pandas,pyarrow,polars[eager],polars[lazy],duckdb coverage combine coverage report --fail-under=50 @@ -87,7 +87,7 @@ jobs: echo "$DEPS" | grep 'duckdb==1.2' - name: Run pytest run: | - coverage run -m pytest tests --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb + coverage run -m pytest tests --runslow --nw-backends=pandas,pyarrow,polars[eager],polars[lazy],duckdb coverage combine coverage report --fail-under=50 @@ -127,7 +127,7 @@ jobs: echo "$DEPS" | grep 'duckdb==1.3' - name: Run pytest run: | - coverage run -m pytest tests --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb + coverage run -m pytest tests --runslow --nw-backends=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb coverage combine coverage report --fail-under=50 @@ -185,6 +185,6 @@ jobs: echo "$DEPS" | grep 'dask.*@' - name: Run pytest run: | - coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb + coverage run -m pytest tests --runslow --nw-backends=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb coverage combine coverage report --fail-under=50 diff --git a/.github/workflows/pytest-ibis.yml b/.github/workflows/pytest-ibis.yml index 5672b5aa06..9823bf1985 100644 --- a/.github/workflows/pytest-ibis.yml +++ b/.github/workflows/pytest-ibis.yml @@ -37,4 +37,4 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --constructors ibis + run: pytest tests --nw-backends ibis diff --git a/.github/workflows/pytest-modin.yml b/.github/workflows/pytest-modin.yml index 2a09e2bd2c..3f491f4ea4 100644 --- a/.github/workflows/pytest-modin.yml +++ b/.github/workflows/pytest-modin.yml @@ -35,4 +35,4 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --constructors modin[pyarrow] + run: pytest tests --nw-backends modin[pyarrow] diff --git a/.github/workflows/pytest-pyspark.yml b/.github/workflows/pytest-pyspark.yml index 125310e5cf..8ec17c834a 100644 --- a/.github/workflows/pytest-pyspark.yml +++ b/.github/workflows/pytest-pyspark.yml @@ -42,7 +42,7 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --constructors pyspark + coverage run -m pytest tests --runslow --nw-backends pyspark coverage combine coverage report --fail-under=95 --include "narwhals/_spark_like/*" @@ -71,7 +71,7 @@ jobs: - name: show-deps run: uv pip freeze - name: Run pytest - run: pytest tests --constructors pyspark + run: pytest tests --nw-backends pyspark pytest-pyspark-connect-constructor: strategy: @@ -138,7 +138,7 @@ jobs: - name: Run pytest run: | - coverage run -m pytest tests --runslow --constructors "pyspark[connect]" + coverage run -m pytest tests --runslow --nw-backends "pyspark[connect]" coverage combine coverage report --fail-under=95 --include="narwhals/_spark_like/*" diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index d043edc61e..562416d5f7 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -37,7 +37,7 @@ jobs: COVERAGE_PATCH_EXECV: ${{ matrix.os == 'windows-latest' && 'subprocess' || 'execv' }} COVERAGE_PATCH_FORK: ${{ matrix.os == 'windows-latest' && 'subprocess' || 'fork' }} run: | - coverage run -m pytest tests --constructors=pandas,pyarrow,polars[eager],polars[lazy] + coverage run -m pytest tests --nw-backends=pandas,pyarrow,polars[eager],polars[lazy] coverage combine coverage report --fail-under=75 - name: install-test-plugin @@ -74,7 +74,7 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 + coverage run -m pytest tests --runslow --nw-backends=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 coverage combine coverage report --fail-under=95 @@ -108,7 +108,7 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 + coverage run -m pytest tests --runslow --nw-backends=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe --durations=30 coverage combine coverage report --fail-under=100 - name: Run doctests @@ -139,20 +139,20 @@ jobs: uv pip install -e ".[pandas]" --group tests uv pip freeze - name: Run pytest (pandas and pandas[nullable]) - run: pytest tests --runslow --constructors=pandas,pandas[nullable] + run: pytest tests --runslow --nw-backends=pandas,pandas[nullable] - name: install-more-reqs run: | uv pip install -U pyarrow uv pip freeze - name: Run pytest (pandas[pyarrow] and pyarrow) - run: pytest tests --runslow --constructors=pandas[pyarrow],pyarrow + run: pytest tests --runslow --nw-backends=pandas[pyarrow],pyarrow - name: install-polars run: | uv pip uninstall pandas pyarrow uv pip install polars uv pip freeze - name: Run pytest (polars) - run: pytest tests --runslow --constructors=polars[eager],polars[lazy] + run: pytest tests --runslow --nw-backends=polars[eager],polars[lazy] python-314: strategy: @@ -177,7 +177,7 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 + coverage run -m pytest tests --runslow --nw-backends=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe --durations=30 coverage combine coverage report --fail-under=50 @@ -206,6 +206,6 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow + coverage run -m pytest tests --runslow --durations=30 --nw-backends=pandas,pandas[nullable],pandas[pyarrow],pyarrow coverage combine coverage report --fail-under=50 diff --git a/.github/workflows/random_ci_pytest.yml b/.github/workflows/random_ci_pytest.yml index 8531c0e639..7e73be44ab 100644 --- a/.github/workflows/random_ci_pytest.yml +++ b/.github/workflows/random_ci_pytest.yml @@ -37,6 +37,6 @@ jobs: run: uv pip freeze - name: Run pytest run: | - coverage run -m pytest tests --constructors=pandas,pyarrow,polars[eager],polars[lazy] + coverage run -m pytest tests --nw-backends=pandas,pyarrow,polars[eager],polars[lazy] coverage combine coverage report --fail-under=75 diff --git a/Makefile b/Makefile index eaa7565654..ba743a77b6 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,6 @@ test: ## Run unittest --editable .[ibis,modin,pyspark] \ --group core \ --group tests - $(VENV_BIN)/uv run --no-sync coverage run -m pytest tests --all-cpu-constructors --numprocesses=logical + $(VENV_BIN)/uv run --no-sync coverage run -m pytest tests --all-nw-backends --numprocesses=logical $(VENV_BIN)/uv run --no-sync coverage combine $(VENV_BIN)/uv run --no-sync coverage report --fail-under=95 diff --git a/docs/api-reference/testing.md b/docs/api-reference/testing.md index 4a8109fe32..d4bf928e59 100644 --- a/docs/api-reference/testing.md +++ b/docs/api-reference/testing.md @@ -1,10 +1,12 @@ # `narwhals.testing` -## Asserts +## Assertions ::: narwhals.testing handler: python options: + show_root_heading: false + heading_level: 3 members: - assert_frame_equal - assert_series_equal @@ -18,19 +20,22 @@ to build native frames from a column-oriented python `dict`. | Fixture | Backends | |---|---| -| `constructor` | every selected backend (eager + lazy) | -| `constructor_eager` | only eager backends | +| `nw_frame_constructor` | every selected backend (eager + lazy) | +| `nw_eager_constructor` | only eager backends | +| `nw_pandas_like_constructor` | pandas-like backends | -The selection is controlled by two CLI options: +### Pytest options -* `--constructors=pandas,polars[lazy],duckdb`: comma-separated list. - Defaults to [`DEFAULT_CONSTRUCTORS`][narwhals.testing.constructors.DEFAULT_CONSTRUCTORS] +The backend selection is controlled by the following CLI options: + +* `--nw-backends=pandas,polars[lazy],duckdb`: comma-separated list. + Defaults to [`DEFAULT_BACKENDS`][narwhals.testing.constructors.DEFAULT_BACKENDS] intersected with the backends installed in the current environment. -* `--all-cpu-constructors`: shortcut for "every CPU backend that is installed". -* `--use-external-constructor`: Skip narwhals.testing's parametrisation and let +* `--nw-all-backends`: shortcut for "every **CPU** backend that is installed". +* `--use-nw-external-constructor`: Skip narwhals.testing's parametrisation and let another plugin provide the `constructor*` fixtures. -Set the `NARWHALS_DEFAULT_CONSTRUCTORS` environment variable to override the default +Set the `NARWHALS_DEFAULT_BACKENDS` environment variable to override the default list (useful e.g. when running under `cudf.pandas`). ### Quick start @@ -43,12 +48,12 @@ from typing import TYPE_CHECKING import narwhals as nw if TYPE_CHECKING: - from narwhals.testing.typing import ConstructorEager, Data + from narwhals.testing.typing import EagerFrameConstructor, Data -def test_shape(constructor_eager: ConstructorEager) -> None: +def test_shape(nw_eager_constructor: EagerFrameConstructor) -> None: data: Data = {"x": [1, 2, 3]} - df = nw.from_native(constructor_eager(data), eager_only=True) + df = nw.from_native(nw_eager_constructor(data), eager_only=True) assert df.shape == (3, 1) ``` @@ -56,17 +61,19 @@ The fixtures are parametrised against every supported backend that is installed in the current environment. Filter the matrix on the command line: ```bash -pytest --constructors="pandas,polars[lazy]" -pytest --all-cpu-constructors +pytest --nw-backends="pandas,polars[lazy]" +pytest --all-nw-backends ``` -### Type aliases +## Type aliases ::: narwhals.testing.typing handler: python options: + show_root_heading: false + heading_level: 3 members: - - Constructor - - ConstructorEager - - ConstructorLazy - Data + - FrameConstructor + - EagerFrameConstructor + - LazyFrameConstructor diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index 17fa574edc..dab2d74fbf 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -65,12 +65,12 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: __all__ = ( - "available_constructors", - "available_cpu_constructors", + "available_backends", + "available_cpu_backends", "frame_constructor", - "get_constructor", + "get_backend_constructor", "is_backend_available", - "prepare_constructors", + "prepare_backends", "pyspark_session", "sqlframe_session", ) @@ -464,7 +464,7 @@ def ibis_lazy_constructor(obj: Data, /, **kwds: Any) -> ibis.Table: # pragma: n return _ibis_backend().create_table(table_name, table, **kwds) -DEFAULT_CONSTRUCTORS: frozenset[str] = frozenset( +DEFAULT_BACKENDS: frozenset[str] = frozenset( { "pandas", "pandas[pyarrow]", @@ -475,17 +475,17 @@ def ibis_lazy_constructor(obj: Data, /, **kwds: Any) -> ibis.Table: # pragma: n "ibis", } ) -"""Subset of constructors enabled by default for parametrised tests when the -user does not pass `--constructors` (mirrors the historical Narwhals defaults). +"""Subset of backends enabled by default for parametrised tests when the +user does not pass `--nw-backends` (mirrors the historical Narwhals defaults). """ -def available_constructors() -> frozenset[str]: +def available_backends() -> frozenset[str]: """Return the names of every constructor whose backend is importable. Examples: - >>> from narwhals.testing.constructors import available_constructors - >>> "pandas" in available_constructors() + >>> from narwhals.testing.constructors import available_backends + >>> "pandas" in available_backends() True """ return frozenset( @@ -493,12 +493,12 @@ def available_constructors() -> frozenset[str]: ) -def available_cpu_constructors() -> frozenset[str]: # pragma: no cover +def available_cpu_backends() -> frozenset[str]: # pragma: no cover """Return the names of every CPU constructor whose backend is importable. Examples: - >>> from narwhals.testing.constructors import available_cpu_constructors - >>> "pandas" in available_cpu_constructors() + >>> from narwhals.testing.constructors import available_cpu_backends + >>> "pandas" in available_cpu_backends() True """ return frozenset( @@ -524,14 +524,14 @@ def available_cpu_constructors() -> frozenset[str]: # pragma: no cover @overload -def get_constructor(name: EagerName) -> frame_constructor[IntoDataFrame]: ... +def get_backend_constructor(name: EagerName) -> frame_constructor[IntoDataFrame]: ... @overload -def get_constructor(name: LazyName) -> frame_constructor[IntoLazyFrame]: ... +def get_backend_constructor(name: LazyName) -> frame_constructor[IntoLazyFrame]: ... @overload -def get_constructor(name: str) -> frame_constructor[IntoFrame]: ... +def get_backend_constructor(name: str) -> frame_constructor[IntoFrame]: ... -def get_constructor(name: str) -> frame_constructor[IntoFrame]: +def get_backend_constructor(name: str) -> frame_constructor[IntoFrame]: """Return the registered constructor for `name`. Arguments: @@ -542,8 +542,8 @@ def get_constructor(name: str) -> frame_constructor[IntoFrame]: ValueError: If `name` is not a registered constructor identifier. Examples: - >>> from narwhals.testing.constructors import get_constructor - >>> get_constructor("pandas") + >>> from narwhals.testing.constructors import get_backend_constructor + >>> get_backend_constructor("pandas") frame_constructor(name='pandas') """ try: @@ -554,7 +554,7 @@ def get_constructor(name: str) -> frame_constructor[IntoFrame]: raise ValueError(msg) from exc -def prepare_constructors( +def prepare_backends( *, include: Iterable[str] | None = None, exclude: Iterable[str] | None = None ) -> list[frame_constructor[IntoFrame]]: """Return available constructors, optionally filtered. @@ -563,14 +563,14 @@ def prepare_constructors( `exclude` is given precedence in the selection. Arguments: - include: If given, only return constructors whose name is in this set. - exclude: If given, remove constructors whose name is in this set. + include: If given, only return backends whose name is in this set. + exclude: If given, remove backends whose name is in this set. Examples: - >>> from narwhals.testing.constructors import prepare_constructors - >>> constructors = prepare_constructors(include=["pandas", "polars[eager]"]) + >>> from narwhals.testing.constructors import prepare_backends + >>> backends = prepare_backends(include=["pandas", "polars[eager]"]) """ - available = available_constructors() + available = available_backends() candidates: list[frame_constructor[Any]] = [ c for name, c in frame_constructor._registry.items() if name in available ] diff --git a/narwhals/testing/pytest_plugin.py b/narwhals/testing/pytest_plugin.py index 418bb4e201..28e9701742 100644 --- a/narwhals/testing/pytest_plugin.py +++ b/narwhals/testing/pytest_plugin.py @@ -1,13 +1,11 @@ -"""Narwhals pytest plugin - auto-parametrises `constructor*` fixtures. +"""Narwhals pytest plugin - auto-parametrises fixtures. -NOTE: All imports from `narwhals.*` are deferred inside the hook functions so -that the entry-point module can be loaded by pytest without pulling in the -narwhals package tree. +NOTE: All imports from `narwhals.*` are deferred inside the hook functions so that +the entry-point module can be loaded by pytest without pulling in the narwhals package tree. -This is critical because entry-point plugins are loaded *before* `pytest-cov` -starts coverage measurement; any narwhals module imported at that stage would -have its module-level code (class definitions, constants, etc.) executed outside -the coverage tracer. +This is critical because entry-point plugins are loaded *before* `coveragepy` starts +coverage measurement; any narwhals module imported at that stage would have its +module-level code (class definitions, constants, etc.) executed outside the coverage tracer. """ from __future__ import annotations @@ -36,79 +34,70 @@ def _pandas_version() -> tuple[int, ...]: return parse_version(pd.__version__) -def _default_constructor_ids() -> list[str]: - """Resolve the default `--constructors` value for the current environment. +def _default_backend_ids() -> list[str]: + """Resolve the default `--nw-backends` value for the current environment. - Honours `NARWHALS_DEFAULT_CONSTRUCTORS` if set, otherwise restricts - [`DEFAULT_CONSTRUCTORS`][] to backends whose libraries are importable. + Honours `NARWHALS_DEFAULT_BACKENDS` if set, otherwise restricts + [`DEFAULT_BACKENDS`][] to backends whose libraries are importable. """ - if env := os.environ.get("NARWHALS_DEFAULT_CONSTRUCTORS"): # pragma: no cover + if env := os.environ.get("NARWHALS_DEFAULT_BACKENDS"): # pragma: no cover return env.split(",") - from narwhals.testing.constructors import DEFAULT_CONSTRUCTORS, frame_constructor + from narwhals.testing.constructors import DEFAULT_BACKENDS, frame_constructor return [ name for name, constructor in frame_constructor._registry.items() - if constructor.is_available and name in DEFAULT_CONSTRUCTORS + if constructor.is_available and name in DEFAULT_BACKENDS ] def pytest_addoption(parser: pytest.Parser) -> None: - from narwhals.testing.constructors import DEFAULT_CONSTRUCTORS + from narwhals.testing.constructors import DEFAULT_BACKENDS - group = parser.getgroup("narwhals", "narwhals.testing") - defaults = ", ".join(f"'{c}'" for c in sorted(DEFAULT_CONSTRUCTORS)) + group = parser.getgroup("narwhals", "narwhals-testing") + defaults = ", ".join(f"'{c}'" for c in sorted(DEFAULT_BACKENDS)) group.addoption( - "--constructors", + "--nw-backends", action="store", - default=",".join(_default_constructor_ids()), + default=",".join(_default_backend_ids()), type=str, help=( - "Comma-separated list of backend constructors to parametrise. " - f"Defaults to the installed subset of ({defaults})" + "Comma-separated list of (data|lazy) frame backend constructors to" + f"parametrise. Defaults to the installed subset of ({defaults})" ), ) group.addoption( - "--all-cpu-constructors", + "--all-nw-backends", action="store_true", default=False, - help=( - "Run tests against every installed CPU constructor " - "(overrides --constructors)." - ), + help=("Run tests against every installed CPU backend (overrides --nw-backends)."), ) - # Escape hatch for downstream test suites that ship their own constructor - # plugin. When set, this plugin still adds the CLI options but stops - # parametrising the fixtures. + # Escape hatch for downstream test suites that ship their own backend plugin. + # When set, this plugin still adds the CLI options but stops parametrising the fixtures. group.addoption( - "--use-external-constructor", + "--use-external-nw-backend", action="store_true", default=False, help=( - "Skip narwhals.testing's parametrisation and let another plugin " - "provide the `constructor*` fixtures." + "Skip narwhals-testing's parametrisation and let another plugin " + "provide the `nw_*frame_constructor` fixtures." ), ) -def _select_constructors( - config: pytest.Config, -) -> list[FrameConstructor]: # pragma: no cover - from narwhals.testing.constructors import ( - available_cpu_constructors, - prepare_constructors, - ) +def _select_backends(config: pytest.Config) -> list[FrameConstructor]: # pragma: no cover + from narwhals.testing.constructors import available_cpu_backends, prepare_backends _all_cpu_exclusions = frozenset({"modin", "pyspark[connect]"}) - if config.getoption("all_cpu_constructors"): - selected = prepare_constructors( - include=available_cpu_constructors(), exclude=_all_cpu_exclusions + if config.getoption("all_nw_backends"): + selected = prepare_backends( + include=available_cpu_backends(), exclude=_all_cpu_exclusions ) else: - opt = cast("str", config.getoption("constructors")) + opt = cast("str", config.getoption("nw_backends")) names = [c for c in opt.split(",") if c] - selected = prepare_constructors(include=names) + selected = prepare_backends(include=names) if _pandas_version() < _MIN_PANDAS_NULLABLE_VERSION: _pandas_nullables = {"pandas[nullable]", "pandas[pyarrow]"} @@ -117,24 +106,33 @@ def _select_constructors( def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - if metafunc.config.getoption("use_external_constructor"): # pragma: no cover + if metafunc.config.getoption("use_external_nw_backend"): # pragma: no cover return fixturenames = set(metafunc.fixturenames) - if not fixturenames & {"constructor", "constructor_eager", "constructor_pandas_like"}: + if not fixturenames & { + "nw_frame", + "nw_dataframe", + "nw_lazyframe", + "nw_pandas_like_frame", + }: return - selected = _select_constructors(metafunc.config) + selected = _select_backends(metafunc.config) - if "constructor_eager" in fixturenames: + if "nw_dataframe" in fixturenames: params = [c for c in selected if c.is_eager] ids = [c.name for c in params] - metafunc.parametrize("constructor_eager", params, ids=ids) - elif "constructor" in fixturenames: - metafunc.parametrize("constructor", selected, ids=[c.name for c in selected]) - elif "constructor_pandas_like" in fixturenames: + metafunc.parametrize("nw_dataframe", params, ids=ids) + elif "nw_lazyframe" in fixturenames: # pragma: no cover + params = [c for c in selected if not c.is_eager] + ids = [c.name for c in params] + metafunc.parametrize("nw_dataframe", params, ids=ids) + elif "nw_frame" in fixturenames: + metafunc.parametrize("nw_frame", selected, ids=[c.name for c in selected]) + elif "nw_pandas_like_frame" in fixturenames: params = [c for c in selected if c.is_eager and c.is_pandas_like] ids = [c.name for c in params] - metafunc.parametrize("constructor_pandas_like", params, ids=ids) + metafunc.parametrize("nw_pandas_like_frame", params, ids=ids) else: # pragma: no cover ... diff --git a/tests/conftest.py b/tests/conftest.py index dbfef9e335..f3f5ebf759 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ from typing_extensions import TypeAlias from narwhals._typing import EagerAllowed + from narwhals.testing.typing import DataFrameConstructor, FrameConstructor from narwhals.typing import NonNestedDType from tests.utils import NestedOrEnumDType @@ -117,3 +118,24 @@ def non_nested_type(request: pytest.FixtureRequest) -> type[NonNestedDType]: def nested_dtype(request: pytest.FixtureRequest) -> NestedOrEnumDType: dtype: NestedOrEnumDType = request.param return dtype + + +# The following fixtures are aliases of those registered in `narwhals/testing/pytest_plugin.py` +# in order to be backward compatible with the old fixture names and avoid having to change +# every single test. +# TODO(FBruzzesi): Rm once all tests start using nw_frame_constructor directly +@pytest.fixture +def constructor(nw_frame: FrameConstructor) -> FrameConstructor: + return nw_frame + + +@pytest.fixture +def constructor_eager(nw_dataframe: DataFrameConstructor) -> FrameConstructor: + return nw_dataframe + + +@pytest.fixture +def constructor_pandas_like( + nw_pandas_like_frame: DataFrameConstructor, +) -> FrameConstructor: + return nw_pandas_like_frame diff --git a/tests/expr_and_series/arithmetic_test.py b/tests/expr_and_series/arithmetic_test.py index af0c464e5b..ec6c2ff8bc 100644 --- a/tests/expr_and_series/arithmetic_test.py +++ b/tests/expr_and_series/arithmetic_test.py @@ -98,16 +98,16 @@ def test_arithmetic_series( attr: str, rhs: Any, expected: list[Any], - constructor_eager: ConstructorEager, + nw_dataframe: ConstructorEager, request: pytest.FixtureRequest, ) -> None: if attr == "__mod__" and any( - x in str(constructor_eager) for x in ["pandas_pyarrow", "modin_pyarrow"] + x in str(nw_dataframe) for x in ["pandas_pyarrow", "modin_pyarrow"] ): request.applymarker(pytest.mark.xfail) data = {"a": [1, 2, 3]} - df = nw.from_native(constructor_eager(data), eager_only=True) + df = nw.from_native(nw_dataframe(data), eager_only=True) result = df.select(getattr(df["a"], attr)(rhs)) assert_equal_data(result, {"a": expected}) @@ -128,29 +128,29 @@ def test_right_arithmetic_series( attr: str, rhs: Any, expected: list[Any], - constructor_eager: ConstructorEager, + nw_dataframe: ConstructorEager, request: pytest.FixtureRequest, ) -> None: if attr == "__rmod__" and any( - x in str(constructor_eager) for x in ["pandas_pyarrow", "modin_pyarrow"] + x in str(nw_dataframe) for x in ["pandas_pyarrow", "modin_pyarrow"] ): request.applymarker(pytest.mark.xfail) data = {"a": [1, 2, 3]} - df = nw.from_native(constructor_eager(data), eager_only=True) + df = nw.from_native(nw_dataframe(data), eager_only=True) result_series = getattr(df["a"], attr)(rhs) assert result_series.name == "a" assert_equal_data({"a": result_series}, {"a": expected}) def test_truediv_same_dims( - constructor_eager: ConstructorEager, request: pytest.FixtureRequest + nw_dataframe: ConstructorEager, request: pytest.FixtureRequest ) -> None: - if "polars" in str(constructor_eager): + if "polars" in str(nw_dataframe): # https://github.com/pola-rs/polars/issues/17760 request.applymarker(pytest.mark.xfail) - s_left = nw.from_native(constructor_eager({"a": [1, 2, 3]}), eager_only=True)["a"] - s_right = nw.from_native(constructor_eager({"a": [2, 2, 1]}), eager_only=True)["a"] + s_left = nw.from_native(nw_dataframe({"a": [1, 2, 3]}), eager_only=True)["a"] + s_right = nw.from_native(nw_dataframe({"a": [2, 2, 1]}), eager_only=True)["a"] result = s_left / s_right assert_equal_data({"a": result}, {"a": [0.5, 1.0, 3.0]}) result = s_left.__rtruediv__(s_right) @@ -160,13 +160,13 @@ def test_truediv_same_dims( @given(left=st.integers(-100, 100), right=st.integers(-100, 100)) @pytest.mark.skipif(PANDAS_VERSION < (2, 0), reason="convert_dtypes not available") @pytest.mark.slow -def test_floordiv(constructor_eager: ConstructorEager, *, left: int, right: int) -> None: - if any(x in str(constructor_eager) for x in ["modin", "cudf"]): +def test_floordiv(nw_dataframe: ConstructorEager, *, left: int, right: int) -> None: + if any(x in str(nw_dataframe) for x in ["modin", "cudf"]): # modin & cudf are too slow here pytest.skip() assume(right != 0) expected = {"a": [left // right]} - result = nw.from_native(constructor_eager({"a": [left]}), eager_only=True).select( + result = nw.from_native(nw_dataframe({"a": [left]}), eager_only=True).select( nw.col("a") // right ) assert_equal_data(result, expected) @@ -175,14 +175,14 @@ def test_floordiv(constructor_eager: ConstructorEager, *, left: int, right: int) @pytest.mark.slow @given(left=st.integers(-100, 100), right=st.integers(-100, 100)) @pytest.mark.skipif(PANDAS_VERSION < (2, 0), reason="convert_dtypes not available") -def test_mod(constructor_eager: ConstructorEager, *, left: int, right: int) -> None: - if any(x in str(constructor_eager) for x in ["pandas_pyarrow", "modin", "cudf"]): +def test_mod(nw_dataframe: ConstructorEager, *, left: int, right: int) -> None: + if any(x in str(nw_dataframe) for x in ["pandas_pyarrow", "modin", "cudf"]): # pandas[pyarrow] does not implement mod # modin & cudf are too slow here pytest.skip() assume(right != 0) expected = {"a": [left % right]} - result = nw.from_native(constructor_eager({"a": [left]}), eager_only=True).select( + result = nw.from_native(nw_dataframe({"a": [left]}), eager_only=True).select( nw.col("a") % right ) assert_equal_data(result, expected) @@ -240,16 +240,16 @@ def test_arithmetic_series_left_literal( attr: str, lhs: Any, expected: list[Any], - constructor_eager: ConstructorEager, + nw_dataframe: ConstructorEager, request: pytest.FixtureRequest, ) -> None: if attr == "__mod__" and any( - x in str(constructor_eager) for x in ["pandas_pyarrow", "modin_pyarrow"] + x in str(nw_dataframe) for x in ["pandas_pyarrow", "modin_pyarrow"] ): request.applymarker(pytest.mark.xfail) data = {"a": [1.0, 2.0, 4.0]} - df = nw.from_native(constructor_eager(data)) + df = nw.from_native(nw_dataframe(data)) result = df.select(getattr(lhs, attr)(nw.col("a"))) assert_equal_data(result, {"literal": expected}) diff --git a/tests/hypothesis/getitem_test.py b/tests/hypothesis/getitem_test.py index c303e80299..d9fc517f60 100644 --- a/tests/hypothesis/getitem_test.py +++ b/tests/hypothesis/getitem_test.py @@ -7,7 +7,7 @@ from hypothesis import assume, given import narwhals as nw -from narwhals.testing.constructors import get_constructor +from narwhals.testing.constructors import get_backend_constructor from tests.utils import assert_equal_data if TYPE_CHECKING: @@ -21,7 +21,8 @@ @pytest.fixture( - params=[get_constructor("pandas"), get_constructor("pyarrow")], scope="module" + params=[get_backend_constructor("pandas"), get_backend_constructor("pyarrow")], + scope="module", ) def pandas_or_pyarrow_constructor(request: pytest.FixtureRequest) -> DataFrameConstructor: return request.param # type: ignore[no-any-return] diff --git a/tests/ibis_test.py b/tests/ibis_test.py index f537ef1c99..10dce38413 100644 --- a/tests/ibis_test.py +++ b/tests/ibis_test.py @@ -3,11 +3,11 @@ import pytest import narwhals as nw -from narwhals.testing.constructors import get_constructor +from narwhals.testing.constructors import get_backend_constructor def test_from_native() -> None: - ibis_constructor = get_constructor("ibis") + ibis_constructor = get_backend_constructor("ibis") if not ibis_constructor.is_available: pytest.skip() df = nw.from_native(ibis_constructor({"a": [1, 2, 3], "b": [4, 5, 6]})) diff --git a/tests/series_only/hist_test.py b/tests/series_only/hist_test.py index 56cb68ccb4..9bf4f26c62 100644 --- a/tests/series_only/hist_test.py +++ b/tests/series_only/hist_test.py @@ -11,7 +11,7 @@ import narwhals as nw from narwhals.exceptions import ComputeError -from narwhals.testing.constructors import get_constructor +from narwhals.testing.constructors import get_backend_constructor from tests.utils import POLARS_VERSION, ConstructorEager, assert_equal_data if TYPE_CHECKING: @@ -50,7 +50,7 @@ def maybe_name_to_constructor(name: EagerName) -> ConstructorEager: - constructor = get_constructor(name) + constructor = get_backend_constructor(name) if constructor.is_available: return constructor diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index 5a6cdaa340..af22cee8a1 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -7,10 +7,9 @@ import narwhals as nw from narwhals._utils import Implementation from narwhals.testing.constructors import ( - available_constructors, - frame_constructor, - get_constructor, - prepare_constructors, + available_backends, + get_backend_constructor, + prepare_backends, ) if TYPE_CHECKING: @@ -22,7 +21,7 @@ def test_eager_returns_eager_frame() -> None: - c = get_constructor("pandas") + c = get_backend_constructor("pandas") if not c.is_available: pytest.skip() @@ -31,7 +30,7 @@ def test_eager_returns_eager_frame() -> None: def test_lazy_returns_lazy_frame() -> None: - c = get_constructor("polars[lazy]") + c = get_backend_constructor("polars[lazy]") if not c.is_available: pytest.skip() @@ -63,70 +62,63 @@ def test_constructor_is_properties( prop: str, true_names: TrueNames, false_names: FalseNames ) -> None: for name in true_names: - c = get_constructor(name) + c = get_backend_constructor(name) assert getattr(c, prop), f"{name}.{prop} should be True" for name in false_names: - c = get_constructor(name) + c = get_backend_constructor(name) assert not getattr(c, prop), f"{name}.{prop} should be False" def test_constructor_implementation() -> None: - assert get_constructor("pandas").implementation is Implementation.PANDAS - assert get_constructor("pandas[pyarrow]").implementation is Implementation.PANDAS - assert get_constructor("polars[eager]").implementation is Implementation.POLARS + assert get_backend_constructor("pandas").implementation is Implementation.PANDAS assert ( - get_constructor("pyspark[connect]").implementation + get_backend_constructor("pandas[pyarrow]").implementation is Implementation.PANDAS + ) + assert ( + get_backend_constructor("polars[eager]").implementation is Implementation.POLARS + ) + assert ( + get_backend_constructor("pyspark[connect]").implementation is Implementation.PYSPARK_CONNECT ) def test_constructor_dunder() -> None: - c1 = get_constructor("pandas") - c2 = get_constructor("pandas") + c1 = get_backend_constructor("pandas") + c2 = get_backend_constructor("pandas") assert c1.identifier == "pandas" assert c1 == c2 assert hash(c1) == hash(c2) - assert c1 != get_constructor("polars[eager]") + assert c1 != get_backend_constructor("polars[eager]") assert c1 != "not a constructor" -def test_register_requires_implementation() -> None: - with pytest.raises(TypeError, match="implementation"): - frame_constructor.register( # type: ignore[call-arg] - name="_bad", requirements=("polars",) - ) - - -def test_get_constructor() -> None: - assert get_constructor("pandas[pyarrow]") == get_constructor("pandas[pyarrow]") - - -def test_get_constructor_invalid_name() -> None: +def test_get_backend_constructor_invalid_name() -> None: with pytest.raises(ValueError, match="Unknown constructor"): - get_constructor("not_a_backend") + get_backend_constructor("not_a_backend") @pytest.mark.parametrize( ("include", "exclude", "expected"), [ - (None, None, available_constructors()), - (None, ["pandas"], available_constructors() - {"pandas"}), + (None, None, available_backends()), + (None, ["pandas"], available_backends() - {"pandas"}), (["pandas", "polars[eager]"], None, {"pandas", "polars[eager]"}), (["pandas", "polars[eager]"], ["pandas"], {"polars[eager]"}), ([], None, frozenset()), ], ) -def test_prepare_constructors( +def test_prepare_backends( include: list[str] | None, exclude: list[str] | None, expected: frozenset[str] ) -> None: for name in (*(include or ()), *(exclude or ())): - if not get_constructor(name).is_available: + if not get_backend_constructor(name).is_available: pytest.skip(f"{name} not installed") - result = prepare_constructors(include=include, exclude=exclude) + result = prepare_backends(include=include, exclude=exclude) assert {c.name for c in result} == expected @pytest.mark.parametrize("kwarg", ["include", "exclude"]) -def test_prepare_constructors_unknown_name_raises(kwarg: str) -> None: +def test_prepare_backends_unknown_name_raises(kwarg: str) -> None: with pytest.raises(ValueError, match="not known constructors"): - prepare_constructors(**{kwarg: ["not_a_backend"]}) + prepare_backends(**{kwarg: ["not_a_backend"]}) diff --git a/tests/testing/plugin_test.py b/tests/testing/plugin_test.py index 051b7707fe..9f7b6e45a8 100644 --- a/tests/testing/plugin_test.py +++ b/tests/testing/plugin_test.py @@ -17,12 +17,12 @@ def test_constructor_eager_fixture_runs_for_each_backend( import narwhals as nw from narwhals.testing.typing import DataFrameConstructor - def test_shape(constructor_eager: DataFrameConstructor) -> None: - df = nw.from_native(constructor_eager({"x": [1, 2, 3]}), eager_only=True) + def test_shape(nw_dataframe: DataFrameConstructor) -> None: + df = nw.from_native(nw_dataframe({"x": [1, 2, 3]}), eager_only=True) assert df.shape == (3, 1) """) result = pytester.runpytest_subprocess( - "-v", "-p", "no:randomly", "--constructors=pandas,polars[eager],pyarrow" + "-v", "-p", "no:randomly", "--nw-backends=pandas,polars[eager],pyarrow" ) result.assert_outcomes(passed=3) result.stdout.fnmatch_lines( @@ -44,12 +44,12 @@ def test_constructor_fixture_includes_lazy_backends(pytester: pytest.Pytester) - import narwhals as nw from narwhals.testing.typing import FrameConstructor - def test_columns(constructor: FrameConstructor) -> None: - df = nw.from_native(constructor({"x": [1, 2, 3]})) + def test_columns(nw_frame: FrameConstructor) -> None: + df = nw.from_native(nw_frame({"x": [1, 2, 3]})) assert df.collect_schema().names() == ["x"] """) result = pytester.runpytest_subprocess( - "-v", "--constructors=pandas,polars[lazy],duckdb" + "-v", "--nw-backends=pandas,polars[lazy],duckdb" ) result.assert_outcomes(passed=3) @@ -59,9 +59,9 @@ def test_external_constructor_disables_parametrisation(pytester: pytest.Pytester pytester.makepyfile(""" from narwhals.testing.typing import DataFrameConstructor - def test_unparam(constructor_eager: DataFrameConstructor) -> None: + def test_unparam(nw_dataframe: DataFrameConstructor) -> None: pass """) - result = pytester.runpytest_subprocess("--use-external-constructor") + result = pytester.runpytest_subprocess("--use-external-nw-backend") # Without external parametrisation in place, the fixture is missing. result.assert_outcomes(errors=1) diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 21bd7ce7b9..a0d94b99fb 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -30,7 +30,7 @@ import narwhals as nw from narwhals._utils import Version -from narwhals.testing.constructors import get_constructor +from narwhals.testing.constructors import get_backend_constructor from tests.utils import Constructor, maybe_get_modin_df if TYPE_CHECKING: @@ -294,7 +294,7 @@ def test_eager_only_lazy_dask(eager_only: Any, context: Any) -> None: def test_series_only_sqlframe() -> None: # pragma: no cover pytest.importorskip("sqlframe") - df = get_constructor("sqlframe")(data) + df = get_backend_constructor("sqlframe")(data) with pytest.raises(TypeError, match="Cannot only use `series_only`"): nw.from_native(df, series_only=True) # type: ignore[call-overload] @@ -315,7 +315,7 @@ def test_series_only_sqlframe() -> None: # pragma: no cover ) def test_eager_only_sqlframe(eager_only: Any, context: Any) -> None: # pragma: no cover pytest.importorskip("sqlframe") - df = get_constructor("sqlframe")(data) + df = get_backend_constructor("sqlframe")(data) with context: res = nw.from_native(df, eager_only=eager_only) diff --git a/utils/sort_api_reference.py b/utils/sort_api_reference.py index 1b417ed63a..243ccbcd6d 100644 --- a/utils/sort_api_reference.py +++ b/utils/sort_api_reference.py @@ -42,7 +42,7 @@ def sort_list(match: re.Match[str]) -> str: PATH = Path("docs") / "api-reference" -FILES_TO_SKIP = {"dtypes", "typing"} +FILES_TO_SKIP = {"dtypes", "typing", "testing"} ret = max( sort_members_in_markdown(file_path=file_path) From 6355f0abf5fb1b82768466a1fd437478837bed20 Mon Sep 17 00:00:00 2001 From: Francesco Bruzzesi <42817048+FBruzzesi@users.noreply.github.com> Date: Tue, 5 May 2026 22:25:50 +0200 Subject: [PATCH 55/71] feat: Let constructor fixtures return narwhals object directly (#3569) --- narwhals/testing/constructors.py | 53 ++++++++- tests/conftest.py | 73 +++++++++--- .../is_narwhals_dataframe_test.py | 5 +- .../is_narwhals_lazyframe_test.py | 5 +- tests/expr_and_series/arithmetic_test.py | 26 ++--- .../dt/datetime_attributes_test.py | 4 +- .../dt/datetime_duration_test.py | 2 +- tests/expr_and_series/over_test.py | 4 +- tests/expr_and_series/struct_/field_test.py | 69 +++-------- tests/frame/join_test.py | 108 +++++++++--------- tests/frame/sample_test.py | 10 +- tests/frame/schema_test.py | 8 +- tests/frame/to_native_test.py | 2 +- tests/frame/to_pandas_test.py | 6 +- tests/hypothesis/getitem_test.py | 2 +- tests/ibis_test.py | 2 +- tests/namespace_test.py | 2 +- ...erve_pandas_like_columns_name_attr_test.py | 8 +- tests/series_only/hist_test.py | 101 ++++++---------- tests/series_only/to_native_test.py | 2 +- tests/testing/assert_frame_equal_test.py | 6 +- tests/testing/assert_series_equal_test.py | 4 +- tests/testing/conftest.py | 2 +- tests/testing/constructors_test.py | 4 +- tests/testing/plugin_test.py | 4 +- tests/translate/from_native_test.py | 8 +- tests/utils.py | 18 +-- tests/v1_test.py | 22 ++-- tests/v2_test.py | 12 +- 29 files changed, 293 insertions(+), 279 deletions(-) diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index dab2d74fbf..1ea6737d29 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -49,6 +49,7 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: if TYPE_CHECKING: from collections.abc import Iterable + from types import ModuleType import ibis import pandas as pd @@ -59,9 +60,16 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: from sqlframe.duckdb import DuckDBSession from typing_extensions import Concatenate, TypeAlias + from narwhals import DataFrame, LazyFrame from narwhals._native import NativeDask, NativeDuckDB, NativePySpark, NativeSQLFrame from narwhals.testing.typing import Data - from narwhals.typing import IntoDataFrame, IntoFrame, IntoLazyFrame + from narwhals.typing import ( + IntoDataFrame, + IntoDataFrameT, + IntoFrame, + IntoLazyFrame, + IntoLazyFrameT, + ) __all__ = ( @@ -97,6 +105,8 @@ class frame_constructor(Generic[T_co]): # noqa: N801 _registry: ClassVar[dict[str, frame_constructor[IntoFrame]]] = {} + func: Callable[Concatenate[Data, ...], T_co] + def __init__( self, func: Callable[Concatenate[Data, ...], T_co], @@ -159,9 +169,44 @@ def decorator(func: Callable[Concatenate[Data, ...], R]) -> frame_constructor[R] return decorator - def __call__(self, obj: Data, /, **kwds: Any) -> T_co: - """Build a native frame from `obj` by delegating to the wrapped function.""" - return self.func(obj, **kwds) + @overload + def __call__( + self: frame_constructor[IntoDataFrameT], + obj: Data, + /, + namespace: ModuleType, + **kwds: Any, + ) -> DataFrame[IntoDataFrameT]: ... + @overload + def __call__( + self: frame_constructor[IntoLazyFrameT], + obj: Data, + /, + namespace: ModuleType, + **kwds: Any, + ) -> LazyFrame[IntoLazyFrameT]: ... + @overload + def __call__( + self: frame_constructor[IntoFrame], + obj: Data, + /, + namespace: ModuleType, + **kwds: Any, + ) -> DataFrame[Any] | LazyFrame[Any]: ... + + def __call__( + self, obj: Data, /, namespace: ModuleType, **kwds: Any + ) -> DataFrame[Any] | LazyFrame[Any]: + """Build a native frame and wrap it with `namespace.from_native`. + + Arguments: + obj: Column-oriented mapping passed to the wrapped builder. + namespace: A narwhals namespace (e.g. `narwhals`, `narwhals.stable.v1`) + whose `from_native` performs the wrapping. + **kwds: Forwarded to the wrapped builder. + """ + native = self.func(obj, **kwds) + return namespace.from_native(native) # type: ignore[no-any-return] @property def identifier(self) -> str: diff --git a/tests/conftest.py b/tests/conftest.py index f3f5ebf759..ed94521264 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ from __future__ import annotations from importlib.util import find_spec -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import pytest @@ -15,16 +15,15 @@ if TYPE_CHECKING: from collections.abc import Sequence - - from typing_extensions import TypeAlias + from types import ModuleType from narwhals._typing import EagerAllowed - from narwhals.testing.typing import DataFrameConstructor, FrameConstructor - from narwhals.typing import NonNestedDType + from narwhals.dataframe import DataFrame, LazyFrame + from narwhals.testing.constructors import frame_constructor + from narwhals.testing.typing import Data, DataFrameConstructor, FrameConstructor + from narwhals.typing import IntoFrame, NonNestedDType from tests.utils import NestedOrEnumDType - Data: TypeAlias = "dict[str, list[Any]]" - # Narwhals-internal pytest options (not part of the public testing plugin) @@ -120,22 +119,62 @@ def nested_dtype(request: pytest.FixtureRequest) -> NestedOrEnumDType: return dtype -# The following fixtures are aliases of those registered in `narwhals/testing/pytest_plugin.py` -# in order to be backward compatible with the old fixture names and avoid having to change -# every single test. -# TODO(FBruzzesi): Rm once all tests start using nw_frame_constructor directly +# The following fixtures are aliases of those registered in `narwhals/testing/pytest_plugin.py`, +# wrapped so that calling them without an explicit `namespace` defaults to the main +# `narwhals` namespace. Tests can still pass `nw_v1` / `nw_v2` explicitly to opt in +# to a stable namespace; the legacy pattern `nw.from_native(constructor(data))` keeps +# working because `nw.from_native` is idempotent on narwhals objects. +# TODO(FBruzzesi): Drop these aliases once every test calls `nw_frame` / `nw_dataframe` +# directly with an explicit namespace. + + +class _PatchedFrameConstructor: + """Proxy over a `frame_constructor` defaulting `namespace` to `narwhals`. + + Delegates attribute access, `str()`, and `repr()` to the wrapped instance + so that test helpers (e.g. `constructor.is_nullable`, `"pandas" in str(constructor)`) + keep working unchanged. + """ + + __slots__ = ("_inner",) + + def __init__(self, inner: frame_constructor[IntoFrame]) -> None: + self._inner = inner + + def __call__( + self, obj: Data, /, namespace: ModuleType = nw, **kwds: Any + ) -> DataFrame[Any] | LazyFrame[Any]: + return self._inner(obj, namespace=namespace, **kwds) + + def __getattr__(self, name: str) -> Any: + return getattr(self._inner, name) + + def __str__(self) -> str: + return str(self._inner) + + def __repr__(self) -> str: + return repr(self._inner) + + +class _PatchedDataFrameConstructor(_PatchedFrameConstructor): + def __call__( + self, obj: Data, /, namespace: ModuleType = nw, **kwds: Any + ) -> DataFrame[Any]: + return cast("DataFrame[Any]", self._inner(obj, namespace=namespace, **kwds)) + + @pytest.fixture -def constructor(nw_frame: FrameConstructor) -> FrameConstructor: - return nw_frame +def constructor(nw_frame: FrameConstructor) -> _PatchedFrameConstructor: + return _PatchedFrameConstructor(nw_frame) @pytest.fixture -def constructor_eager(nw_dataframe: DataFrameConstructor) -> FrameConstructor: - return nw_dataframe +def constructor_eager(nw_dataframe: DataFrameConstructor) -> _PatchedDataFrameConstructor: + return _PatchedDataFrameConstructor(nw_dataframe) @pytest.fixture def constructor_pandas_like( nw_pandas_like_frame: DataFrameConstructor, -) -> FrameConstructor: - return nw_pandas_like_frame +) -> _PatchedDataFrameConstructor: + return _PatchedDataFrameConstructor(nw_pandas_like_frame) diff --git a/tests/dependencies/is_narwhals_dataframe_test.py b/tests/dependencies/is_narwhals_dataframe_test.py index aeedf15981..0897e64cc8 100644 --- a/tests/dependencies/is_narwhals_dataframe_test.py +++ b/tests/dependencies/is_narwhals_dataframe_test.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING -import narwhals as nw from narwhals.stable.v1.dependencies import is_narwhals_dataframe if TYPE_CHECKING: @@ -12,5 +11,5 @@ def test_is_narwhals_dataframe(constructor_eager: ConstructorEager) -> None: df = constructor_eager({"col1": [1, 2], "col2": [3, 4]}) - assert is_narwhals_dataframe(nw.from_native(df)) - assert not is_narwhals_dataframe(df) + assert is_narwhals_dataframe(df) + assert not is_narwhals_dataframe(df.to_native()) diff --git a/tests/dependencies/is_narwhals_lazyframe_test.py b/tests/dependencies/is_narwhals_lazyframe_test.py index 0e4c6e1bd9..113fd4a511 100644 --- a/tests/dependencies/is_narwhals_lazyframe_test.py +++ b/tests/dependencies/is_narwhals_lazyframe_test.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING -import narwhals as nw from narwhals.stable.v1.dependencies import is_narwhals_lazyframe from tests.utils import Constructor @@ -13,5 +12,5 @@ def test_is_narwhals_lazyframe(constructor: Constructor) -> None: lf = constructor({"a": [1, 2, 3]}) - assert is_narwhals_lazyframe(nw.from_native(lf).lazy()) - assert not is_narwhals_lazyframe(lf) + assert is_narwhals_lazyframe(lf.lazy()) + assert not is_narwhals_lazyframe(lf.to_native()) diff --git a/tests/expr_and_series/arithmetic_test.py b/tests/expr_and_series/arithmetic_test.py index ec6c2ff8bc..9755435871 100644 --- a/tests/expr_and_series/arithmetic_test.py +++ b/tests/expr_and_series/arithmetic_test.py @@ -45,7 +45,7 @@ def test_arithmetic_expr( request.applymarker(pytest.mark.xfail) data = {"a": [1.0, 2.0, 3.0]} - df = nw.from_native(constructor(data)) + df = constructor(data, nw) result = df.select(getattr(nw.col("a"), attr)(rhs)) assert_equal_data(result, {"a": expected}) @@ -76,7 +76,7 @@ def test_right_arithmetic_expr( ): request.applymarker(pytest.mark.xfail) data = {"a": [1, 2, 3]} - df = nw.from_native(constructor(data)) + df = constructor(data) result = df.select(getattr(nw.col("a"), attr)(rhs)) assert_equal_data(result, {"literal": expected}) @@ -107,7 +107,7 @@ def test_arithmetic_series( request.applymarker(pytest.mark.xfail) data = {"a": [1, 2, 3]} - df = nw.from_native(nw_dataframe(data), eager_only=True) + df = nw_dataframe(data, nw) result = df.select(getattr(df["a"], attr)(rhs)) assert_equal_data(result, {"a": expected}) @@ -137,7 +137,7 @@ def test_right_arithmetic_series( request.applymarker(pytest.mark.xfail) data = {"a": [1, 2, 3]} - df = nw.from_native(nw_dataframe(data), eager_only=True) + df = nw_dataframe(data, nw) result_series = getattr(df["a"], attr)(rhs) assert result_series.name == "a" assert_equal_data({"a": result_series}, {"a": expected}) @@ -149,8 +149,8 @@ def test_truediv_same_dims( if "polars" in str(nw_dataframe): # https://github.com/pola-rs/polars/issues/17760 request.applymarker(pytest.mark.xfail) - s_left = nw.from_native(nw_dataframe({"a": [1, 2, 3]}), eager_only=True)["a"] - s_right = nw.from_native(nw_dataframe({"a": [2, 2, 1]}), eager_only=True)["a"] + s_left = nw_dataframe({"a": [1, 2, 3]}, nw)["a"] + s_right = nw_dataframe({"a": [2, 2, 1]}, nw)["a"] result = s_left / s_right assert_equal_data({"a": result}, {"a": [0.5, 1.0, 3.0]}) result = s_left.__rtruediv__(s_right) @@ -166,9 +166,7 @@ def test_floordiv(nw_dataframe: ConstructorEager, *, left: int, right: int) -> N pytest.skip() assume(right != 0) expected = {"a": [left // right]} - result = nw.from_native(nw_dataframe({"a": [left]}), eager_only=True).select( - nw.col("a") // right - ) + result = nw_dataframe({"a": [left]}, nw).select(nw.col("a") // right) assert_equal_data(result, expected) @@ -182,9 +180,7 @@ def test_mod(nw_dataframe: ConstructorEager, *, left: int, right: int) -> None: pytest.skip() assume(right != 0) expected = {"a": [left % right]} - result = nw.from_native(nw_dataframe({"a": [left]}), eager_only=True).select( - nw.col("a") % right - ) + result = nw_dataframe({"a": [left]}, nw).select(nw.col("a") % right) assert_equal_data(result, expected) @@ -218,7 +214,7 @@ def test_arithmetic_expr_left_literal( request.applymarker(pytest.mark.xfail) data = {"a": [1.0, 2.0, 4.0]} - df = nw.from_native(constructor(data)) + df = constructor(data, nw) result = df.select(getattr(lhs, attr)(nw.col("a"))) assert_equal_data(result, {"literal": expected}) @@ -249,7 +245,7 @@ def test_arithmetic_series_left_literal( request.applymarker(pytest.mark.xfail) data = {"a": [1.0, 2.0, 4.0]} - df = nw.from_native(nw_dataframe(data)) + df = nw_dataframe(data, nw) result = df.select(getattr(lhs, attr)(nw.col("a"))) assert_equal_data(result, {"literal": expected}) @@ -258,7 +254,7 @@ def test_std_broadcating(constructor: Constructor) -> None: if "duckdb" in str(constructor) and DUCKDB_VERSION < (1, 3): # `std(ddof=2)` fails for duckdb here pytest.skip() - df = nw.from_native(constructor({"a": [1, 2, 3]})) + df = constructor({"a": [1, 2, 3]}, nw) result = df.with_columns(b=nw.col("a").std()).sort("a") expected = {"a": [1, 2, 3], "b": [1.0, 1.0, 1.0]} assert_equal_data(result, expected) diff --git a/tests/expr_and_series/dt/datetime_attributes_test.py b/tests/expr_and_series/dt/datetime_attributes_test.py index c7bf55e7c0..830666cac2 100644 --- a/tests/expr_and_series/dt/datetime_attributes_test.py +++ b/tests/expr_and_series/dt/datetime_attributes_test.py @@ -123,10 +123,10 @@ def test_to_date(request: pytest.FixtureRequest, constructor: Constructor) -> No request.applymarker(pytest.mark.xfail) dates = {"a": [datetime(2001, 1, 1), None, datetime(2001, 1, 3)]} if "dask" in str(constructor): - df_dask = cast("dd.DataFrame", constructor(dates)) + df_dask = cast("dd.DataFrame", constructor(dates).to_native()) df_dask = cast("dd.DataFrame", df_dask.astype({"a": "timestamp[ns][pyarrow]"})) df = nw.from_native(df_dask) else: - df = nw.from_native(constructor(dates)) + df = constructor(dates) result = df.select(nw.col("a").dt.date()) assert result.collect_schema() == {"a": nw.Date} diff --git a/tests/expr_and_series/dt/datetime_duration_test.py b/tests/expr_and_series/dt/datetime_duration_test.py index b84ecfa66e..ac7d132bfc 100644 --- a/tests/expr_and_series/dt/datetime_duration_test.py +++ b/tests/expr_and_series/dt/datetime_duration_test.py @@ -74,7 +74,7 @@ def test_duration_attributes_nano( import numpy as np data = {"c": np.array([None, 20], dtype="timedelta64[ns]")} - df = nw.from_native(constructor(data)) + df = constructor(data, nw) result_c = df.select(getattr(nw.col("c").dt, attribute)().fill_null(0)) assert_equal_data(result_c, {"c": expected_c}) diff --git a/tests/expr_and_series/over_test.py b/tests/expr_and_series/over_test.py index ec9dea6104..7ea345af30 100644 --- a/tests/expr_and_series/over_test.py +++ b/tests/expr_and_series/over_test.py @@ -475,10 +475,10 @@ def test_over_quantile(constructor: Constructor, request: pytest.FixtureRequest) data = {"a": [1, 2, 3, 4, 5, 6], "b": ["x", "x", "x", "y", "y", "y"]} quantile_expr = nw.col("a").quantile(quantile=0.5, interpolation="linear") - native_frame = constructor(data) + native_frame = constructor(data).to_native() if "dask" in str(constructor): - native_frame = native_frame.repartition(npartitions=1) # type: ignore[union-attr] + native_frame = native_frame.repartition(npartitions=1) result = ( nw.from_native(native_frame) diff --git a/tests/expr_and_series/struct_/field_test.py b/tests/expr_and_series/struct_/field_test.py index a351c31500..dd8dd26790 100644 --- a/tests/expr_and_series/struct_/field_test.py +++ b/tests/expr_and_series/struct_/field_test.py @@ -1,84 +1,53 @@ from __future__ import annotations -from typing import cast - import pytest import narwhals as nw -from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data +from tests.utils import ( + DUCKDB_VERSION, + PANDAS_VERSION, + Constructor, + ConstructorEager, + assert_equal_data, +) def test_get_field_expr(request: pytest.FixtureRequest, constructor: Constructor) -> None: pytest.importorskip("pyarrow") - import pyarrow as pa - if any(backend in str(constructor) for backend in ("dask", "modin")): + if any(backend in str(constructor) for backend in ("dask",)): request.applymarker(pytest.mark.xfail) - if "pandas" in str(constructor) and PANDAS_VERSION < (2, 2, 0): + if ("pandas" in str(constructor) and PANDAS_VERSION < (2, 2, 0)) or ( + "duckdb" in str(constructor) and DUCKDB_VERSION < (1, 3, 0) + ): pytest.skip() - data = {"user": [{"id": "0", "name": "john"}, {"id": "1", "name": "jane"}]} - - df_native = constructor(data) - - if "pandas" in str(constructor): - import pandas as pd - df_native = cast("pd.DataFrame", df_native).assign( - user=pd.Series( - data["user"], - dtype=pd.ArrowDtype( - pa.struct([("id", pa.string()), ("name", pa.string())]) - ), - ) - ) - - df = nw.from_native(df_native) + data = {"id": ["0", "1"], "name": ["john", "jane"]} + expected = data.copy() + df = constructor(data, nw).select(user=nw.struct("id", "name")) result = nw.from_native(df).select( nw.col("user").struct.field("id"), nw.col("user").struct.field("name") ) - expected = {"id": ["0", "1"], "name": ["john", "jane"]} assert_equal_data(result, expected) result = nw.from_native(df).select(nw.col("user").struct.field("id").name.keep()) expected = {"user": ["0", "1"]} assert_equal_data(result, expected) -def test_get_field_series( - request: pytest.FixtureRequest, constructor_eager: ConstructorEager -) -> None: +def test_get_field_series(constructor_eager: ConstructorEager) -> None: pytest.importorskip("pyarrow") - import pyarrow as pa - if any(backend in str(constructor_eager) for backend in ("modin",)): - request.applymarker(pytest.mark.xfail) if "pandas" in str(constructor_eager) and PANDAS_VERSION < (2, 2, 0): pytest.skip() - data = {"user": [{"id": "0", "name": "john"}, {"id": "1", "name": "jane"}]} - expected = {"id": ["0", "1"], "name": ["john", "jane"]} - - _expected = expected.copy() - df_native = constructor_eager(data) - - if "pandas" in str(constructor_eager): - import pandas as pd - - df_native = cast("pd.DataFrame", df_native).assign( - user=pd.Series( - data["user"], - dtype=pd.ArrowDtype( - pa.struct([("id", pa.string()), ("name", pa.string())]) - ), - ) - ) - - df = nw.from_native(df_native, eager_only=True) + data = {"id": ["0", "1"], "name": ["john", "jane"]} + expected = data.copy() + df = constructor_eager(data, nw).select(user=nw.struct("id", "name")) result = nw.from_native(df).select( df["user"].struct.field("id"), df["user"].struct.field("name") ) - expected = {"id": ["0", "1"], "name": ["john", "jane"]} - assert_equal_data(result, _expected) + assert_equal_data(result, expected) def test_pandas_object_series() -> None: diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py index c007c78cf8..076917903a 100644 --- a/tests/frame/join_test.py +++ b/tests/frame/join_test.py @@ -16,19 +16,7 @@ ) if TYPE_CHECKING: - from narwhals.typing import IntoDataFrame, IntoLazyFrameT, JoinStrategy - - -def from_native_lazy( - native: IntoLazyFrameT | IntoDataFrame, -) -> nw.LazyFrame[IntoLazyFrameT] | nw.LazyFrame[Any]: - """Every join test [needs to use `.lazy()` for typing]*. - - *Unless both left/right frames are of the same concrete type. - - [needs to use `.lazy()` for typing]: https://github.com/narwhals-dev/narwhals/pull/2944#discussion_r2286264815 - """ - return nw.from_native(native).lazy() + from narwhals.typing import JoinStrategy @pytest.mark.parametrize( @@ -107,8 +95,8 @@ def test_full_join( right_on: None | str | list[str], constructor: Constructor, ) -> None: - df_left = from_native_lazy(constructor(df1)) - df_right = from_native_lazy(constructor(df2)) + df_left = constructor(df1).lazy() + df_right = constructor(df2).lazy() result = df_left.join( df_right, on=on, left_on=left_on, right_on=right_on, how="full" ).sort("id", nulls_last=True) @@ -123,8 +111,8 @@ def test_full_join_duplicate( df1 = {"foo": [1, 2, 3], "val1": [1, 2, 3]} df2 = {"foo": [1, 2, 3], "foo_right": [1, 2, 3]} - df_left = from_native_lazy(constructor(df1)) - df_right = from_native_lazy(constructor(df2)) + df_left = constructor(df1).lazy() + df_right = constructor(df2).lazy() exceptions: list[type[Exception]] = [nw.exceptions.NarwhalsError] if "pyspark" in str(constructor) and "sqlframe" not in str(constructor): @@ -146,7 +134,7 @@ def test_inner_join_two_keys(constructor: Constructor) -> None: "zor ro": [7.0, 8.0, 9.0], "idx": [0, 1, 2], } - df = from_native_lazy(constructor(data)) + df = constructor(data).lazy() df_right = df result = df.join( df_right, @@ -175,7 +163,7 @@ def test_inner_join_single_key(constructor: Constructor) -> None: "zor ro": [7.0, 8.0, 9.0], "idx": [0, 1, 2], } - df = from_native_lazy(constructor(data)) + df = constructor(data).lazy() df_right = df result = df.join( df_right, left_on="antananarivo", right_on="antananarivo", how="inner" @@ -199,7 +187,7 @@ def test_cross_join(constructor: Constructor) -> None: if "duckdb" in str(constructor) and DUCKDB_VERSION < (1, 1, 4): pytest.skip() data = {"antananarivo": [1, 3, 2]} - df = from_native_lazy(constructor(data)) + df = constructor(data).lazy() result = df.join(df, how="cross").sort("antananarivo", "antananarivo_right") expected = { "antananarivo": [1, 1, 1, 2, 2, 2, 3, 3, 3], @@ -219,7 +207,7 @@ def test_suffix( constructor: Constructor, how: Literal["inner", "left"], suffix: str ) -> None: data = {"antananarivo": [1, 3, 2], "bob": [4, 4, 6], "zor ro": [7.0, 8.0, 9.0]} - df = from_native_lazy(constructor(data)) + df = constructor(data).lazy() df_right = df result = df.join( df_right, @@ -237,7 +225,7 @@ def test_cross_join_suffix(constructor: Constructor, suffix: str) -> None: if "duckdb" in str(constructor) and DUCKDB_VERSION < (1, 1, 4): pytest.skip() data = {"antananarivo": [1, 3, 2]} - df = from_native_lazy(constructor(data)) + df = constructor(data).lazy() result = df.join(df, how="cross", suffix=suffix).sort( "antananarivo", f"antananarivo{suffix}" ) @@ -287,7 +275,7 @@ def test_anti_join( expected: dict[str, list[Any]], ) -> None: data = {"antananarivo": [1, 3, 2], "bob": [4, 4, 6], "zor ro": [7.0, 8.0, 9.0]} - df = from_native_lazy(constructor(data)) + df = constructor(data).lazy() other = df.filter(filter_expr) result = df.join(other, how="anti", left_on=join_key, right_on=join_key) assert_equal_data(result, expected) @@ -325,7 +313,7 @@ def test_semi_join( expected: dict[str, list[Any]], ) -> None: data = {"antananarivo": [1, 3, 2], "bob": [4, 4, 6], "zor ro": [7.0, 8.0, 9.0]} - df = from_native_lazy(constructor(data)) + df = constructor(data).lazy() other = df.filter(filter_expr) result = df.join(other, how="semi", left_on=join_key, right_on=join_key).sort( "antananarivo" @@ -336,7 +324,7 @@ def test_semi_join( @pytest.mark.parametrize("how", ["right"]) def test_join_not_implemented(constructor: Constructor, how: str) -> None: data = {"antananarivo": [1, 3, 2], "bob": [4, 4, 6], "zor ro": [7.0, 8.0, 9.0]} - df = from_native_lazy(constructor(data)) + df = constructor(data).lazy() with pytest.raises( NotImplementedError, @@ -363,8 +351,8 @@ def test_left_join(constructor: Constructor) -> None: "co": [4.0, 5.0, 7.0], "idx": [0.0, 1.0, 2.0], } - df_left = from_native_lazy(constructor(data_left)) - df_right = from_native_lazy(constructor(data_right)) + df_left = constructor(data_left).lazy() + df_right = constructor(data_right).lazy() result = df_left.join(df_right, left_on="bob", right_on="co", how="left") result = result.sort("idx") result = result.drop("idx_right") @@ -389,8 +377,8 @@ def test_left_join(constructor: Constructor) -> None: def test_left_join_multiple_column(constructor: Constructor) -> None: data_left = {"antananarivo": [1, 2, 3], "bob": [4, 5, 6], "idx": [0, 1, 2]} data_right = {"antananarivo": [1, 2, 3], "c": [4, 5, 6], "idx": [0, 1, 2]} - df_left = from_native_lazy(constructor(data_left)) - df_right = from_native_lazy(constructor(data_right)) + df_left = constructor(data_left).lazy() + df_right = constructor(data_right).lazy() result = df_left.join( df_right, left_on=["antananarivo", "bob"], @@ -416,8 +404,8 @@ def test_left_join_overlapping_column(constructor: Constructor) -> None: "d": [1.0, 4.0, 2.0], "idx": [0.0, 1.0, 2.0], } - df_left = from_native_lazy(constructor(data_left)) - df_right = from_native_lazy(constructor(data_right)) + df_left = constructor(data_left).lazy() + df_right = constructor(data_right).lazy() result = df_left.join(df_right, left_on="bob", right_on="c", how="left").sort("idx") result = result.drop("idx_right") expected: dict[str, list[Any]] = { @@ -446,7 +434,7 @@ def test_left_join_overlapping_column(constructor: Constructor) -> None: @pytest.mark.parametrize("how", ["inner", "left", "semi", "anti"]) def test_join_keys_exceptions(constructor: Constructor, how: JoinStrategy) -> None: data = {"antananarivo": [1, 3, 2], "bob": [4, 4, 6], "zor ro": [7.0, 8.0, 9.0]} - df = from_native_lazy(constructor(data)) + df = constructor(data).lazy() with pytest.raises( ValueError, @@ -515,8 +503,8 @@ def test_joinasof_numeric( data_left = {"antananarivo": [1, 5, 10], "val": ["a", "b", "c"]} data_right = {"antananarivo": [1, 2, 3, 6, 7], "val": [1, 2, 3, 6, 7]} - left_lf = from_native_lazy(constructor(data_left)).sort("antananarivo") - right_lf = from_native_lazy(constructor(data_right)).sort("antananarivo") + left_lf = constructor(data_left).lazy().sort("antananarivo") + right_lf = constructor(data_right).lazy().sort("antananarivo") result: nw.DataFrame[Any] | nw.LazyFrame[Any] result_on: nw.DataFrame[Any] | nw.LazyFrame[Any] @@ -592,7 +580,7 @@ def test_joinasof_time( request.applymarker(pytest.mark.xfail) if PANDAS_VERSION < (2, 1) and ("pandas_pyarrow" in str(constructor)): request.applymarker(pytest.mark.xfail) - df = from_native_lazy( + df = ( constructor( { "datetime": [ @@ -603,8 +591,10 @@ def test_joinasof_time( "population": [82.19, 82.66, 83.12], } ) - ).sort("datetime") - df_right = from_native_lazy( + .lazy() + .sort("datetime") + ) + df_right = ( constructor( { "datetime": [ @@ -617,7 +607,9 @@ def test_joinasof_time( "gdp": [4164, 4411, 4566, 4696, 4827], } ) - ).sort("datetime") + .lazy() + .sort("datetime") + ) result = df.join_asof( df_right, left_on="datetime", right_on="datetime", strategy=strategy ) @@ -633,7 +625,7 @@ def test_joinasof_by(constructor: Constructor, request: pytest.FixtureRequest) - ("pandas_pyarrow" in str(constructor)) or ("pandas_nullable" in str(constructor)) ): request.applymarker(pytest.mark.xfail) - df = from_native_lazy( + df = ( constructor( { "antananarivo": [1, 5, 7, 10], @@ -641,12 +633,16 @@ def test_joinasof_by(constructor: Constructor, request: pytest.FixtureRequest) - "c": [9, 2, 1, 1], } ) - ).sort("antananarivo") - df_right = from_native_lazy( + .lazy() + .sort("antananarivo") + ) + df_right = ( constructor( {"antananarivo": [1, 4, 5, 8], "bob": ["D", "D", "A", "F"], "d": [1, 3, 4, 1]} ) - ).sort("antananarivo") + .lazy() + .sort("antananarivo") + ) result = df.join_asof(df_right, on="antananarivo", by_left="bob", by_right="bob") result_by = df.join_asof(df_right, on="antananarivo", by="bob") expected = { @@ -668,12 +664,16 @@ def test_joinasof_suffix( ("pandas_pyarrow" in str(constructor)) or ("pandas_nullable" in str(constructor)) ): request.applymarker(pytest.mark.xfail) - df = from_native_lazy( + df = ( constructor({"antananarivo": [1, 5, 10], "val": ["a", "b", "c"]}) - ).sort("antananarivo") - df_right = from_native_lazy( + .lazy() + .sort("antananarivo") + ) + df_right = ( constructor({"antananarivo": [1, 2, 3, 6, 7], "val": [1, 2, 3, 6, 7]}) - ).sort("antananarivo") + .lazy() + .sort("antananarivo") + ) result = df.join_asof( df_right, left_on="antananarivo", right_on="antananarivo", suffix="_y" ) @@ -686,7 +686,7 @@ def test_joinasof_not_implemented( constructor: Constructor, strategy: Literal["backward", "forward"] ) -> None: data = {"antananarivo": [1, 3, 2], "bob": [4, 4, 6], "zor ro": [7.0, 8.0, 9.0]} - df = from_native_lazy(constructor(data)) + df = constructor(data).lazy() with pytest.raises( NotImplementedError, @@ -699,7 +699,7 @@ def test_joinasof_not_implemented( def test_joinasof_keys_exceptions(constructor: Constructor) -> None: data = {"antananarivo": [1, 3, 2], "bob": [4, 4, 6], "zor ro": [7.0, 8.0, 9.0]} - df = from_native_lazy(constructor(data)) + df = constructor(data).lazy() with pytest.raises( ValueError, @@ -765,7 +765,7 @@ def test_joinasof_by_exceptions( message: str, ) -> None: data = {ON: [1, 3, 2], BY: [4, 4, 6], "zor ro": [7.0, 8.0, 9.0]} - frame = from_native_lazy(constructor(data)) + frame = constructor(data).lazy() if constructor.is_lazy: with pytest.raises(ValueError, match=message): @@ -791,7 +791,7 @@ def test_join_duplicate_column_names( ): request.applymarker(pytest.mark.xfail) data = {"a": [1, 2, 3, 4, 5], "b": [6, 6, 6, 6, 6]} - lf = from_native_lazy(constructor(data)) + lf = constructor(data).lazy() if any( x in str(constructor) for x in ("pandas", "pandas[pyarrow]", "pandas[nullable]", "dask") @@ -891,8 +891,8 @@ def test_join_on_null_values( data_left = {**keys, "x": [1, 2, 3, 4]} data_right = {**keys, "y": [1.2, 3.4, 5.6, 7.8]} - df_left = from_native_lazy(constructor(data_left)) - df_right = from_native_lazy(constructor(data_right)) + df_left = constructor(data_left).lazy() + df_right = constructor(data_right).lazy() on = None if how == "cross" else list(keys) sort_by = ["a", "x", "y"] if how in {"cross", "full"} else ["a", "x"] @@ -918,8 +918,8 @@ def test_full_join_with_overlapping_non_key_columns_and_nulls( "right_only": [100, 200, 300], } - df_left = from_native_lazy(constructor(data_left)) - df_right = from_native_lazy(constructor(data_right)) + df_left = constructor(data_left).lazy() + df_right = constructor(data_right).lazy() result = df_left.join(df_right, on="id", how="full", suffix="_r").sort( "id", nulls_last=True diff --git a/tests/frame/sample_test.py b/tests/frame/sample_test.py index b86ddaee1d..8db480e02c 100644 --- a/tests/frame/sample_test.py +++ b/tests/frame/sample_test.py @@ -19,9 +19,7 @@ def test_sample_n(constructor_eager: ConstructorEager) -> None: def test_sample_fraction(constructor_eager: ConstructorEager) -> None: - df = nw.from_native( - constructor_eager({"a": [1, 2, 3, 4], "b": ["x", "y", "x", "y"]}), eager_only=True - ) + df = constructor_eager({"a": [1, 2, 3, 4], "b": ["x", "y", "x", "y"]}) result_expr = df.sample(fraction=0.5).shape expected_expr = (2, 2) @@ -30,11 +28,11 @@ def test_sample_fraction(constructor_eager: ConstructorEager) -> None: def test_sample_with_seed(constructor_eager: ConstructorEager) -> None: size, n = 100, 10 - df = nw.from_native(constructor_eager({"a": range(size)}), eager_only=True) + df = constructor_eager({"a": range(size)}) r1 = nw.to_native(df.sample(n=n, seed=123)) r2 = nw.to_native(df.sample(n=n, seed=123)) r3 = nw.to_native(df.sample(n=n, seed=42)) - assert r1.equals(r2) # type: ignore[attr-defined] - assert not r1.equals(r3) # type: ignore[attr-defined] + assert r1.equals(r2) + assert not r1.equals(r3) diff --git a/tests/frame/schema_test.py b/tests/frame/schema_test.py index b6fe91beb8..3d66c308b7 100644 --- a/tests/frame/schema_test.py +++ b/tests/frame/schema_test.py @@ -9,7 +9,7 @@ import narwhals as nw from narwhals.exceptions import PerformanceWarning -from tests.utils import PANDAS_VERSION, POLARS_VERSION, ConstructorPandasLike +from tests.utils import PANDAS_VERSION, POLARS_VERSION if TYPE_CHECKING: from collections.abc import Callable, Sequence @@ -23,7 +23,7 @@ IntoPandasSchema, IntoPolarsSchema, ) - from tests.utils import Constructor, ConstructorEager + from tests.utils import Constructor, ConstructorEager, ConstructorPandasLike TimeUnit: TypeAlias = Literal["ns", "us"] @@ -578,7 +578,7 @@ def origin_pandas_like( "d": [5.3, 4.99], "e": [datetime(2006, 1, 1), datetime(2001, 9, 3)], } - return constructor_pandas_like(data).dtypes.to_dict() + return constructor_pandas_like(data).to_native().dtypes.to_dict() # type: ignore[no-any-return] @pytest.fixture @@ -603,7 +603,7 @@ def origin_pandas_like_pyarrow( df_nw = nw.from_native(df_pd).with_columns( nw.col("f").cast(nw.Date()), nw.col("g").cast(nw.Time()) ) - return df_nw.to_native().dtypes.to_dict() + return df_nw.to_native().dtypes.to_dict() # type: ignore[no-any-return] def test_schema_from_polars( diff --git a/tests/frame/to_native_test.py b/tests/frame/to_native_test.py index 0ef0ae885a..cdb03e2675 100644 --- a/tests/frame/to_native_test.py +++ b/tests/frame/to_native_test.py @@ -10,7 +10,7 @@ def test_to_native(constructor: Constructor) -> None: data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8.0, 9.0]} - df_raw = constructor(data) + df_raw = constructor(data).to_native() df = nw.from_native(df_raw) assert isinstance(df.to_native(), df_raw.__class__) diff --git a/tests/frame/to_pandas_test.py b/tests/frame/to_pandas_test.py index b74c9a98b1..bcdcc10fc3 100644 --- a/tests/frame/to_pandas_test.py +++ b/tests/frame/to_pandas_test.py @@ -7,7 +7,6 @@ pytest.importorskip("pandas") import pandas as pd -import narwhals as nw from tests.utils import PANDAS_VERSION if TYPE_CHECKING: @@ -19,11 +18,10 @@ def test_convert_pandas(constructor_eager: ConstructorEager) -> None: pytest.importorskip("pyarrow") data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} - df_raw = constructor_eager(data) - result = nw.from_native(df_raw, eager_only=True).to_pandas() + result = constructor_eager(data).to_pandas() if str(constructor_eager).startswith("pandas"): - expected = cast("pd.DataFrame", constructor_eager(data)) + expected = cast("pd.DataFrame", constructor_eager(data).to_native()) elif "modin_pyarrow" in str(constructor_eager): expected = pd.DataFrame(data).convert_dtypes(dtype_backend="pyarrow") else: diff --git a/tests/hypothesis/getitem_test.py b/tests/hypothesis/getitem_test.py index d9fc517f60..c18f860872 100644 --- a/tests/hypothesis/getitem_test.py +++ b/tests/hypothesis/getitem_test.py @@ -158,7 +158,7 @@ def test_getitem( # rows/columns sides. return - df_other = nw.from_native(pandas_or_pyarrow_constructor(TEST_DATA)) + df_other = pandas_or_pyarrow_constructor(TEST_DATA, nw) result_other = df_other[cast("Any", selector)] if isinstance(result_polars, nw.Series): diff --git a/tests/ibis_test.py b/tests/ibis_test.py index 10dce38413..a9a9dc413b 100644 --- a/tests/ibis_test.py +++ b/tests/ibis_test.py @@ -10,5 +10,5 @@ def test_from_native() -> None: ibis_constructor = get_backend_constructor("ibis") if not ibis_constructor.is_available: pytest.skip() - df = nw.from_native(ibis_constructor({"a": [1, 2, 3], "b": [4, 5, 6]})) + df = ibis_constructor({"a": [1, 2, 3], "b": [4, 5, 6]}, nw) assert df.columns == ["a", "b"] diff --git a/tests/namespace_test.py b/tests/namespace_test.py index 34d0d60204..ce95c01e05 100644 --- a/tests/namespace_test.py +++ b/tests/namespace_test.py @@ -72,7 +72,7 @@ def test_namespace_from_backend_name(backend: BackendName) -> None: def test_namespace_from_native_object(constructor: Constructor) -> None: data = {"a": [1, 2, 3], "b": [4, 5, 6]} - frame = constructor(data) + frame = constructor(data, nw).to_native() namespace = Namespace.from_native_object(frame) nw_frame = nw.from_native(frame) assert namespace.implementation == nw_frame.implementation diff --git a/tests/preserve_pandas_like_columns_name_attr_test.py b/tests/preserve_pandas_like_columns_name_attr_test.py index 3127040bee..546b388f67 100644 --- a/tests/preserve_pandas_like_columns_name_attr_test.py +++ b/tests/preserve_pandas_like_columns_name_attr_test.py @@ -1,17 +1,17 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING import pytest import narwhals as nw if TYPE_CHECKING: - import pandas as pd + from tests.utils import Constructor def test_ops_preserve_column_index_name( - constructor: Callable[..., pd.DataFrame], request: pytest.FixtureRequest + constructor: Constructor, request: pytest.FixtureRequest ) -> None: if not any(x in str(constructor) for x in ("pandas", "modin", "cudf", "dask")): pytest.skip( @@ -22,7 +22,7 @@ def test_ops_preserve_column_index_name( request.applymarker(pytest.mark.xfail) data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} - df_native = constructor(data) + df_native = constructor(data).to_native() df_native.columns.name = "foo" df = nw.from_native(df_native) diff --git a/tests/series_only/hist_test.py b/tests/series_only/hist_test.py index 9bf4f26c62..7db42c31bc 100644 --- a/tests/series_only/hist_test.py +++ b/tests/series_only/hist_test.py @@ -11,13 +11,13 @@ import narwhals as nw from narwhals.exceptions import ComputeError -from narwhals.testing.constructors import get_backend_constructor -from tests.utils import POLARS_VERSION, ConstructorEager, assert_equal_data +from tests.utils import POLARS_VERSION, assert_equal_data if TYPE_CHECKING: from collections.abc import Sequence - from narwhals.testing.constructors import EagerName + from narwhals.testing.typing import DataFrameConstructor + rnd = Random(0) # noqa: S311 @@ -49,14 +49,6 @@ param_name = pytest.mark.parametrize("name", ["pandas", "polars[eager]", "pyarrow"]) -def maybe_name_to_constructor(name: EagerName) -> ConstructorEager: - constructor = get_backend_constructor(name) - if constructor.is_available: - return constructor - - pytest.skip() - - SHIFT_BINS_BY = 10 """shift bins property""" @@ -75,18 +67,14 @@ def maybe_name_to_constructor(name: EagerName) -> ConstructorEager: ], ids=str, ) -@param_name def test_hist_bin( - name: EagerName, + nw_dataframe: DataFrameConstructor, bins: list[float], expected: Sequence[float], *, include_breakpoint: bool, ) -> None: - constructor_eager = maybe_name_to_constructor(name) - df = nw.from_native(constructor_eager(data)).with_columns( - float=nw.col("int").cast(nw.Float64) - ) + df = nw_dataframe(data, nw).with_columns(float=nw.col("int").cast(nw.Float64)) expected_full = {"count": expected} if include_breakpoint: expected_full = {"breakpoint": bins[1:], **expected_full} @@ -111,10 +99,8 @@ def test_hist_bin( assert_equal_data(result, expected_full) # missing/nan results - df = nw.from_native( - constructor_eager( - {"has_nan": [float("nan"), *data["int"]], "has_null": [None, *data["int"]]} - ) + df = nw_dataframe( + {"has_nan": [float("nan"), *data["int"]], "has_null": [None, *data["int"]]}, nw ) expected_full = {"count": expected} if include_breakpoint: @@ -126,14 +112,13 @@ def test_hist_bin( @pytest.mark.parametrize("params", counts_and_expected) @param_include_breakpoint -@param_name def test_hist_count( - name: EagerName, *, params: dict[str, Any], include_breakpoint: bool + nw_dataframe: DataFrameConstructor, + *, + params: dict[str, Any], + include_breakpoint: bool, ) -> None: - constructor_eager = maybe_name_to_constructor(name) - df = nw.from_native(constructor_eager(data)).with_columns( - float=nw.col("int").cast(nw.Float64) - ) + df = nw_dataframe(data, nw).with_columns(float=nw.col("int").cast(nw.Float64)) bin_count = params["bin_count"] expected_bins = params["expected_bins"] @@ -153,10 +138,8 @@ def test_hist_count( assert result["count"].sum() == df[col].count() # missing/nan results - df = nw.from_native( - constructor_eager( - {"has_nan": [float("nan"), *data["int"]], "has_null": [None, *data["int"]]} - ) + df = nw_dataframe( + {"has_nan": [float("nan"), *data["int"]], "has_null": [None, *data["int"]]}, nw ) for col in df.columns: @@ -171,11 +154,9 @@ def test_hist_count( ) -@param_name -def test_hist_count_no_spread(name: EagerName) -> None: - constructor_eager = maybe_name_to_constructor(name) +def test_hist_count_no_spread(nw_dataframe: DataFrameConstructor) -> None: data = {"all_zero": [0, 0, 0], "all_non_zero": [5, 5, 5]} - df = nw.from_native(constructor_eager(data)) + df = nw_dataframe(data, nw) result = df["all_zero"].hist(bin_count=4, include_breakpoint=True) expected = {"breakpoint": [-0.25, 0.0, 0.25, 0.5], "count": [0, 3, 0, 0]} @@ -203,12 +184,12 @@ def test_hist_bin_and_bin_count() -> None: @param_include_breakpoint -@param_name -def test_hist_no_data(name: EagerName, *, include_breakpoint: bool) -> None: - constructor_eager = maybe_name_to_constructor(name) - s = nw.from_native(constructor_eager({"values": []})).select( - nw.col("values").cast(nw.Float64) - )["values"] +def test_hist_no_data( + nw_dataframe: DataFrameConstructor, *, include_breakpoint: bool +) -> None: + s = nw_dataframe({"values": []}, nw).select(nw.col("values").cast(nw.Float64))[ + "values" + ] for bin_count in [1, 10]: result = s.hist(bin_count=bin_count, include_breakpoint=include_breakpoint) assert len(result) == bin_count @@ -225,10 +206,8 @@ def test_hist_no_data(name: EagerName, *, include_breakpoint: bool) -> None: assert result["count"].sum() == 0 -@param_name -def test_hist_small_bins(name: EagerName) -> None: - constructor_eager = maybe_name_to_constructor(name) - s = nw.from_native(constructor_eager({"values": [1, 2, 3]})) +def test_hist_small_bins(nw_dataframe: DataFrameConstructor) -> None: + s = nw_dataframe({"values": [1, 2, 3]}, nw) result = s["values"].hist(bins=None, bin_count=None) assert len(result) == 10 @@ -236,11 +215,11 @@ def test_hist_small_bins(name: EagerName) -> None: s["values"].hist(bins=[1, 3], bin_count=4) -def test_hist_non_monotonic(constructor_eager: ConstructorEager) -> None: - if "cudf" in str(constructor_eager): +def test_hist_non_monotonic(nw_dataframe: DataFrameConstructor) -> None: + if "cudf" in str(nw_dataframe): # TODO(unassigned): too many spurious failures, report and revisit return - df = nw.from_native(constructor_eager({"int": [0, 1, 2, 3, 4, 5, 6]})) + df = nw_dataframe({"int": [0, 1, 2, 3, 4, 5, 6]}, nw) with pytest.raises(ComputeError, match="monotonic"): df["int"].hist(bins=[5, 0, 2]) @@ -275,22 +254,17 @@ def test_hist_non_monotonic(constructor_eager: ConstructorEager) -> None: POLARS_VERSION < (1, 27), reason="polars cannot be used for compatibility checks since narwhals aims to mimic polars>=1.27 behavior", ) -@param_name @pytest.mark.filterwarnings("ignore:invalid value encountered in cast:RuntimeWarning") @pytest.mark.slow def test_hist_bin_hypotheis( - name: EagerName, data: list[float], bin_deltas: list[float] + nw_dataframe: DataFrameConstructor, data: list[float], bin_deltas: list[float] ) -> None: - constructor_eager = maybe_name_to_constructor(name) pytest.importorskip("polars") import polars as pl - df = nw.from_native(constructor_eager({"values": data})).select( - nw.col("values").cast(nw.Float64) - ) - df_bins_native = constructor_eager({"bins": bin_deltas}) + df = nw_dataframe({"values": data}, nw).select(nw.col("values").cast(nw.Float64)) bins = ( - nw.from_native(df_bins_native, eager_only=True) + nw_dataframe({"bins": bin_deltas}, nw) .get_column("bins") .cast(nw.Float64) .cum_sum() @@ -317,18 +291,17 @@ def test_hist_bin_hypotheis( reason="polars cannot be used for compatibility checks since narwhals aims to mimic polars>=1.27 behavior", ) @pytest.mark.filterwarnings("ignore:invalid value encountered in cast:RuntimeWarning") -@param_name @pytest.mark.slow def test_hist_count_hypothesis( - name: EagerName, data: list[float], bin_count: int, request: pytest.FixtureRequest + nw_dataframe: DataFrameConstructor, + data: list[float], + bin_count: int, + request: pytest.FixtureRequest, ) -> None: pytest.importorskip("polars") import polars as pl - constructor_eager = maybe_name_to_constructor(name) - df = nw.from_native(constructor_eager({"values": data})).select( - nw.col("values").cast(nw.Float64) - ) + df = nw_dataframe({"values": data}, nw).select(nw.col("values").cast(nw.Float64)) try: result = df["values"].hist(bin_count=bin_count, include_breakpoint=True) @@ -349,9 +322,7 @@ def test_hist_count_hypothesis( if expected[ "count" - ].sum() != expected_data.is_not_nan().sum() and "polars" not in str( - constructor_eager - ): + ].sum() != expected_data.is_not_nan().sum() and "polars" not in str(nw_dataframe): request.applymarker(pytest.mark.xfail) assert_equal_data(result, expected.to_dict(as_series=False)) diff --git a/tests/series_only/to_native_test.py b/tests/series_only/to_native_test.py index 350d81764d..c2a7ad5ecb 100644 --- a/tests/series_only/to_native_test.py +++ b/tests/series_only/to_native_test.py @@ -11,7 +11,7 @@ def test_to_native(constructor_eager: ConstructorEager) -> None: - orig_series = constructor_eager({"a": data})["a"] # type: ignore[index] + orig_series = constructor_eager({"a": data})["a"].to_native() nw_series = nw.from_native(constructor_eager({"a": data}), eager_only=True)["a"] result = nw_series.to_native() assert isinstance(result, orig_series.__class__) diff --git a/tests/testing/assert_frame_equal_test.py b/tests/testing/assert_frame_equal_test.py index 9b0f813b2f..5d2f187267 100644 --- a/tests/testing/assert_frame_equal_test.py +++ b/tests/testing/assert_frame_equal_test.py @@ -12,8 +12,8 @@ from tests.utils import PANDAS_VERSION if TYPE_CHECKING: + from narwhals.testing.typing import Data from narwhals.typing import IntoSchema - from tests.conftest import Data from tests.utils import Constructor, ConstructorEager @@ -24,12 +24,12 @@ def _assertion_error(detail: str) -> pytest.RaisesExc: def test_check_narwhals_objects(constructor: Constructor) -> None: """Test that a type error is raised if the input is not a Narwhals object.""" - frame = constructor({"a": [1, 2, 3]}) + frame = constructor({"a": [1, 2, 3]}).to_native() msg = re.escape( "Expected `narwhals.DataFrame` or `narwhals.LazyFrame` instance, found" ) with pytest.raises(TypeError, match=msg): - assert_frame_equal(frame, frame) # type: ignore[arg-type] + assert_frame_equal(frame, frame) def test_implementation_mismatch() -> None: diff --git a/tests/testing/assert_series_equal_test.py b/tests/testing/assert_series_equal_test.py index c4826c695e..064cc546dd 100644 --- a/tests/testing/assert_series_equal_test.py +++ b/tests/testing/assert_series_equal_test.py @@ -13,8 +13,8 @@ if TYPE_CHECKING: from typing_extensions import TypeAlias + from narwhals.testing.typing import Data from narwhals.typing import IntoSchema, IntoSeriesT - from tests.conftest import Data from tests.utils import ConstructorEager SetupFn: TypeAlias = Callable[[nw.Series[Any]], tuple[nw.Series[Any], nw.Series[Any]]] @@ -406,7 +406,7 @@ def test_categorical_as_str( "left": ["beluga", "dolphin", "narwhal", "orca"], "right": ["unicorn", "orca", "narwhal", "orca"], } - frame = nw.from_native(constructor_eager(data), eager_only=True) + frame = constructor_eager(data, namespace=nw) left = frame["left"].cast(nw.Categorical())[2:] right = frame["right"].cast(nw.Categorical())[2:] diff --git a/tests/testing/conftest.py b/tests/testing/conftest.py index a41d4fdce4..0ff5e9935f 100644 --- a/tests/testing/conftest.py +++ b/tests/testing/conftest.py @@ -8,8 +8,8 @@ import narwhals as nw if TYPE_CHECKING: + from narwhals.testing.typing import Data from narwhals.typing import IntoSchema - from tests.conftest import Data @pytest.fixture(scope="module") diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index af22cee8a1..3520d492da 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -25,7 +25,7 @@ def test_eager_returns_eager_frame() -> None: if not c.is_available: pytest.skip() - df = nw.from_native(c({"x": [1, 2, 3]})) + df = c({"x": [1, 2, 3]}, nw) assert isinstance(df, nw.DataFrame) @@ -34,7 +34,7 @@ def test_lazy_returns_lazy_frame() -> None: if not c.is_available: pytest.skip() - lf = nw.from_native(c({"x": [1, 2, 3]})) + lf = c({"x": [1, 2, 3]}, nw) assert isinstance(lf, nw.LazyFrame) diff --git a/tests/testing/plugin_test.py b/tests/testing/plugin_test.py index 9f7b6e45a8..2c49818047 100644 --- a/tests/testing/plugin_test.py +++ b/tests/testing/plugin_test.py @@ -18,7 +18,7 @@ def test_constructor_eager_fixture_runs_for_each_backend( from narwhals.testing.typing import DataFrameConstructor def test_shape(nw_dataframe: DataFrameConstructor) -> None: - df = nw.from_native(nw_dataframe({"x": [1, 2, 3]}), eager_only=True) + df = nw_dataframe({"x": [1, 2, 3]}, namespace=nw) assert df.shape == (3, 1) """) result = pytester.runpytest_subprocess( @@ -45,7 +45,7 @@ def test_constructor_fixture_includes_lazy_backends(pytester: pytest.Pytester) - from narwhals.testing.typing import FrameConstructor def test_columns(nw_frame: FrameConstructor) -> None: - df = nw.from_native(nw_frame({"x": [1, 2, 3]})) + df = nw_frame({"x": [1, 2, 3]}, namespace=nw) assert df.collect_schema().names() == ["x"] """) result = pytester.runpytest_subprocess( diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 9bebf3967b..d23cbfd39c 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -294,7 +294,7 @@ def test_eager_only_lazy_dask(eager_only: Any, context: Any) -> None: def test_series_only_sqlframe() -> None: # pragma: no cover pytest.importorskip("sqlframe") - df = get_backend_constructor("sqlframe")(data) + df = get_backend_constructor("sqlframe")(data, nw).to_native() with pytest.raises(TypeError, match="Cannot only use `series_only`"): nw.from_native(df, series_only=True) # type: ignore[call-overload] # pyrefly: ignore[no-matching-overload] @@ -315,7 +315,7 @@ def test_series_only_sqlframe() -> None: # pragma: no cover ) def test_eager_only_sqlframe(eager_only: Any, context: Any) -> None: # pragma: no cover pytest.importorskip("sqlframe") - df = get_backend_constructor("sqlframe")(data) + df = get_backend_constructor("sqlframe")(data, nw).to_native() with context: res = nw.from_native(df, eager_only=eager_only) @@ -528,7 +528,7 @@ def test_eager_only_pass_through_main(constructor: Constructor) -> None: if not any(s in str(constructor) for s in ("pyspark", "dask", "ibis", "duckdb")): pytest.skip(reason="Non lazy or polars") - df = constructor(data) + df = constructor(data).to_native() r1 = nw.from_native(df, eager_only=False, pass_through=False) r2 = nw.from_native(df, eager_only=False, pass_through=True) @@ -539,7 +539,7 @@ def test_eager_only_pass_through_main(constructor: Constructor) -> None: assert not isinstance(r3, nw.LazyFrame) with pytest.raises(TypeError, match=r"Cannot.+use.+eager_only"): - nw.from_native(df, eager_only=True, pass_through=False) # type: ignore[type-var] + nw.from_native(df, eager_only=True, pass_through=False) def test_from_native_lazyframe_exhaustive() -> None: # noqa: PLR0914, PLR0915 diff --git a/tests/utils.py b/tests/utils.py index 3281951896..5b53e3a49b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,20 +6,22 @@ import warnings from datetime import date, datetime from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw from narwhals._utils import Implementation, parse_version, zip_strict from narwhals.dependencies import get_pandas -from narwhals.testing.typing import ( - # TODO(FBruzzesi): Remove these aliases once all the test suite migrates to *FrameConstructor's - DataFrameConstructor as ConstructorEager, - FrameConstructor as Constructor, -) from narwhals.translate import from_native +# TODO(FBruzzesi): Replace these aliases once all the test suite migrates to *FrameConstructor's +from tests.conftest import ( + _PatchedDataFrameConstructor as ConstructorEager, + _PatchedDataFrameConstructor as ConstructorPandasLike, + _PatchedFrameConstructor as Constructor, +) + if TYPE_CHECKING: from collections.abc import Mapping, Sequence @@ -31,7 +33,7 @@ # TODO(FBruzzesi): Remove these aliases once all the test suite migrates to *FrameConstructor's # NOTE: Explicitly exported otherwise mypy will raise an [attr-defined] error for each file # importing them from `tests.utils` rather than `narwhals.testing.typing` directly. -__all__ = ("Constructor", "ConstructorEager") +__all__ = ("Constructor", "ConstructorEager", "ConstructorPandasLike") def get_module_version_as_tuple(module_name: str) -> tuple[int, ...]: @@ -51,8 +53,6 @@ def get_module_version_as_tuple(module_name: str) -> tuple[int, ...]: PYSPARK_VERSION: tuple[int, ...] = get_module_version_as_tuple("pyspark") CUDF_VERSION: tuple[int, ...] = get_module_version_as_tuple("cudf") -ConstructorPandasLike: TypeAlias = Callable[[Any], "pd.DataFrame"] - NestedOrEnumDType: TypeAlias = "nw.List | nw.Array | nw.Struct | nw.Enum" """`DType`s which **cannot** be used as bare types.""" diff --git a/tests/v1_test.py b/tests/v1_test.py index 9882c4ed15..8ddb64a118 100644 --- a/tests/v1_test.py +++ b/tests/v1_test.py @@ -318,11 +318,11 @@ def test_cast_to_enum_v1( ): request.applymarker(pytest.mark.xfail) - df_native = constructor({"a": ["a", "b"]}) + df = constructor({"a": ["a", "b"]}, nw_v1) msg = re.escape("Converting to Enum is not supported in narwhals.stable.v1") with pytest.raises(NotImplementedError, match=msg): - nw_v1.from_native(df_native).select(nw_v1.col("a").cast(nw_v1.Enum)) # type: ignore[arg-type] + df.select(nw_v1.col("a").cast(nw_v1.Enum)) # type: ignore[arg-type] def test_v1_ordered_categorical_pandas() -> None: @@ -459,7 +459,7 @@ def test_with_row_index(constructor: Constructor) -> None: pytest.skip() data = {"abc": ["foo", "bars"], "xyz": [100, 200], "const": [42, 42]} - frame = nw_v1.from_native(constructor(data)) + frame = constructor(data, nw_v1) msg = "Cannot pass `order_by`" context = ( @@ -469,7 +469,7 @@ def test_with_row_index(constructor: Constructor) -> None: ) with context: - result = frame.with_row_index() + result = frame.with_row_index() # type: ignore[call-arg] expected = {"index": [0, 1], **data} assert_equal_data(result, expected) @@ -887,7 +887,7 @@ def test_is_frame() -> None: def test_with_version(constructor: Constructor) -> None: - lf = nw_v1.from_native(constructor({"a": [1, 2]})).lazy() + lf = constructor({"a": [1, 2]}, nw_v1).lazy() assert isinstance(lf, nw_v1.LazyFrame) assert lf._compliant_frame._with_version(Version.MAIN)._version is Version.MAIN @@ -896,7 +896,7 @@ def test_with_version(constructor: Constructor) -> None: @pytest.mark.parametrize("offset", [1, 2]) def test_gather_every(constructor_eager: ConstructorEager, n: int, offset: int) -> None: data = {"a": list(range(10))} - df_v1 = nw_v1.from_native(constructor_eager(data)) + df_v1 = constructor_eager(data, nw_v1) result = df_v1.gather_every(n=n, offset=offset) expected = {"a": data["a"][offset::n]} assert_equal_data(result, expected) @@ -1156,7 +1156,7 @@ def test_series_from_iterable( def test_mode_single_expr(constructor_eager: ConstructorEager) -> None: data = {"a": [1, 1, 2, 2, 3], "b": [1, 2, 3, 3, 4]} - df = nw_v1.from_native(constructor_eager(data)) + df = constructor_eager(data, nw_v1) result = df.select(nw_v1.col("a").mode()).sort("a") expected = {"a": [1, 2]} assert_equal_data(result, expected) @@ -1164,7 +1164,7 @@ def test_mode_single_expr(constructor_eager: ConstructorEager) -> None: def test_mode_series(constructor_eager: ConstructorEager) -> None: data = {"a": [1, 1, 2, 2, 3], "b": [1, 2, 3, 3, 4]} - series = nw_v1.from_native(constructor_eager(data), eager_only=True)["a"] + series = constructor_eager(data, nw_v1)["a"] result = series.mode().sort() expected = {"a": [1, 2]} assert_equal_data({"a": result}, expected) @@ -1173,7 +1173,7 @@ def test_mode_series(constructor_eager: ConstructorEager) -> None: def test_mode_different_lengths(constructor_eager: ConstructorEager) -> None: if "polars" in str(constructor_eager) and POLARS_VERSION < (1, 10): pytest.skip() - df = nw_v1.from_native(constructor_eager({"a": [1, 1, 2], "b": [4, 5, 6]})) + df = constructor_eager({"a": [1, 1, 2], "b": [4, 5, 6]}, nw_v1) with pytest.raises(ShapeError): df.select(nw_v1.col("a", "b").mode()) @@ -1196,7 +1196,7 @@ def test_any_value_expr(constructor: Constructor, request: pytest.FixtureRequest "b": [1, 2, 3, 4, 5, 6], "c": [None, None, 1, None, 2, None], } - df = nw_v1.from_native(constructor(data)) + df = constructor(data, nw_v1) with pytest.warns(NarwhalsUnstableWarning): df.select(nw_v1.col("a", "b").any_value()) @@ -1204,7 +1204,7 @@ def test_any_value_expr(constructor: Constructor, request: pytest.FixtureRequest def test_any_value_series(constructor_eager: ConstructorEager) -> None: data = {"a": [1, 1, 1, 2, 2, 3]} - df = nw_v1.from_native(constructor_eager(data)) + df = constructor_eager(data, nw_v1) with pytest.warns(NarwhalsUnstableWarning): df["a"].any_value() diff --git a/tests/v2_test.py b/tests/v2_test.py index 7a1903425c..d33ae97edb 100644 --- a/tests/v2_test.py +++ b/tests/v2_test.py @@ -347,7 +347,7 @@ def fun2(self, df: Any) -> Any: # pragma: no cover def test_with_version(constructor: Constructor) -> None: - lf = nw_v2.from_native(constructor({"a": [1, 2]})).lazy() + lf = constructor({"a": [1, 2]}, nw_v2).lazy() assert isinstance(lf, nw_v2.LazyFrame) assert lf._compliant_frame._with_version(Version.MAIN)._version is Version.MAIN @@ -503,7 +503,7 @@ def test_series_from_iterable( def test_mode_single_expr(constructor_eager: ConstructorEager) -> None: data = {"a": [1, 1, 2, 2, 3], "b": [1, 2, 3, 3, 4]} - df = nw_v2.from_native(constructor_eager(data)) + df = constructor_eager(data, nw_v2) result = df.select(nw_v2.col("a").mode()).sort("a") expected = {"a": [1, 2]} assert_equal_data(result, expected) @@ -511,7 +511,7 @@ def test_mode_single_expr(constructor_eager: ConstructorEager) -> None: def test_mode_series(constructor_eager: ConstructorEager) -> None: data = {"a": [1, 1, 2, 2, 3], "b": [1, 2, 3, 3, 4]} - series = nw_v2.from_native(constructor_eager(data), eager_only=True)["a"] + series = constructor_eager(data, nw_v2)["a"] result = series.mode().sort() expected = {"a": [1, 2]} assert_equal_data({"a": result}, expected) @@ -520,7 +520,7 @@ def test_mode_series(constructor_eager: ConstructorEager) -> None: def test_mode_different_lengths(constructor_eager: ConstructorEager) -> None: if "polars" in str(constructor_eager) and POLARS_VERSION < (1, 10): pytest.skip() - df = nw_v2.from_native(constructor_eager({"a": [1, 1, 2], "b": [4, 5, 6]})) + df = constructor_eager({"a": [1, 1, 2], "b": [4, 5, 6]}, nw_v2) with pytest.raises(ShapeError): df.select(nw_v2.col("a", "b").mode()) @@ -535,7 +535,7 @@ def test_any_value_expr(constructor: Constructor, request: pytest.FixtureRequest "b": [1, 2, 3, 4, 5, 6], "c": [None, None, 1, None, 2, None], } - df = nw_v2.from_native(constructor(data)) + df = constructor(data, nw_v2) with pytest.warns(NarwhalsUnstableWarning): df.select(nw_v2.col("a", "b").any_value()) @@ -543,7 +543,7 @@ def test_any_value_expr(constructor: Constructor, request: pytest.FixtureRequest def test_any_value_series(constructor_eager: ConstructorEager) -> None: data = {"a": [1, 1, 1, 2, 2, 3]} - df = nw_v2.from_native(constructor_eager(data)) + df = constructor_eager(data, nw_v2) with pytest.warns(NarwhalsUnstableWarning): df["a"].any_value() From b6ba91ecc8bbec9816f354cccb2ca1c17f2ae7d3 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 5 May 2026 22:49:45 +0200 Subject: [PATCH 56/71] fixup testing.md --- docs/api-reference/testing.md | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/api-reference/testing.md b/docs/api-reference/testing.md index d4bf928e59..0ee8ec5f36 100644 --- a/docs/api-reference/testing.md +++ b/docs/api-reference/testing.md @@ -14,22 +14,23 @@ ## `pytest` plugin Narwhals register a pytest plugin that exposes parametrized fixtures with callables -to build native frames from a column-oriented python `dict`. +to build Narwhals frames from a column-oriented python `dict`. ### Available fixtures | Fixture | Backends | |---|---| -| `nw_frame_constructor` | every selected backend (eager + lazy) | -| `nw_eager_constructor` | only eager backends | -| `nw_pandas_like_constructor` | pandas-like backends | +| `nw_frame` | every selected backend (eager + lazy) | +| `nw_lazyframe` | only lazy backends | +| `nw_dataframe` | only eager backends | +| `nw_pandas_like_frame` | pandas-like backends | ### Pytest options The backend selection is controlled by the following CLI options: * `--nw-backends=pandas,polars[lazy],duckdb`: comma-separated list. - Defaults to [`DEFAULT_BACKENDS`][narwhals.testing.constructors.DEFAULT_BACKENDS] + Defaults to the following list: `pandas,pandas[pyarrow],polars[eager],pyarrow,duckdb,sqlframe,ibis` intersected with the backends installed in the current environment. * `--nw-all-backends`: shortcut for "every **CPU** backend that is installed". * `--use-nw-external-constructor`: Skip narwhals.testing's parametrisation and let @@ -46,15 +47,22 @@ The plugin auto-loads as soon as you `pip install narwhals`. Just write a test: from typing import TYPE_CHECKING import narwhals as nw +import narwhals.stable.v2 as nw_v2 if TYPE_CHECKING: - from narwhals.testing.typing import EagerFrameConstructor, Data + from narwhals.testing.typing import Data, DataFrameConstructor, LazyFrameConstructor -def test_shape(nw_eager_constructor: EagerFrameConstructor) -> None: +def test_shape(nw_dataframe: DataFrameConstructor) -> None: data: Data = {"x": [1, 2, 3]} - df = nw.from_native(nw_eager_constructor(data), eager_only=True) + df = nw_dataframe(data, namespace=nw) assert df.shape == (3, 1) + + +def test_laziness(nw_lazyframe: LazyFrameConstructor) -> None: + data: Data = {"x": [1, 2, 3]} + lf = nw_lazyframe(data, namespace=nw_v2) + assert isinstance(lf, nw_v2.LazyFrame) ``` The fixtures are parametrised against every supported backend that is installed @@ -75,5 +83,5 @@ pytest --all-nw-backends members: - Data - FrameConstructor - - EagerFrameConstructor + - DataFrameConstructor - LazyFrameConstructor From 73fd0fca18998e04781af4ff958916b0a014b8f3 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 8 May 2026 14:01:40 +0200 Subject: [PATCH 57/71] fixup _select_backends --- narwhals/testing/pytest_plugin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/narwhals/testing/pytest_plugin.py b/narwhals/testing/pytest_plugin.py index 28e9701742..bf1204445b 100644 --- a/narwhals/testing/pytest_plugin.py +++ b/narwhals/testing/pytest_plugin.py @@ -88,11 +88,17 @@ def pytest_addoption(parser: pytest.Parser) -> None: def _select_backends(config: pytest.Config) -> list[FrameConstructor]: # pragma: no cover from narwhals.testing.constructors import available_cpu_backends, prepare_backends - _all_cpu_exclusions = frozenset({"modin", "pyspark[connect]"}) + _available_cpu_backends = available_cpu_backends() + + # Intersection is needed otherwise prepare_backends raises an exception if + # modin or pyspark are not installed + _all_cpu_exclusions = frozenset({"modin", "pyspark[connect]"}).intersection( + _available_cpu_backends + ) if config.getoption("all_nw_backends"): selected = prepare_backends( - include=available_cpu_backends(), exclude=_all_cpu_exclusions + include=_available_cpu_backends, exclude=_all_cpu_exclusions ) else: opt = cast("str", config.getoption("nw_backends")) From 141a537cefb793676886a9bb8a9e6ea8320c4600 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 8 May 2026 14:42:55 +0200 Subject: [PATCH 58/71] modules as implementation of protocols --- docs/api-reference/testing.md | 1 + narwhals/testing/constructors.py | 11 +++++------ narwhals/testing/typing.py | 8 +++++++- tests/conftest.py | 12 ++++++++---- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/api-reference/testing.md b/docs/api-reference/testing.md index 0ee8ec5f36..2cc088c941 100644 --- a/docs/api-reference/testing.md +++ b/docs/api-reference/testing.md @@ -85,3 +85,4 @@ pytest --all-nw-backends - FrameConstructor - DataFrameConstructor - LazyFrameConstructor + - NarwhalsNamespace diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index 1ea6737d29..25759521c7 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -49,7 +49,6 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: if TYPE_CHECKING: from collections.abc import Iterable - from types import ModuleType import ibis import pandas as pd @@ -62,7 +61,7 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: from narwhals import DataFrame, LazyFrame from narwhals._native import NativeDask, NativeDuckDB, NativePySpark, NativeSQLFrame - from narwhals.testing.typing import Data + from narwhals.testing.typing import Data, NarwhalsNamespace from narwhals.typing import ( IntoDataFrame, IntoDataFrameT, @@ -174,7 +173,7 @@ def __call__( self: frame_constructor[IntoDataFrameT], obj: Data, /, - namespace: ModuleType, + namespace: NarwhalsNamespace, **kwds: Any, ) -> DataFrame[IntoDataFrameT]: ... @overload @@ -182,7 +181,7 @@ def __call__( self: frame_constructor[IntoLazyFrameT], obj: Data, /, - namespace: ModuleType, + namespace: NarwhalsNamespace, **kwds: Any, ) -> LazyFrame[IntoLazyFrameT]: ... @overload @@ -190,12 +189,12 @@ def __call__( self: frame_constructor[IntoFrame], obj: Data, /, - namespace: ModuleType, + namespace: NarwhalsNamespace, **kwds: Any, ) -> DataFrame[Any] | LazyFrame[Any]: ... def __call__( - self, obj: Data, /, namespace: ModuleType, **kwds: Any + self, obj: Data, /, namespace: NarwhalsNamespace, **kwds: Any ) -> DataFrame[Any] | LazyFrame[Any]: """Build a native frame and wrap it with `namespace.from_native`. diff --git a/narwhals/testing/typing.py b/narwhals/testing/typing.py index f03e946887..0c55689061 100644 --- a/narwhals/testing/typing.py +++ b/narwhals/testing/typing.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Callable, Protocol if TYPE_CHECKING: from typing_extensions import TypeAlias @@ -22,3 +22,9 @@ Data: TypeAlias = dict[str, Any] # TODO(Unassined): This should have a better annotation """A column-oriented mapping used as input to a frame constructor.""" + + +class NarwhalsNamespace(Protocol): + """Minimal specs of a narwhals namespace (e.g. `narwhals`, `narwhals.stable.v1`).""" + + from_native: Callable[..., Any] diff --git a/tests/conftest.py b/tests/conftest.py index ed94521264..8916a762a4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,12 +15,16 @@ if TYPE_CHECKING: from collections.abc import Sequence - from types import ModuleType from narwhals._typing import EagerAllowed from narwhals.dataframe import DataFrame, LazyFrame from narwhals.testing.constructors import frame_constructor - from narwhals.testing.typing import Data, DataFrameConstructor, FrameConstructor + from narwhals.testing.typing import ( + Data, + DataFrameConstructor, + FrameConstructor, + NarwhalsNamespace, + ) from narwhals.typing import IntoFrame, NonNestedDType from tests.utils import NestedOrEnumDType @@ -142,7 +146,7 @@ def __init__(self, inner: frame_constructor[IntoFrame]) -> None: self._inner = inner def __call__( - self, obj: Data, /, namespace: ModuleType = nw, **kwds: Any + self, obj: Data, /, namespace: NarwhalsNamespace = nw, **kwds: Any ) -> DataFrame[Any] | LazyFrame[Any]: return self._inner(obj, namespace=namespace, **kwds) @@ -158,7 +162,7 @@ def __repr__(self) -> str: class _PatchedDataFrameConstructor(_PatchedFrameConstructor): def __call__( - self, obj: Data, /, namespace: ModuleType = nw, **kwds: Any + self, obj: Data, /, namespace: NarwhalsNamespace = nw, **kwds: Any ) -> DataFrame[Any]: return cast("DataFrame[Any]", self._inner(obj, namespace=namespace, **kwds)) From 5f57dd1ef521e3c39d52501024cdde3f549da5e1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 15:00:59 +0000 Subject: [PATCH 59/71] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- narwhals/testing/constructors.py | 16 +++------------- narwhals/testing/typing.py | 5 +++-- tests/testing/constructors_test.py | 2 +- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index 25759521c7..d958384e52 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -33,22 +33,13 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: from copy import deepcopy from functools import lru_cache from importlib.util import find_spec -from typing import ( - TYPE_CHECKING, - Any, - Callable, - ClassVar, - Generic, - Literal, - TypeVar, - cast, - overload, -) +from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeVar, cast, overload from narwhals._utils import Implementation, generate_temporary_column_name if TYPE_CHECKING: - from collections.abc import Iterable + from collections.abc import Callable, Iterable + from typing import Concatenate, TypeAlias import ibis import pandas as pd @@ -57,7 +48,6 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: from ibis.backends.duckdb import Backend as IbisDuckDBBackend from pyspark.sql import SparkSession from sqlframe.duckdb import DuckDBSession - from typing_extensions import Concatenate, TypeAlias from narwhals import DataFrame, LazyFrame from narwhals._native import NativeDask, NativeDuckDB, NativePySpark, NativeSQLFrame diff --git a/narwhals/testing/typing.py b/narwhals/testing/typing.py index 0c55689061..a5d5144bd7 100644 --- a/narwhals/testing/typing.py +++ b/narwhals/testing/typing.py @@ -1,9 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Protocol +from typing import TYPE_CHECKING, Any, Protocol if TYPE_CHECKING: - from typing_extensions import TypeAlias + from collections.abc import Callable + from typing import TypeAlias from narwhals.testing.constructors import frame_constructor from narwhals.typing import IntoDataFrame, IntoFrame, IntoLazyFrame diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index 3520d492da..ac7530436a 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -13,7 +13,7 @@ ) if TYPE_CHECKING: - from typing_extensions import TypeAlias + from typing import TypeAlias PropertyName: TypeAlias = str TrueNames: TypeAlias = set[str] From 6c7f6256a9967079fde6c8cb78ba3328ccdc3de2 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 9 May 2026 17:06:16 +0200 Subject: [PATCH 60/71] fix up formatting and linting --- narwhals/testing/constructors.py | 24 +++++++++--------------- narwhals/testing/typing.py | 5 +++-- tests/testing/constructors_test.py | 2 +- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/narwhals/testing/constructors.py b/narwhals/testing/constructors.py index 25759521c7..7f5a06b1ab 100644 --- a/narwhals/testing/constructors.py +++ b/narwhals/testing/constructors.py @@ -33,22 +33,13 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: from copy import deepcopy from functools import lru_cache from importlib.util import find_spec -from typing import ( - TYPE_CHECKING, - Any, - Callable, - ClassVar, - Generic, - Literal, - TypeVar, - cast, - overload, -) +from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeVar, cast, overload from narwhals._utils import Implementation, generate_temporary_column_name if TYPE_CHECKING: - from collections.abc import Iterable + from collections.abc import Callable, Iterable + from typing import Concatenate, TypeAlias import ibis import pandas as pd @@ -57,7 +48,6 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: from ibis.backends.duckdb import Backend as IbisDuckDBBackend from pyspark.sql import SparkSession from sqlframe.duckdb import DuckDBSession - from typing_extensions import Concatenate, TypeAlias from narwhals import DataFrame, LazyFrame from narwhals._native import NativeDask, NativeDuckDB, NativePySpark, NativeSQLFrame @@ -457,7 +447,9 @@ def _pyspark_build(obj: Data, /, **kwds: Any) -> NativePySpark: # pragma: no co index_col_name = generate_temporary_column_name(n_bytes=8, columns=list(_obj)) _obj[index_col_name] = list(range(len(_obj[next(iter(_obj))]))) result = ( - session.createDataFrame([*zip(*_obj.values())], schema=[*_obj.keys()], **kwds) + session.createDataFrame( + [*zip(*_obj.values(), strict=True)], schema=[*_obj.keys()], **kwds + ) .repartition(2) .orderBy(index_col_name) .drop(index_col_name) @@ -492,7 +484,9 @@ def pyspark_connect_lazy_constructor( ) def sqlframe_pyspark_lazy_constructor(obj: Data, /, **kwds: Any) -> NativeSQLFrame: session = sqlframe_session() - return session.createDataFrame([*zip(*obj.values())], schema=[*obj.keys()], **kwds) + return session.createDataFrame( + [*zip(*obj.values(), strict=True)], schema=[*obj.keys()], **kwds + ) @frame_constructor.register( diff --git a/narwhals/testing/typing.py b/narwhals/testing/typing.py index 0c55689061..a5d5144bd7 100644 --- a/narwhals/testing/typing.py +++ b/narwhals/testing/typing.py @@ -1,9 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Protocol +from typing import TYPE_CHECKING, Any, Protocol if TYPE_CHECKING: - from typing_extensions import TypeAlias + from collections.abc import Callable + from typing import TypeAlias from narwhals.testing.constructors import frame_constructor from narwhals.typing import IntoDataFrame, IntoFrame, IntoLazyFrame diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index 3520d492da..ac7530436a 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -13,7 +13,7 @@ ) if TYPE_CHECKING: - from typing_extensions import TypeAlias + from typing import TypeAlias PropertyName: TypeAlias = str TrueNames: TypeAlias = set[str] From fc34512054ef62cf5f44c250b4e64121e8df6139 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Mon, 1 Jun 2026 14:01:13 +0200 Subject: [PATCH 61/71] convert to native --- tests/dependencies/is_into_dataframe_test.py | 2 +- tests/dependencies/is_into_lazyframe_test.py | 2 +- tests/dependencies/is_into_series_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/dependencies/is_into_dataframe_test.py b/tests/dependencies/is_into_dataframe_test.py index 4e31b71afd..95544d593c 100644 --- a/tests/dependencies/is_into_dataframe_test.py +++ b/tests/dependencies/is_into_dataframe_test.py @@ -42,7 +42,7 @@ def join(self, *args: Any, **kwargs: Any) -> Any: ... def test_is_into_dataframe(constructor: Constructor) -> None: - native_frame = constructor(data) + native_frame = constructor(data).to_native() nw_frame = nw.from_native(native_frame) nw_v1_frame = nw_v1.from_native(native_frame) nw_v2_frame = nw_v2.from_native(native_frame) diff --git a/tests/dependencies/is_into_lazyframe_test.py b/tests/dependencies/is_into_lazyframe_test.py index b9cabdea83..b0edfd5ab5 100644 --- a/tests/dependencies/is_into_lazyframe_test.py +++ b/tests/dependencies/is_into_lazyframe_test.py @@ -40,7 +40,7 @@ def join(self, *args: Any, **kwargs: Any) -> Any: ... def test_is_into_lazyframe(constructor: Constructor) -> None: - native_frame = constructor(data) + native_frame = constructor(data).to_native() nw_frame = nw.from_native(native_frame) nw_v1_frame = nw_v1.from_native(native_frame) nw_v2_frame = nw_v2.from_native(native_frame) diff --git a/tests/dependencies/is_into_series_test.py b/tests/dependencies/is_into_series_test.py index 06dc1fb8b1..4a8b68d34b 100644 --- a/tests/dependencies/is_into_series_test.py +++ b/tests/dependencies/is_into_series_test.py @@ -38,7 +38,7 @@ def unique(self, *args: Any, **kwargs: Any) -> Any: ... def test_is_into_series(constructor_eager: ConstructorEager) -> None: - native_frame = constructor_eager(data) + native_frame = constructor_eager(data).to_native() nw_series = nw.from_native(native_frame)["a"] nw_v1_series = nw_v1.from_native(native_frame)["a"] nw_v2_series = nw_v2.from_native(native_frame)["a"] From ef90f4494db1776033d6da9129d3a2790a9f0290 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Mon, 1 Jun 2026 14:29:31 +0200 Subject: [PATCH 62/71] add cov_source flag in make command --- .github/workflows/pytest-pyspark.yml | 6 +++--- .github/workflows/pytest.yml | 2 -- Makefile | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pytest-pyspark.yml b/.github/workflows/pytest-pyspark.yml index 109a2e0e7c..4674180a1d 100644 --- a/.github/workflows/pytest-pyspark.yml +++ b/.github/workflows/pytest-pyspark.yml @@ -40,7 +40,7 @@ jobs: cache-suffix: pyspark-${{ matrix.python-version }} cache-dependency-glob: "pyproject.toml" - name: Run pytest - run: make run-ci-coverage DEPS="--group core-tests --extra pyspark --extra pandas" COV_RUN_ARGS="--source=src/narwhals/_spark_like" FAIL_UNDER="95" CMD="pytest tests --runslow --nw-backends=pyspark" + run: make run-ci-coverage DEPS="--group core-tests --extra pyspark --extra pandas" COV_SOURCE="src/narwhals/_spark_like" FAIL_UNDER="95" CMD="pytest tests --runslow --nw-backends=pyspark" pytest-pyspark-min-version-constructor: if: github.head_ref != 'bump-version' @@ -59,7 +59,7 @@ jobs: cache-suffix: pyspark-min-${{ matrix.python-version }} cache-dependency-glob: "pyproject.toml" - name: Run pytest - run: make run-ci-coverage DEPS="--group min-pyspark-version --extra pandas" COV_RUN_ARGS="--source=src/narwhals/_spark_like" FAIL_UNDER="95" CMD="pytest tests --runslow --nw-backends=pyspark" + run: make run-ci-coverage DEPS="--group min-pyspark-version --extra pandas" COV_SOURCE="src/narwhals/_spark_like" FAIL_UNDER="95" CMD="pytest tests --runslow --nw-backends=pyspark" pytest-pyspark-connect-constructor: if: github.head_ref != 'bump-version' @@ -130,7 +130,7 @@ jobs: run: | uv run --no-sync coverage run --source=src/narwhals/_spark_like -m pytest tests --runslow --nw-backends "pyspark[connect]" uv run --no-sync coverage combine - uv run --no-sync coverage report --fail-under=95 + uv run --no-sync coverage report --include="src/narwhals/_spark_like/*" --fail-under=95 - name: Stop Spark Connect server if: always() && env.SPARK_HOME != '' diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index de4f05e917..a8b46c5d08 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -39,8 +39,6 @@ jobs: COVERAGE_PATCH_EXECV: ${{ matrix.os == 'windows-latest' && 'subprocess' || 'execv' }} COVERAGE_PATCH_FORK: ${{ matrix.os == 'windows-latest' && 'subprocess' || 'fork' }} run: make run-ci-coverage DEPS="--group tests --extra pandas --extra polars --extra pyarrow" FAIL_UNDER="75" CMD="pytest tests --nw-backends=pandas,pyarrow,polars[eager],polars[lazy]" - - name: install-test-plugin - run: uv pip install -e test-plugin/. pytest-windows: if: github.head_ref != 'bump-version' diff --git a/Makefile b/Makefile index 74130b3f76..7235e0d352 100644 --- a/Makefile +++ b/Makefile @@ -36,8 +36,8 @@ run-ci: ## Print resolved deps, then run a command via uv (no coverage; used by uv run $(DEPS) $(RUN_ONLY) $(CMD) .PHONY: run-ci-coverage -run-ci-coverage: ## Like run-ci but under coverage (run -> combine -> report). Usage: make run-ci-coverage DEPS="" CMD="" [FAIL_UNDER=""] [COV_RUN_ARGS=""] [RUN_ONLY=""] +run-ci-coverage: ## Like run-ci but under coverage (run -> combine -> report). Usage: make run-ci-coverage DEPS="" CMD="" [FAIL_UNDER=""] [COV_SOURCE=""] [RUN_ONLY=""] uv export --no-annotate --no-hashes $(DEPS) - uv run $(DEPS) $(RUN_ONLY) coverage run $(COV_RUN_ARGS) -m $(CMD) + uv run $(DEPS) $(RUN_ONLY) coverage run $(if $(COV_SOURCE),--source=$(COV_SOURCE)) -m $(CMD) uv run $(DEPS) $(RUN_ONLY) coverage combine - uv run $(DEPS) $(RUN_ONLY) coverage report $(if $(FAIL_UNDER),--fail-under=$(FAIL_UNDER)) + uv run $(DEPS) $(RUN_ONLY) coverage report $(if $(COV_SOURCE),--include=$(COV_SOURCE)/*) $(if $(FAIL_UNDER),--fail-under=$(FAIL_UNDER)) From 13c6dde40c71f7b4ec49612d58fa2c4b8e2f9d78 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 3 Jun 2026 13:16:30 +0200 Subject: [PATCH 63/71] constructors -> nw-backends --- .github/workflows/test_sdist.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_sdist.yml b/.github/workflows/test_sdist.yml index d0060c8d9a..74b381e963 100644 --- a/.github/workflows/test_sdist.yml +++ b/.github/workflows/test_sdist.yml @@ -65,4 +65,4 @@ jobs: src=$(echo "${RUNNER_TEMP}/sdist"/narwhals-*) cd "${src}" "${RUNNER_TEMP}/venv/bin/python" -m pytest tests \ - --constructors="pandas,pyarrow,polars[eager],polars[lazy]" + --nw-backends="pandas,pyarrow,polars[eager],polars[lazy]" From d52403d4041062bf6699c4131df5f0ea47ae45e5 Mon Sep 17 00:00:00 2001 From: Edoardo Abati <29585319+EdAbati@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:55:33 +0200 Subject: [PATCH 64/71] add available_default_cpu_backends (#3673) --- src/narwhals/testing/constructors.py | 29 +++++++++++++++++++++++++++ src/narwhals/testing/pytest_plugin.py | 15 ++++---------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/narwhals/testing/constructors.py b/src/narwhals/testing/constructors.py index 7f5a06b1ab..847600f977 100644 --- a/src/narwhals/testing/constructors.py +++ b/src/narwhals/testing/constructors.py @@ -64,6 +64,7 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: __all__ = ( "available_backends", "available_cpu_backends", + "available_default_cpu_backends", "frame_constructor", "get_backend_constructor", "is_backend_available", @@ -107,6 +108,7 @@ def __init__( is_eager: bool = False, is_nullable: bool = True, needs_gpu: bool = False, + default_include: bool = True, ) -> None: self.func = func self.name = name @@ -115,6 +117,7 @@ def __init__( self.is_eager = is_eager self.is_nullable = is_nullable self.needs_gpu = needs_gpu + self.default_include = default_include @classmethod def register( @@ -126,6 +129,7 @@ def register( is_eager: bool = False, is_nullable: bool = True, needs_gpu: bool = False, + default_include: bool = True, ) -> Callable[[Callable[Concatenate[Data, ...], R]], frame_constructor[R]]: """Decorator: register `func` as the constructor named `name`. @@ -137,6 +141,9 @@ def register( is_eager: Whether the backend returns an eager dataframe. is_nullable: Whether the backend has native null support. needs_gpu: Whether the backend requires GPU hardware. + default_include: Whether this backend is included by default when running + `--all-nw-backends`. Set to `False` for backends that require + explicit opt-in (e.g. modin, pyspark[connect]). Returns: A decorator that replaces `func` with a `frame_constructor` @@ -152,6 +159,7 @@ def decorator(func: Callable[Concatenate[Data, ...], R]) -> frame_constructor[R] is_eager=is_eager, is_nullable=is_nullable, needs_gpu=needs_gpu, + default_include=default_include, ) cls._registry[name] = inst return inst @@ -352,6 +360,7 @@ def pyarrow_table_constructor(obj: Data, /, **kwds: Any) -> pa.Table: requirements=("modin",), is_eager=True, is_nullable=False, + default_include=False, ) def modin_constructor(obj: Data, /, **kwds: Any) -> IntoDataFrame: # pragma: no cover import modin.pandas as mpd @@ -365,6 +374,7 @@ def modin_constructor(obj: Data, /, **kwds: Any) -> IntoDataFrame: # pragma: no implementation=Implementation.MODIN, requirements=("modin", "pyarrow"), is_eager=True, + default_include=False, ) def modin_pyarrow_constructor( obj: Data, /, **kwds: Any @@ -470,6 +480,7 @@ def pyspark_lazy_constructor( name="pyspark[connect]", implementation=Implementation.PYSPARK_CONNECT, requirements=("pyspark",), + default_include=False, ) def pyspark_connect_lazy_constructor( obj: Data, /, **kwds: Any @@ -546,6 +557,24 @@ def available_cpu_backends() -> frozenset[str]: # pragma: no cover ) +def available_default_cpu_backends() -> frozenset[str]: # pragma: no cover + """Return the names of every CPU constructor that is available and included by default. + + Backends registered with `default_include=False` (e.g. modin, pyspark[connect]) are + excluded — they must be requested explicitly via `--nw-backends`. + + Examples: + >>> from narwhals.testing.constructors import available_default_cpu_backends + >>> "pandas" in available_default_cpu_backends() + True + """ + return frozenset( + name + for name, c in frame_constructor._registry.items() + if c.is_available and not c.needs_gpu and c.default_include + ) + + EagerName: TypeAlias = Literal[ "pandas", "pandas[nullable]", diff --git a/src/narwhals/testing/pytest_plugin.py b/src/narwhals/testing/pytest_plugin.py index bf1204445b..981e1e4016 100644 --- a/src/narwhals/testing/pytest_plugin.py +++ b/src/narwhals/testing/pytest_plugin.py @@ -86,20 +86,13 @@ def pytest_addoption(parser: pytest.Parser) -> None: def _select_backends(config: pytest.Config) -> list[FrameConstructor]: # pragma: no cover - from narwhals.testing.constructors import available_cpu_backends, prepare_backends - - _available_cpu_backends = available_cpu_backends() - - # Intersection is needed otherwise prepare_backends raises an exception if - # modin or pyspark are not installed - _all_cpu_exclusions = frozenset({"modin", "pyspark[connect]"}).intersection( - _available_cpu_backends + from narwhals.testing.constructors import ( + available_default_cpu_backends, + prepare_backends, ) if config.getoption("all_nw_backends"): - selected = prepare_backends( - include=_available_cpu_backends, exclude=_all_cpu_exclusions - ) + selected = prepare_backends(include=available_default_cpu_backends()) else: opt = cast("str", config.getoption("nw_backends")) names = [c for c in opt.split(",") if c] From 62ba0074b6bb8f6413e8e7c40440fb3350d6a1c9 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 4 Jun 2026 15:36:10 +0200 Subject: [PATCH 65/71] Refactor pytest_addoption with Edo suggestion --- src/narwhals/testing/pytest_plugin.py | 44 ++++++++++++--------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/narwhals/testing/pytest_plugin.py b/src/narwhals/testing/pytest_plugin.py index 981e1e4016..7c0a02e600 100644 --- a/src/narwhals/testing/pytest_plugin.py +++ b/src/narwhals/testing/pytest_plugin.py @@ -14,6 +14,8 @@ from typing import TYPE_CHECKING, cast if TYPE_CHECKING: + from collections.abc import Callable + import pytest from narwhals.testing.typing import FrameConstructor @@ -40,7 +42,7 @@ def _default_backend_ids() -> list[str]: Honours `NARWHALS_DEFAULT_BACKENDS` if set, otherwise restricts [`DEFAULT_BACKENDS`][] to backends whose libraries are importable. """ - if env := os.environ.get("NARWHALS_DEFAULT_BACKENDS"): # pragma: no cover + if env := os.environ.get("NARWHALS_DEFAULT_BACKENDS"): return env.split(",") from narwhals.testing.constructors import DEFAULT_BACKENDS, frame_constructor @@ -108,30 +110,22 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: if metafunc.config.getoption("use_external_nw_backend"): # pragma: no cover return + fixture_filters: dict[str, Callable[[FrameConstructor], bool]] = { + "nw_dataframe": lambda c: c.is_eager, + "nw_lazyframe": lambda c: not c.is_eager, + "nw_frame": lambda _: True, + "nw_pandas_like_frame": lambda c: c.is_eager and c.is_pandas_like, + } fixturenames = set(metafunc.fixturenames) - if not fixturenames & { - "nw_frame", - "nw_dataframe", - "nw_lazyframe", - "nw_pandas_like_frame", - }: + if not (matched_fixtures := fixturenames & fixture_filters.keys()): return - + if len(matched_fixtures) > 1: + msg = ( + f"A test may only request one narwhals frame fixture, got: {matched_fixtures}" + ) + raise ValueError(msg) selected = _select_backends(metafunc.config) - - if "nw_dataframe" in fixturenames: - params = [c for c in selected if c.is_eager] - ids = [c.name for c in params] - metafunc.parametrize("nw_dataframe", params, ids=ids) - elif "nw_lazyframe" in fixturenames: # pragma: no cover - params = [c for c in selected if not c.is_eager] - ids = [c.name for c in params] - metafunc.parametrize("nw_dataframe", params, ids=ids) - elif "nw_frame" in fixturenames: - metafunc.parametrize("nw_frame", selected, ids=[c.name for c in selected]) - elif "nw_pandas_like_frame" in fixturenames: - params = [c for c in selected if c.is_eager and c.is_pandas_like] - ids = [c.name for c in params] - metafunc.parametrize("nw_pandas_like_frame", params, ids=ids) - else: # pragma: no cover - ... + fixture_name = next(iter(matched_fixtures)) + filter_fn = fixture_filters[fixture_name] + params = [c for c in selected if filter_fn(c)] + metafunc.parametrize(fixture_name, params, ids=[c.name for c in params]) From 1db05c9f956b25e1374057f26b95c08b87d93a9c Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 4 Jun 2026 15:36:28 +0200 Subject: [PATCH 66/71] add test for _default_backend_ids NARWHALS_DEFAULT_BACKENDS path --- tests/testing/plugin_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/testing/plugin_test.py b/tests/testing/plugin_test.py index 2c49818047..40549e23cd 100644 --- a/tests/testing/plugin_test.py +++ b/tests/testing/plugin_test.py @@ -1,7 +1,11 @@ from __future__ import annotations +from unittest import mock + import pytest +from narwhals.testing.pytest_plugin import _default_backend_ids + pytest_plugins = ["pytester"] @@ -65,3 +69,10 @@ def test_unparam(nw_dataframe: DataFrameConstructor) -> None: result = pytester.runpytest_subprocess("--use-external-nw-backend") # Without external parametrisation in place, the fixture is missing. result.assert_outcomes(errors=1) + + +def test_default_backends_env_var() -> None: + with mock.patch.dict( + "os.environ", {"NARWHALS_DEFAULT_BACKENDS": "pandas,polars[eager]"} + ): + assert _default_backend_ids() == ["pandas", "polars[eager]"] From e18ca1cd75d008e75efcfe92a947d1979296709a Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 4 Jun 2026 15:47:06 +0200 Subject: [PATCH 67/71] add test for multiple narwhals fixtures at once --- tests/testing/plugin_test.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/testing/plugin_test.py b/tests/testing/plugin_test.py index 40549e23cd..e8b4ba5ff6 100644 --- a/tests/testing/plugin_test.py +++ b/tests/testing/plugin_test.py @@ -71,6 +71,23 @@ def test_unparam(nw_dataframe: DataFrameConstructor) -> None: result.assert_outcomes(errors=1) +def test_multiple_frame_fixtures_raises(pytester: pytest.Pytester) -> None: + pytester.makeconftest("") + pytester.makepyfile(""" + from narwhals.testing.typing import DataFrameConstructor, LazyFrameConstructor + + def test_too_many( + nw_dataframe: DataFrameConstructor, nw_lazyframe: LazyFrameConstructor + ) -> None: + pass + """) + result = pytester.runpytest_subprocess() + result.assert_outcomes(errors=1) + result.stdout.fnmatch_lines( + ["*A test may only request one narwhals frame fixture, got:*"] + ) + + def test_default_backends_env_var() -> None: with mock.patch.dict( "os.environ", {"NARWHALS_DEFAULT_BACKENDS": "pandas,polars[eager]"} From 91a0fd498fa5aad413aae6d4e0ef26bf4915291f Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 4 Jun 2026 16:07:56 +0200 Subject: [PATCH 68/71] rollback typing changes... as I cannot recall their reason --- src/narwhals/stable/v1/typing.py | 11 ++--------- src/narwhals/testing/asserts/frame.py | 19 ++++++++----------- tests/testing/assert_frame_equal_test.py | 2 +- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/narwhals/stable/v1/typing.py b/src/narwhals/stable/v1/typing.py index bc224e1879..5f4a1daff5 100644 --- a/src/narwhals/stable/v1/typing.py +++ b/src/narwhals/stable/v1/typing.py @@ -8,12 +8,7 @@ if TYPE_CHECKING: from typing import TypeAlias - from narwhals._native import ( - NativeDataFrame, - NativeDuckDB, - NativeIbis, - NativeLazyFrame, - ) + from narwhals._native import NativeDataFrame, NativeDuckDB, NativeLazyFrame from narwhals.stable.v1 import DataFrame, Expr, LazyFrame, Series class DataFrameLike(Protocol): @@ -30,9 +25,7 @@ def __dataframe__(self, *args: Any, **kwargs: Any) -> Any: ... `nw.Expr`, e.g. `df.select('a')`. """ -IntoDataFrame: TypeAlias = Union[ - "NativeDataFrame", "DataFrameLike", "NativeDuckDB", "NativeIbis" -] +IntoDataFrame: TypeAlias = Union["NativeDataFrame", "DataFrameLike", "NativeDuckDB"] """Anything which can be converted to a Narwhals DataFrame. Use this if your function accepts a narwhalifiable object but doesn't care about its backend. diff --git a/src/narwhals/testing/asserts/frame.py b/src/narwhals/testing/asserts/frame.py index 9386abad13..64eec42abc 100644 --- a/src/narwhals/testing/asserts/frame.py +++ b/src/narwhals/testing/asserts/frame.py @@ -13,6 +13,7 @@ if TYPE_CHECKING: from narwhals._typing import Arrow, IntoBackend, Pandas, Polars + from narwhals.typing import DataFrameT, LazyFrameT GUARANTEES_ROW_ORDER = { Implementation.PANDAS, @@ -25,8 +26,8 @@ def assert_frame_equal( - left: DataFrame[Any] | LazyFrame[Any], - right: DataFrame[Any] | LazyFrame[Any], + left: DataFrameT | LazyFrameT, + right: DataFrameT | LazyFrameT, *, check_row_order: bool = True, check_column_order: bool = True, @@ -144,8 +145,8 @@ def assert_frame_equal( def _check_correct_input_type( # noqa: RET503 - left: DataFrame[Any] | LazyFrame[Any], - right: DataFrame[Any] | LazyFrame[Any], + left: DataFrameT | LazyFrameT, + right: DataFrameT | LazyFrameT, backend: IntoBackend[Polars | Pandas | Arrow] | None, ) -> tuple[DataFrame[Any], DataFrame[Any]]: # Adapted from https://github.com/pola-rs/polars/blob/afdbf3056d1228cf493901e45f536b0905cec8ea/py-polars/src/polars/testing/asserts/frame.py#L15-L17 @@ -164,8 +165,8 @@ def _check_correct_input_type( # noqa: RET503 def _assert_dataframe_equal( - left: DataFrame[Any], - right: DataFrame[Any], + left: DataFrameT, + right: DataFrameT, impl: Implementation, *, check_row_order: bool, @@ -231,11 +232,7 @@ def _assert_dataframe_equal( def _check_schema_equal( - left: DataFrame[Any], - right: DataFrame[Any], - *, - check_dtypes: bool, - check_column_order: bool, + left: DataFrameT, right: DataFrameT, *, check_dtypes: bool, check_column_order: bool ) -> None: """Compares DataFrame schema based on specified criteria. diff --git a/tests/testing/assert_frame_equal_test.py b/tests/testing/assert_frame_equal_test.py index 5d2f187267..31e899697a 100644 --- a/tests/testing/assert_frame_equal_test.py +++ b/tests/testing/assert_frame_equal_test.py @@ -41,7 +41,7 @@ def test_implementation_mismatch() -> None: import pyarrow as pa with _assertion_error("implementation mismatch"): - assert_frame_equal( + assert_frame_equal( # type: ignore[type-var] nw.from_native(pd.DataFrame({"a": [1]})), nw.from_native(pa.table({"a": [1]})) ) From ef3a00a21b7453f8f26f4c95ea6d50f5b1d8b008 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 12 Jun 2026 16:38:28 +0200 Subject: [PATCH 69/71] Move coverage patches for Windows users into Makefile --- .github/workflows/pytest.yml | 10 ---------- Makefile | 8 ++++++++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index ad4ce4b2a1..a00fcd0766 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -33,11 +33,6 @@ jobs: cache-dependency-glob: "pyproject.toml" - name: Run pytest shell: bash - env: - # coverage's execv/fork patches raise on Windows; collapse to `subprocess` - # there (coverage dedupes) and keep the default values on Linux. - COVERAGE_PATCH_EXECV: ${{ matrix.os == 'windows-latest' && 'subprocess' || 'execv' }} - COVERAGE_PATCH_FORK: ${{ matrix.os == 'windows-latest' && 'subprocess' || 'fork' }} run: make run-ci-coverage DEPS="--group tests --group plugins --extra pandas --extra polars --extra pyarrow" FAIL_UNDER="75" CMD="pytest tests --nw-backends=pandas,pyarrow,polars[eager],polars[lazy]" pytest-windows: @@ -47,11 +42,6 @@ jobs: python-version: ["3.10", "3.13"] os: [windows-latest] runs-on: ${{ matrix.os }} - env: - # coverage's execv/fork patches raise on Windows; collapse them to `subprocess` - # in the pyproject `patch` list (coverage dedupes). - COVERAGE_PATCH_EXECV: subprocess - COVERAGE_PATCH_FORK: subprocess steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv diff --git a/Makefile b/Makefile index 7235e0d352..47ee88fc60 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,14 @@ .DEFAULT_GOAL := help +# coverage's execv/fork patches raise on Windows; collapse them to `subprocess` +# there (coverage dedupes) and keep the default values elsewhere. +# See `tool.coverage.run.patch` in pyproject.toml. +ifeq ($(OS),Windows_NT) +export COVERAGE_PATCH_EXECV ?= subprocess +export COVERAGE_PATCH_FORK ?= subprocess +endif + .PHONY: help help: ## Display this help screen @echo -e "\033[1mAvailable commands:\033[0m" From 87cc74ee26a322c3c3fc9f65e56fe78383a7492b Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 12 Jun 2026 16:40:02 +0200 Subject: [PATCH 70/71] Rename is_nullable to nan_is_null --- src/narwhals/testing/constructors.py | 16 ++++++++-------- tests/conftest.py | 2 +- tests/expr_and_series/fill_nan_test.py | 6 ++---- tests/expr_and_series/is_close_test.py | 8 ++++---- tests/expr_and_series/is_finite_test.py | 2 +- tests/expr_and_series/is_nan_test.py | 4 ++-- tests/testing/constructors_test.py | 2 +- 7 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/narwhals/testing/constructors.py b/src/narwhals/testing/constructors.py index 847600f977..74a8ca9c75 100644 --- a/src/narwhals/testing/constructors.py +++ b/src/narwhals/testing/constructors.py @@ -106,7 +106,7 @@ def __init__( implementation: Implementation, requirements: tuple[str, ...] = (), is_eager: bool = False, - is_nullable: bool = True, + nan_is_null: bool = False, needs_gpu: bool = False, default_include: bool = True, ) -> None: @@ -115,7 +115,7 @@ def __init__( self.implementation = implementation self.requirements = requirements self.is_eager = is_eager - self.is_nullable = is_nullable + self.nan_is_null = nan_is_null self.needs_gpu = needs_gpu self.default_include = default_include @@ -127,7 +127,7 @@ def register( implementation: Implementation, requirements: tuple[str, ...] = (), is_eager: bool = False, - is_nullable: bool = True, + nan_is_null: bool = False, needs_gpu: bool = False, default_include: bool = True, ) -> Callable[[Callable[Concatenate[Data, ...], R]], frame_constructor[R]]: @@ -139,7 +139,7 @@ def register( requirements: Package names that must be importable for this constructor to be available (checked via `importlib.util.find_spec`). is_eager: Whether the backend returns an eager dataframe. - is_nullable: Whether the backend has native null support. + nan_is_null: Whether floating-point NaN values are considered null. needs_gpu: Whether the backend requires GPU hardware. default_include: Whether this backend is included by default when running `--all-nw-backends`. Set to `False` for backends that require @@ -157,7 +157,7 @@ def decorator(func: Callable[Concatenate[Data, ...], R]) -> frame_constructor[R] implementation=implementation, requirements=requirements, is_eager=is_eager, - is_nullable=is_nullable, + nan_is_null=nan_is_null, needs_gpu=needs_gpu, default_include=default_include, ) @@ -310,7 +310,7 @@ def __eq__(self, other: object) -> bool: implementation=Implementation.PANDAS, requirements=("pandas",), is_eager=True, - is_nullable=False, + nan_is_null=True, ) def pandas_constructor(obj: Data, /, **kwds: Any) -> pd.DataFrame: import pandas as pd @@ -359,7 +359,7 @@ def pyarrow_table_constructor(obj: Data, /, **kwds: Any) -> pa.Table: implementation=Implementation.MODIN, requirements=("modin",), is_eager=True, - is_nullable=False, + nan_is_null=True, default_include=False, ) def modin_constructor(obj: Data, /, **kwds: Any) -> IntoDataFrame: # pragma: no cover @@ -427,7 +427,7 @@ def polars_lazy_constructor(obj: Data, /, **kwds: Any) -> pl.LazyFrame: name="dask", implementation=Implementation.DASK, requirements=("dask",), - is_nullable=False, + nan_is_null=True, ) def dask_lazy_p2_constructor( obj: Data, /, npartitions: int = 2, **kwds: Any diff --git a/tests/conftest.py b/tests/conftest.py index ec306ab3d4..a1b2fbf6e1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -136,7 +136,7 @@ class _PatchedFrameConstructor: """Proxy over a `frame_constructor` defaulting `namespace` to `narwhals`. Delegates attribute access, `str()`, and `repr()` to the wrapped instance - so that test helpers (e.g. `constructor.is_nullable`, `"pandas" in str(constructor)`) + so that test helpers (e.g. `constructor.nan_is_null`, `"pandas" in str(constructor)`) keep working unchanged. """ diff --git a/tests/expr_and_series/fill_nan_test.py b/tests/expr_and_series/fill_nan_test.py index 1835d6c1f1..f74310cd82 100644 --- a/tests/expr_and_series/fill_nan_test.py +++ b/tests/expr_and_series/fill_nan_test.py @@ -23,8 +23,7 @@ def test_fill_nan(request: pytest.FixtureRequest, constructor: Constructor) -> N assert_equal_data(result, expected) assert result.lazy().collect()["float_na"].null_count() == 2 result = df.select(nw.all().fill_nan(3.0)) - if not constructor.is_nullable: - # no nan vs null distinction + if constructor.nan_is_null: expected = {"float": [-1.0, 1.0, 3.0], "float_na": [3.0, 1.0, 3.0]} assert result.lazy().collect()["float_na"].null_count() == 0 elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): @@ -42,8 +41,7 @@ def test_fill_nan_series(constructor_eager: ConstructorEager) -> None: "float_na" ] result = s.fill_nan(999) - if not constructor_eager.is_nullable: - # no nan vs null distinction + if constructor_eager.nan_is_null: assert_equal_data({"a": result}, {"a": [999.0, 1.0, 999.0]}) elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): assert_equal_data({"a": result}, {"a": [None, 1.0, None]}) diff --git a/tests/expr_and_series/is_close_test.py b/tests/expr_and_series/is_close_test.py index 01579e3f3f..8e51264897 100644 --- a/tests/expr_and_series/is_close_test.py +++ b/tests/expr_and_series/is_close_test.py @@ -114,7 +114,7 @@ def test_is_close_series_with_series( y = y.zip_with(y != NAN_PLACEHOLDER, y**0.5).zip_with(y != NULL_PLACEHOLDER, nulls) result = x.is_close(y, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal) - if not constructor_eager.is_nullable: + if constructor_eager.nan_is_null: expected = [v if v is not None else nans_equal for v in expected] elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): expected = [ @@ -142,7 +142,7 @@ def test_is_close_series_with_scalar( y = y.zip_with(y != NAN_PLACEHOLDER, y**0.5).zip_with(y != NULL_PLACEHOLDER, nulls) result = y.is_close(other, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal) - if not constructor_eager.is_nullable: + if constructor_eager.nan_is_null: expected = [v if v is not None else False for v in expected] elif "pandas" in str(constructor_eager) and PANDAS_VERSION >= (3,): expected = [ @@ -187,7 +187,7 @@ def test_is_close_expr_with_expr( ) .sort("idx") ) - if not constructor.is_nullable: + if constructor.nan_is_null: expected = [v if v is not None else nans_equal for v in expected] elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): expected = [ @@ -228,7 +228,7 @@ def test_is_close_expr_with_scalar( ) .sort("idx") ) - if not constructor.is_nullable: + if constructor.nan_is_null: expected = [v if v is not None else False for v in expected] elif "pandas" in str(constructor) and PANDAS_VERSION >= (3,): expected = [ diff --git a/tests/expr_and_series/is_finite_test.py b/tests/expr_and_series/is_finite_test.py index f55b106593..bd6dc588fd 100644 --- a/tests/expr_and_series/is_finite_test.py +++ b/tests/expr_and_series/is_finite_test.py @@ -64,7 +64,7 @@ def test_is_finite_column_with_null(constructor: Constructor, data: list[float]) result = df.select(nw.col("a").is_finite()) expected: dict[str, list[Any]] - if not constructor.is_nullable: + if constructor.nan_is_null: # Null values are coerced to NaN for non-nullable datatypes expected = {"a": [True, True, False]} else: diff --git a/tests/expr_and_series/is_nan_test.py b/tests/expr_and_series/is_nan_test.py index 9dce78c535..1fe5e714e6 100644 --- a/tests/expr_and_series/is_nan_test.py +++ b/tests/expr_and_series/is_nan_test.py @@ -20,7 +20,7 @@ def test_nan(constructor: Constructor) -> None: ) expected: dict[str, list[Any]] - if not constructor.is_nullable: + if constructor.nan_is_null: # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], @@ -57,7 +57,7 @@ def test_nan_series(constructor_eager: ConstructorEager) -> None: "float_na": df["float_na"].is_nan(), } expected: dict[str, list[Any]] - if not constructor_eager.is_nullable: + if constructor_eager.nan_is_null: # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], diff --git a/tests/testing/constructors_test.py b/tests/testing/constructors_test.py index ac7530436a..20b8b31af2 100644 --- a/tests/testing/constructors_test.py +++ b/tests/testing/constructors_test.py @@ -53,7 +53,7 @@ def test_lazy_returns_lazy_frame() -> None: ("is_spark_like", {"pyspark", "sqlframe", "pyspark[connect]"}, {"pandas"}), ("is_lazy", {"polars[lazy]", "dask", "duckdb"}, {"pandas"}), ("needs_pyarrow", {"pyarrow", "duckdb", "ibis"}, {"pandas"}), - ("is_nullable", {"polars[eager]"}, {"pandas", "modin", "dask"}), + ("nan_is_null", {"pandas", "modin", "dask"}, {"polars[eager]"}), ] From 81f0c7362439061997ba7864f09f87d911722c8d Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 12 Jun 2026 16:42:42 +0200 Subject: [PATCH 71/71] Apply most typing suggestions --- src/narwhals/stable/v1/typing.py | 2 +- tests/frame/interchange_native_namespace_test.py | 4 +--- tests/frame/interchange_select_test.py | 2 +- tests/ibis_test.py | 4 ++-- tests/testing/assert_frame_equal_test.py | 13 +++++++------ 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/narwhals/stable/v1/typing.py b/src/narwhals/stable/v1/typing.py index 5f4a1daff5..8ec49dd628 100644 --- a/src/narwhals/stable/v1/typing.py +++ b/src/narwhals/stable/v1/typing.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: from typing import TypeAlias - from narwhals._native import NativeDataFrame, NativeDuckDB, NativeLazyFrame + from narwhals._native import NativeDataFrame, NativeLazyFrame from narwhals.stable.v1 import DataFrame, Expr, LazyFrame, Series class DataFrameLike(Protocol): diff --git a/tests/frame/interchange_native_namespace_test.py b/tests/frame/interchange_native_namespace_test.py index 0face73928..a2faeaa480 100644 --- a/tests/frame/interchange_native_namespace_test.py +++ b/tests/frame/interchange_native_namespace_test.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Any - import pytest import narwhals.stable.v1 as nw_v1 @@ -9,7 +7,7 @@ pytest.importorskip("polars") import polars as pl -data: dict[str, list[Any]] = {"a": [1, 2, 3], "b": [4.5, 6.7, 8.9], "z": ["x", "y", "w"]} +data = {"a": [1, 2, 3], "b": [4.5, 6.7, 8.9], "z": ["x", "y", "w"]} def test_interchange() -> None: diff --git a/tests/frame/interchange_select_test.py b/tests/frame/interchange_select_test.py index 90279f0296..045ad257d6 100644 --- a/tests/frame/interchange_select_test.py +++ b/tests/frame/interchange_select_test.py @@ -7,7 +7,7 @@ import narwhals as nw import narwhals.stable.v1 as nw_v1 -data: dict[str, list[Any]] = {"a": [1, 2, 3], "b": [4.0, 5.0, 6.1], "z": ["x", "y", "z"]} +data = {"a": [1, 2, 3], "b": [4.0, 5.0, 6.1], "z": ["x", "y", "z"]} class InterchangeDataFrame: diff --git a/tests/ibis_test.py b/tests/ibis_test.py index a9a9dc413b..a03ef07fba 100644 --- a/tests/ibis_test.py +++ b/tests/ibis_test.py @@ -8,7 +8,7 @@ def test_from_native() -> None: ibis_constructor = get_backend_constructor("ibis") - if not ibis_constructor.is_available: - pytest.skip() + for requirement in ibis_constructor.requirements: + pytest.importorskip(requirement) df = ibis_constructor({"a": [1, 2, 3], "b": [4, 5, 6]}, nw) assert df.columns == ["a", "b"] diff --git a/tests/testing/assert_frame_equal_test.py b/tests/testing/assert_frame_equal_test.py index 31e899697a..3e9d453592 100644 --- a/tests/testing/assert_frame_equal_test.py +++ b/tests/testing/assert_frame_equal_test.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from narwhals.testing.typing import Data - from narwhals.typing import IntoSchema + from narwhals.typing import IntoFrame, IntoSchema from tests.utils import Constructor, ConstructorEager @@ -24,12 +24,12 @@ def _assertion_error(detail: str) -> pytest.RaisesExc: def test_check_narwhals_objects(constructor: Constructor) -> None: """Test that a type error is raised if the input is not a Narwhals object.""" - frame = constructor({"a": [1, 2, 3]}).to_native() + frame: IntoFrame = constructor({"a": [1, 2, 3]}).to_native() msg = re.escape( "Expected `narwhals.DataFrame` or `narwhals.LazyFrame` instance, found" ) with pytest.raises(TypeError, match=msg): - assert_frame_equal(frame, frame) + assert_frame_equal(frame, frame) # type: ignore[arg-type] def test_implementation_mismatch() -> None: @@ -40,10 +40,11 @@ def test_implementation_mismatch() -> None: import pandas as pd import pyarrow as pa + left = nw.from_native(pd.DataFrame({"a": [1]})) + right = nw.from_native(pa.table({"a": [1]})) + with _assertion_error("implementation mismatch"): - assert_frame_equal( # type: ignore[type-var] - nw.from_native(pd.DataFrame({"a": [1]})), nw.from_native(pa.table({"a": [1]})) - ) + assert_frame_equal(left, right) # type: ignore[type-var] # pyright: ignore[reportArgumentType] def test_check_same_input_type(constructor_eager: ConstructorEager) -> None: