diff --git a/docs/api-reference/testing.md b/docs/api-reference/testing.md index 2cc088c941..8be76787f6 100644 --- a/docs/api-reference/testing.md +++ b/docs/api-reference/testing.md @@ -46,7 +46,6 @@ 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 import narwhals.stable.v2 as nw_v2 if TYPE_CHECKING: @@ -55,16 +54,19 @@ if TYPE_CHECKING: def test_shape(nw_dataframe: DataFrameConstructor) -> None: data: Data = {"x": [1, 2, 3]} - df = nw_dataframe(data, namespace=nw) + df = nw_dataframe(data) # (1)! 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) + lf = nw_lazyframe(data, namespace=nw_v2) # (2)! assert isinstance(lf, nw_v2.LazyFrame) ``` +1. Wraps with the main `narwhals` namespace by default +2. Opt in to a stable namespace + The fixtures are parametrised against every supported backend that is installed in the current environment. Filter the matrix on the command line: @@ -73,7 +75,7 @@ pytest --nw-backends="pandas,polars[lazy]" pytest --all-nw-backends ``` -## Type aliases +## Typing ::: narwhals.testing.typing handler: python @@ -82,7 +84,9 @@ pytest --all-nw-backends heading_level: 3 members: - Data + - ConstructorProtocol - FrameConstructor - DataFrameConstructor - LazyFrameConstructor + - PandasConstructor - NarwhalsNamespace diff --git a/pyproject.toml b/pyproject.toml index 03bd8ce0f6..a20b0f4ce3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -457,6 +457,7 @@ exclude_also = [ 'if "(cudf|modin|pyspark|ibis)" in', "if not .*.is_pandas", "if is_ibis_table", + "if is_native_ibis", "if MYPY", "except ModuleNotFoundError", 'request.applymarker\(pytest.mark.xfail', diff --git a/src/narwhals/_namespace.py b/src/narwhals/_namespace.py index 87b2f2b359..fb715e5792 100644 --- a/src/narwhals/_namespace.py +++ b/src/narwhals/_namespace.py @@ -273,7 +273,7 @@ def from_native_object( impl = Implementation.CUDF elif is_native_modin(native): # pragma: no cover impl = Implementation.MODIN - elif is_native_ibis(native): # pragma: no cover + elif is_native_ibis(native): impl = Implementation.IBIS else: msg = f"Unsupported type: {type(native).__qualname__!r}" diff --git a/src/narwhals/testing/constructors.py b/src/narwhals/testing/constructors.py index 74a8ca9c75..5cce116163 100644 --- a/src/narwhals/testing/constructors.py +++ b/src/narwhals/testing/constructors.py @@ -36,6 +36,7 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeVar, cast, overload from narwhals._utils import Implementation, generate_temporary_column_name +from narwhals.testing.typing import ConstructorProtocol if TYPE_CHECKING: from collections.abc import Callable, Iterable @@ -77,7 +78,9 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame: R = TypeVar("R", bound="IntoFrame") -class frame_constructor(Generic[T_co]): # noqa: N801 +class frame_constructor( # noqa: N801 + ConstructorProtocol["DataFrame[Any] | LazyFrame[Any]"], Generic[T_co] +): """Callable wrapper around a backend frame builder. Turns a column-oriented `dict` (typed as [`Data`][narwhals.testing.typing.Data]) @@ -86,6 +89,9 @@ class frame_constructor(Generic[T_co]): # noqa: N801 `func`. Equality and hashing are keyed on `(type, name)`, so two lookups of the same registered constructor compare equal. + Implements [`ConstructorProtocol`][narwhals.testing.typing.ConstructorProtocol], + from which the member docstrings are inherited. + Warning: Instances should be created via [`narwhals.testing.constructors.frame_constructor.register`][], which is the only supported entry point. @@ -171,7 +177,7 @@ def __call__( self: frame_constructor[IntoDataFrameT], obj: Data, /, - namespace: NarwhalsNamespace, + namespace: NarwhalsNamespace | None = ..., **kwds: Any, ) -> DataFrame[IntoDataFrameT]: ... @overload @@ -179,7 +185,7 @@ def __call__( self: frame_constructor[IntoLazyFrameT], obj: Data, /, - namespace: NarwhalsNamespace, + namespace: NarwhalsNamespace | None = ..., **kwds: Any, ) -> LazyFrame[IntoLazyFrameT]: ... @overload @@ -187,103 +193,83 @@ def __call__( self: frame_constructor[IntoFrame], obj: Data, /, - namespace: NarwhalsNamespace, + namespace: NarwhalsNamespace | None = ..., **kwds: Any, - ) -> DataFrame[Any] | LazyFrame[Any]: ... + ) -> DataFrame[IntoDataFrame] | LazyFrame[IntoLazyFrame]: ... def __call__( - self, obj: Data, /, namespace: NarwhalsNamespace, **kwds: Any + self, obj: Data, /, namespace: NarwhalsNamespace | None = None, **kwds: Any ) -> DataFrame[Any] | LazyFrame[Any]: - """Build a native frame and wrap it with `namespace.from_native`. + if namespace is None: + import narwhals - 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. - """ + namespace = narwhals native = self.func(obj, **kwds) return namespace.from_native(native) # type: ignore[no-any-return] @property def identifier(self) -> str: - """Instance-level string identifier for test IDs.""" 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.""" 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: diff --git a/src/narwhals/testing/pytest_plugin.py b/src/narwhals/testing/pytest_plugin.py index 7c0a02e600..f3527f3e47 100644 --- a/src/narwhals/testing/pytest_plugin.py +++ b/src/narwhals/testing/pytest_plugin.py @@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, cast if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Callable, Sequence import pytest @@ -87,7 +87,9 @@ def pytest_addoption(parser: pytest.Parser) -> None: ) -def _select_backends(config: pytest.Config) -> list[FrameConstructor]: # pragma: no cover +def _select_backends( # pragma: no cover + config: pytest.Config, +) -> Sequence[FrameConstructor]: from narwhals.testing.constructors import ( available_default_cpu_backends, prepare_backends, @@ -128,4 +130,4 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: 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]) + metafunc.parametrize(fixture_name, params, ids=[c.identifier for c in params]) diff --git a/src/narwhals/testing/typing.py b/src/narwhals/testing/typing.py index a5d5144bd7..2a4c0157f1 100644 --- a/src/narwhals/testing/typing.py +++ b/src/narwhals/testing/typing.py @@ -1,25 +1,26 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Protocol +from typing import TYPE_CHECKING, Any, Protocol, TypeVar if TYPE_CHECKING: from collections.abc import Callable from typing import TypeAlias - from narwhals.testing.constructors import frame_constructor - from narwhals.typing import IntoDataFrame, IntoFrame, IntoLazyFrame + import pandas as pd + from narwhals import DataFrame, LazyFrame + from narwhals.typing import IntoDataFrame, IntoLazyFrame -__all__ = ("Data", "DataFrameConstructor", "FrameConstructor", "LazyFrameConstructor") -FrameConstructor: TypeAlias = "frame_constructor[IntoFrame]" -"""Type alias for a constructor that returns a native eager or lazy frame.""" - -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.""" +__all__ = ( + "ConstructorProtocol", + "Data", + "DataFrameConstructor", + "FrameConstructor", + "LazyFrameConstructor", + "NarwhalsNamespace", + "PandasConstructor", +) Data: TypeAlias = dict[str, Any] # TODO(Unassined): This should have a better annotation """A column-oriented mapping used as input to a frame constructor.""" @@ -29,3 +30,149 @@ class NarwhalsNamespace(Protocol): """Minimal specs of a narwhals namespace (e.g. `narwhals`, `narwhals.stable.v1`).""" from_native: Callable[..., Any] + + +FrameT_co = TypeVar("FrameT_co", bound="DataFrame[Any] | LazyFrame[Any]", covariant=True) +"""A narwhals frame type produced by a constructor (covariant).""" + + +class ConstructorProtocol(Protocol[FrameT_co]): + """Interface of a frame constructor, generic over the narwhals frame it returns. + + Implemented by [`narwhals.testing.frame_constructor`][], which inherits the + member docstrings defined here. Use the parametrized aliases + ([`FrameConstructor`][narwhals.testing.typing.FrameConstructor], + [`DataFrameConstructor`][narwhals.testing.typing.DataFrameConstructor], + [`LazyFrameConstructor`][narwhals.testing.typing.LazyFrameConstructor], + [`PandasConstructor`][narwhals.testing.typing.PandasConstructor]) to annotate + test parameters. + """ + + is_eager: bool + """Whether the backend returns an eager dataframe.""" + + nan_is_null: bool + """Whether floating-point NaN values are considered null.""" + + needs_gpu: bool + """Whether the backend requires GPU hardware.""" + + default_include: bool + """Whether this backend is included by default when running `--all-nw-backends`.""" + + def __hash__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + + def __call__( + self, obj: Data, /, namespace: NarwhalsNamespace | None = None, **kwds: Any + ) -> FrameT_co: + """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. + Defaults to the main `narwhals` namespace. + **kwds: Forwarded to the wrapped builder. + """ + ... + + @property + def identifier(self) -> str: + """Instance-level string identifier for test IDs.""" + ... + + @property + def is_lazy(self) -> bool: + """Whether this constructor produces a lazy native frame.""" + ... + + @property + def is_pandas(self) -> bool: + """Whether this is one of the pandas constructors.""" + ... + + @property + def is_modin(self) -> bool: + """Whether this is one of the modin constructors.""" + ... + + @property + def is_cudf(self) -> bool: + """Whether this is the cudf constructor.""" + ... + + @property + def is_pandas_like(self) -> bool: + """Whether this constructor produces a pandas-like dataframe (pandas, modin, cudf).""" + ... + + @property + def is_polars(self) -> bool: + """Whether this is one of the polars constructors.""" + ... + + @property + def is_pyarrow(self) -> bool: + """Whether this is the pyarrow table constructor.""" + ... + + @property + def is_dask(self) -> bool: + """Whether this is the dask constructor.""" + ... + + @property + def is_duckdb(self) -> bool: + """Whether this is the duckdb constructor.""" + ... + + @property + def is_pyspark(self) -> bool: + """Whether this is one of the pyspark constructors.""" + ... + + @property + def is_sqlframe(self) -> bool: + """Whether this is the sqlframe constructor.""" + ... + + @property + def is_ibis(self) -> bool: + """Whether this is the ibis constructor.""" + ... + + @property + def is_spark_like(self) -> bool: + """Whether this constructor uses a spark-like backend (pyspark, sqlframe).""" + ... + + @property + def needs_pyarrow(self) -> bool: + """Whether this constructor requires `pyarrow` to be installed.""" + ... + + @property + def is_available(self) -> bool: + """Whether every package this constructor needs is importable.""" + ... + + +FrameConstructor: TypeAlias = ( + "ConstructorProtocol[DataFrame[IntoDataFrame] | LazyFrame[IntoLazyFrame]]" +) +"""Type alias for a constructor that returns a native eager or lazy frame.""" + +DataFrameConstructor: TypeAlias = "ConstructorProtocol[DataFrame[IntoDataFrame]]" +"""Type alias for a constructor that returns an eager native dataframe.""" + +LazyFrameConstructor: TypeAlias = "ConstructorProtocol[LazyFrame[IntoLazyFrame]]" +"""Type alias for a constructor that returns a lazy native frame.""" + +PandasConstructor: TypeAlias = "ConstructorProtocol[DataFrame[pd.DataFrame]]" +"""Type alias for a constructor whose native frame is treated as a `pandas.DataFrame`. + +Use it in pandas-specific tests so `.to_native()` exposes the concrete `pandas` +API (`.columns.name`, `.dtypes`, ...). At runtime such a test may also receive a +pandas-*like* frame (modin, cudf); the annotation is a deliberate simplification. +""" diff --git a/tests/conftest.py b/tests/conftest.py index 5cb6d489f6..4c155d4f3b 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, cast +from typing import TYPE_CHECKING import pytest @@ -17,15 +17,8 @@ from collections.abc import Sequence 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, - NarwhalsNamespace, - ) - from narwhals.typing import IntoFrame, NonNestedDType + from narwhals.testing.typing import DataFrameConstructor, FrameConstructor + from narwhals.typing import NonNestedDType from tests.utils import NestedOrEnumDType @@ -124,62 +117,28 @@ def nested_dtype(request: pytest.FixtureRequest) -> NestedOrEnumDType: return dtype -# 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.nan_is_null`, `"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: NarwhalsNamespace = 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: NarwhalsNamespace = nw, **kwds: Any - ) -> DataFrame[Any]: - return cast("DataFrame[Any]", self._inner(obj, namespace=namespace, **kwds)) +# The following fixtures are short-name aliases of those registered in +# `narwhals/testing/pytest_plugin.py`. Calling a constructor 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 requests `nw_frame` / +# `nw_dataframe` / `nw_pandas_like_frame` directly. @pytest.fixture -def constructor(nw_frame: FrameConstructor) -> _PatchedFrameConstructor: - return _PatchedFrameConstructor(nw_frame) +def constructor(nw_frame: FrameConstructor) -> FrameConstructor: + return nw_frame @pytest.fixture -def constructor_eager(nw_dataframe: DataFrameConstructor) -> _PatchedDataFrameConstructor: - return _PatchedDataFrameConstructor(nw_dataframe) +def constructor_eager(nw_dataframe: DataFrameConstructor) -> DataFrameConstructor: + return nw_dataframe @pytest.fixture def constructor_pandas_like( nw_pandas_like_frame: DataFrameConstructor, -) -> _PatchedDataFrameConstructor: - return _PatchedDataFrameConstructor(nw_pandas_like_frame) +) -> DataFrameConstructor: + return nw_pandas_like_frame diff --git a/tests/dependencies/is_into_dataframe_test.py b/tests/dependencies/is_into_dataframe_test.py index 95544d593c..0681da4417 100644 --- a/tests/dependencies/is_into_dataframe_test.py +++ b/tests/dependencies/is_into_dataframe_test.py @@ -7,6 +7,7 @@ import narwhals as nw import narwhals.stable.v1 as nw_v1 import narwhals.stable.v2 as nw_v2 +from narwhals._native import is_native_ibis from narwhals.dependencies import is_into_dataframe from narwhals.stable.v1.dependencies import is_into_dataframe as v1_is_into_dataframe from narwhals.stable.v2.dependencies import is_into_dataframe as v2_is_into_dataframe @@ -16,8 +17,8 @@ from typing_extensions import Self + from narwhals.testing.typing import FrameConstructor from tests.dependencies.conftest import AlwaysHasAttr - from tests.utils import Constructor EAGER_CONSTRUCTOR_NAMES = ("pandas", "modin", "cudf", "polars_eager", "pyarrow") V1_INTO_DATAFRAMES = (*EAGER_CONSTRUCTOR_NAMES, "duckdb", "ibis") @@ -41,22 +42,28 @@ def drop(self, *args: Any, **kwargs: Any) -> Any: ... def join(self, *args: Any, **kwargs: Any) -> Any: ... -def test_is_into_dataframe(constructor: Constructor) -> None: - native_frame = constructor(data).to_native() - nw_frame = nw.from_native(native_frame) - nw_v1_frame = nw_v1.from_native(native_frame) +def test_is_into_dataframe(nw_frame: FrameConstructor) -> None: + native_frame = nw_frame(data, nw).to_native() + frame = nw.from_native(native_frame) + if is_native_ibis(native_frame): + # NOTE: `call-overload` because v1 `from_native` typing does not admit + # `NativeIbis`, yet at runtime ibis tables are still wrapped + # (as interchange-level frames). + nw_v1_frame = nw_v1.from_native(native_frame) # type: ignore[call-overload] + else: + nw_v1_frame = nw_v1.from_native(native_frame) nw_v2_frame = nw_v2.from_native(native_frame) - result = any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES) - result_v1 = any(x in str(constructor) for x in V1_INTO_DATAFRAMES) + result = any(x in str(nw_frame) for x in EAGER_CONSTRUCTOR_NAMES) + result_v1 = any(x in str(nw_frame) for x in V1_INTO_DATAFRAMES) assert is_into_dataframe(native_frame) == result assert v1_is_into_dataframe(native_frame) == result assert v2_is_into_dataframe(native_frame) == result - assert is_into_dataframe(nw_frame) == result - assert not v1_is_into_dataframe(nw_frame) - assert not v2_is_into_dataframe(nw_frame) + assert is_into_dataframe(frame) == result + assert not v1_is_into_dataframe(frame) + assert not v2_is_into_dataframe(frame) assert is_into_dataframe(nw_v1_frame) == result_v1 assert v1_is_into_dataframe(nw_v1_frame) == result_v1 diff --git a/tests/dependencies/is_into_lazyframe_test.py b/tests/dependencies/is_into_lazyframe_test.py index b0edfd5ab5..c480235b0c 100644 --- a/tests/dependencies/is_into_lazyframe_test.py +++ b/tests/dependencies/is_into_lazyframe_test.py @@ -7,6 +7,7 @@ import narwhals as nw import narwhals.stable.v1 as nw_v1 import narwhals.stable.v2 as nw_v2 +from narwhals._native import is_native_ibis from narwhals.dependencies import is_into_lazyframe from narwhals.stable.v1.dependencies import is_into_lazyframe as v1_is_into_lazyframe from narwhals.stable.v2.dependencies import is_into_lazyframe as v2_is_into_lazyframe @@ -42,7 +43,13 @@ def join(self, *args: Any, **kwargs: Any) -> Any: ... def test_is_into_lazyframe(constructor: Constructor) -> None: native_frame = constructor(data).to_native() nw_frame = nw.from_native(native_frame) - nw_v1_frame = nw_v1.from_native(native_frame) + if is_native_ibis(native_frame): + # NOTE: `call-overload` because v1 `from_native` typing does not admit + # `NativeIbis`, yet at runtime ibis tables are still wrapped + # (as interchange-level frames). + nw_v1_frame = nw_v1.from_native(native_frame) # type: ignore[call-overload] + else: + nw_v1_frame = nw_v1.from_native(native_frame) nw_v2_frame = nw_v2.from_native(native_frame) result = not any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES) diff --git a/tests/expr_and_series/dt/timestamp_test.py b/tests/expr_and_series/dt/timestamp_test.py index e199fcac76..82520931b3 100644 --- a/tests/expr_and_series/dt/timestamp_test.py +++ b/tests/expr_and_series/dt/timestamp_test.py @@ -171,7 +171,7 @@ def test_timestamp_dates( dates = {"a": [datetime(2001, 1, 1), None, datetime(2001, 1, 3)]} if "dask" in str(constructor): df = nw.from_native( - constructor(dates).astype({"a": "timestamp[ns][pyarrow]"}) # type: ignore[union-attr] + constructor(dates).to_native().astype({"a": "timestamp[ns][pyarrow]"}) # type: ignore[union-attr] ) else: df = nw.from_native(constructor(dates)) diff --git a/tests/expr_and_series/over_test.py b/tests/expr_and_series/over_test.py index 0cee2ed6ba..141f4f62e3 100644 --- a/tests/expr_and_series/over_test.py +++ b/tests/expr_and_series/over_test.py @@ -475,7 +475,7 @@ def test_over_quantile(constructor: Constructor, request: pytest.FixtureRequest) native_frame = constructor(data).to_native() if "dask" in str(constructor): - native_frame = native_frame.repartition(npartitions=1) + native_frame = native_frame.repartition(npartitions=1) # type: ignore[union-attr] result = ( nw.from_native(native_frame) diff --git a/tests/frame/sample_test.py b/tests/frame/sample_test.py index 7ef63a04f3..dbc5ddf67a 100644 --- a/tests/frame/sample_test.py +++ b/tests/frame/sample_test.py @@ -71,5 +71,5 @@ def test_sample_with_seed(constructor_eager: ConstructorEager) -> None: r2 = nw.to_native(df.sample(n=n, seed=123)) r3 = nw.to_native(df.sample(n=n, seed=42)) - assert r1.equals(r2) - assert not r1.equals(r3) + assert r1.equals(r2) # type: ignore[attr-defined] + assert not r1.equals(r3) # type: ignore[attr-defined] diff --git a/tests/frame/schema_test.py b/tests/frame/schema_test.py index 0246904493..12fad75b43 100644 --- a/tests/frame/schema_test.py +++ b/tests/frame/schema_test.py @@ -23,7 +23,7 @@ IntoPandasSchema, IntoPolarsSchema, ) - from tests.utils import Constructor, ConstructorEager, ConstructorPandasLike + from tests.utils import Constructor, ConstructorEager, PandasConstructor TimeUnit: TypeAlias = Literal["ns", "us"] @@ -571,9 +571,7 @@ def origin_arrow( @pytest.fixture -def origin_pandas_like( - constructor_pandas_like: ConstructorPandasLike, -) -> IntoPandasSchema: +def origin_pandas_like(constructor_pandas_like: PandasConstructor) -> IntoPandasSchema: data: dict[str, Any] = { "a": [2, 1], "b": ["hello", "hi"], @@ -581,12 +579,12 @@ def origin_pandas_like( "d": [5.3, 4.99], "e": [datetime(2006, 1, 1), datetime(2001, 9, 3)], } - return constructor_pandas_like(data).to_native().dtypes.to_dict() # type: ignore[no-any-return] + return constructor_pandas_like(data).to_native().dtypes.to_dict() @pytest.fixture def origin_pandas_like_pyarrow( - constructor_pandas_like: ConstructorPandasLike, + constructor_pandas_like: PandasConstructor, ) -> IntoPandasSchema: if PANDAS_VERSION < (1, 5): pytest.skip(reason="pandas too old for `pyarrow`") @@ -606,7 +604,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() # type: ignore[no-any-return] + return df_nw.to_native().dtypes.to_dict() def test_schema_from_polars( diff --git a/tests/frame/with_row_index_test.py b/tests/frame/with_row_index_test.py index cfc47b23f7..7a76d8a90b 100644 --- a/tests/frame/with_row_index_test.py +++ b/tests/frame/with_row_index_test.py @@ -56,7 +56,7 @@ def test_with_row_index_lazy_exception(constructor: Constructor) -> None: msg = r"(LazyFrame\.)?with_row_index\(\) missing 1 required keyword-only argument: 'order_by'$" if isinstance(frame, nw.LazyFrame): with pytest.raises(TypeError, match=msg): - frame.with_row_index() # type: ignore[call-arg] + frame.with_row_index() # type: ignore[call-arg] # pyright: ignore[reportCallIssue] # pyrefly: ignore[missing-argument] else: result = frame.with_row_index() assert_equal_data(result, {"index": [0, 1], **data}) diff --git a/tests/preserve_pandas_like_columns_name_attr_test.py b/tests/preserve_pandas_like_columns_name_attr_test.py index 546b388f67..c915cedf1f 100644 --- a/tests/preserve_pandas_like_columns_name_attr_test.py +++ b/tests/preserve_pandas_like_columns_name_attr_test.py @@ -7,11 +7,11 @@ import narwhals as nw if TYPE_CHECKING: - from tests.utils import Constructor + from tests.utils import PandasConstructor def test_ops_preserve_column_index_name( - constructor: Constructor, request: pytest.FixtureRequest + constructor: PandasConstructor, request: pytest.FixtureRequest ) -> None: if not any(x in str(constructor) for x in ("pandas", "modin", "cudf", "dask")): pytest.skip( diff --git a/tests/testing/assert_frame_equal_test.py b/tests/testing/assert_frame_equal_test.py index 3e9d453592..c5894ab94a 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 IntoFrame, IntoSchema + from narwhals.typing import IntoSchema from tests.utils import Constructor, ConstructorEager @@ -24,7 +24,7 @@ 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: IntoFrame = constructor({"a": [1, 2, 3]}).to_native() + frame = constructor({"a": [1, 2, 3]}).to_native() msg = re.escape( "Expected `narwhals.DataFrame` or `narwhals.LazyFrame` instance, found" ) diff --git a/tests/testing/plugin_test.py b/tests/testing/plugin_test.py index e8b4ba5ff6..c1f80a9de0 100644 --- a/tests/testing/plugin_test.py +++ b/tests/testing/plugin_test.py @@ -18,11 +18,11 @@ def test_constructor_eager_fixture_runs_for_each_backend( pytester.makeconftest("") pytester.makepyfile(""" - import narwhals as nw from narwhals.testing.typing import DataFrameConstructor def test_shape(nw_dataframe: DataFrameConstructor) -> None: - df = nw_dataframe({"x": [1, 2, 3]}, namespace=nw) + # `namespace` defaults to the main `narwhals` namespace + df = nw_dataframe({"x": [1, 2, 3]}) assert df.shape == (3, 1) """) result = pytester.runpytest_subprocess( diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 6c72eb9fa6..0a47c4ae77 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -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) + nw.from_native(df, eager_only=True, pass_through=False) # type: ignore[type-var] def test_from_native_lazyframe_exhaustive() -> None: # noqa: PLR0914, PLR0915 diff --git a/tests/utils.py b/tests/utils.py index ffa167d5f7..91ff7f7a75 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -13,14 +13,15 @@ import narwhals as nw from narwhals._utils import Implementation, parse_version from narwhals.dependencies import get_pandas -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, +from narwhals.testing.typing import ( + DataFrameConstructor as ConstructorEager, + DataFrameConstructor as ConstructorPandasLike, + FrameConstructor as Constructor, + PandasConstructor, ) +from narwhals.translate import from_native if TYPE_CHECKING: from collections.abc import Mapping, Sequence @@ -33,7 +34,12 @@ # 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", "ConstructorPandasLike") +__all__ = ( + "Constructor", + "ConstructorEager", + "ConstructorPandasLike", + "PandasConstructor", +) def get_module_version_as_tuple(module_name: str) -> tuple[int, ...]: diff --git a/tests/v1_test.py b/tests/v1_test.py index fb45f129fd..b0ebd9eca3 100644 --- a/tests/v1_test.py +++ b/tests/v1_test.py @@ -489,7 +489,7 @@ def test_with_row_index(constructor: Constructor) -> None: ) with context: - result = frame.with_row_index() # type: ignore[call-arg] + result = frame.with_row_index() # type: ignore[call-arg] # pyright: ignore[reportCallIssue] # pyrefly: ignore[missing-argument] expected = {"index": [0, 1], **data} assert_equal_data(result, expected)