Skip to content
Draft
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
100ac3b
feat: pytest plugin
FBruzzesi Apr 15, 2026
1fd776d
simplify a bit
FBruzzesi Apr 15, 2026
6ba28de
Simplify narwhals codebase
FBruzzesi Apr 15, 2026
7e2c50d
bulk tests change with minimal impact
FBruzzesi Apr 15, 2026
a6361f9
Adjust TPCH
FBruzzesi Apr 15, 2026
8c0f27f
split testing plugin and constructors into multiple test files
FBruzzesi Apr 15, 2026
24743ce
mix with Dan implementation
FBruzzesi Apr 16, 2026
1e05a49
fixup and allow kwargs
FBruzzesi Apr 16, 2026
c55beaa
simplify pytest_plugin
FBruzzesi Apr 16, 2026
879fb3e
merge main
FBruzzesi Apr 16, 2026
7e05734
defer imports
FBruzzesi Apr 16, 2026
db4784e
adjust GHA accordingly
FBruzzesi Apr 16, 2026
1ad6ffb
constructors module coverage
FBruzzesi Apr 16, 2026
c833b87
add makefile rule
FBruzzesi Apr 16, 2026
380b7be
fix test deps
FBruzzesi Apr 16, 2026
612b470
try with coverage run ... && coverage report
FBruzzesi Apr 16, 2026
af99861
fix(typing): WIP
FBruzzesi Apr 16, 2026
6e01b9b
revert 7e2c50d8bf2205e27d04addb4199f64823af083f
FBruzzesi Apr 16, 2026
81f41c9
forgot one file :(
FBruzzesi Apr 16, 2026
92a2ab1
alias imports
FBruzzesi Apr 16, 2026
9d9d0d2
fixup tests
FBruzzesi Apr 16, 2026
af31a60
try inline &&
FBruzzesi Apr 16, 2026
06be300
c.__name__ -> str(c)
FBruzzesi Apr 16, 2026
e9b94bb
try with fail-under flag under report
FBruzzesi Apr 16, 2026
2ac3da9
try no pytest-cov
FBruzzesi Apr 16, 2026
0e313de
fixup command
FBruzzesi Apr 16, 2026
6e09bfc
fixup pyarrow getitem
FBruzzesi Apr 16, 2026
5b5f507
back to pytest-cov
FBruzzesi Apr 16, 2026
11a8183
Act 1
FBruzzesi Apr 17, 2026
3acdbac
fully remove ConstructorName
FBruzzesi Apr 17, 2026
90d90c0
remove pytest-cov, coverage: run -> combine -> report
FBruzzesi Apr 17, 2026
b0fbd1b
add patch=subprocess option
FBruzzesi Apr 17, 2026
edb0263
pragma: no cover constructors as current main
FBruzzesi Apr 17, 2026
06a6961
add patches to coveragepy
FBruzzesi Apr 18, 2026
024640a
remove mapping hint in tests
FBruzzesi Apr 18, 2026
7d127f7
rename base classes
FBruzzesi Apr 18, 2026
e5c192e
merge main
FBruzzesi Apr 18, 2026
76b5851
rm execv patch flag
FBruzzesi Apr 18, 2026
e1749ac
addressing typing issues part 1
FBruzzesi Apr 18, 2026
df1afc3
add NativeIbis to v1 IntoDataFrame
FBruzzesi Apr 18, 2026
21befee
match polars signature in assert_frame_equal to fix typing
FBruzzesi Apr 18, 2026
96b6a44
try hack to define coverage patch flags dynamically
FBruzzesi Apr 18, 2026
57a5f18
port fix from #3555
FBruzzesi Apr 18, 2026
ffc2166
fix: pytest on py3.14 ported wrong min coverage
FBruzzesi Apr 18, 2026
ffbf61a
Run join_test with eager frames as well
FBruzzesi Apr 18, 2026
8042564
do not collect too early
FBruzzesi Apr 18, 2026
25126ea
fixup join test part 2
FBruzzesi Apr 18, 2026
024135a
test_join_duplicate_column_names fix
FBruzzesi Apr 18, 2026
e1d09e5
Adopt registry pattern, tackle a few other feedbacks
FBruzzesi Apr 21, 2026
e97f331
fixup typing and prepare_constructors regression
FBruzzesi Apr 21, 2026
e76e50d
fix __eq__
FBruzzesi Apr 21, 2026
b1da6a7
match is_nullable default
FBruzzesi Apr 21, 2026
bb6b042
fixup typevar default
FBruzzesi Apr 21, 2026
32018b3
Fixup default backends parsing
FBruzzesi Apr 21, 2026
37c1edb
no cover + GHA
FBruzzesi Apr 21, 2026
e811459
Merge branch 'main' into feat/testing-constructors
FBruzzesi Apr 22, 2026
9712559
refactor: Rename exposed fixtures and pytest options (#3556)
FBruzzesi Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions .github/workflows/extremes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -44,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:
Expand Down Expand Up @@ -82,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:
Expand Down Expand Up @@ -119,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:
Expand Down Expand Up @@ -175,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
1 change: 1 addition & 0 deletions .github/workflows/pytest-ibis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
env:
PY_COLORS: 1
PYTEST_ADDOPTS: "--numprocesses=logical"
COVERAGE_PROCESS_START: pyproject.toml
UV_SYSTEM_PYTHON: 1
jobs:

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pytest-modin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
env:
PY_COLORS: 1
PYTEST_ADDOPTS: "--numprocesses=logical"
COVERAGE_PROCESS_START: pyproject.toml
UV_SYSTEM_PYTHON: 1
jobs:

Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/pytest-pyspark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -40,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:
Expand Down Expand Up @@ -133,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()
Expand Down
35 changes: 30 additions & 5 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -30,7 +31,15 @@ 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]
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
coverage report --fail-under=75
- name: install-test-plugin
run: uv pip install -e test-plugin/.

Expand All @@ -40,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
Expand All @@ -60,7 +74,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:
Expand Down Expand Up @@ -91,7 +107,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'
Expand Down Expand Up @@ -157,7 +176,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=50

python-314t:
strategy:
Expand All @@ -183,4 +205,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
6 changes: 4 additions & 2 deletions .github/workflows/random_ci_pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
env:
PY_COLORS: 1
PYTEST_ADDOPTS: "--numprocesses=logical"
COVERAGE_PROCESS_START: pyproject.toml

jobs:
tox:
Expand Down Expand Up @@ -36,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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,15 @@ 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 --all-cpu-constructors --numprocesses=logical
$(VENV_BIN)/uv run --no-sync coverage combine
$(VENV_BIN)/uv run --no-sync coverage report --fail-under=95
64 changes: 64 additions & 0 deletions docs/api-reference/testing.md
Original file line number Diff line number Diff line change
@@ -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
11 changes: 9 additions & 2 deletions narwhals/stable/v1/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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.
Expand Down
19 changes: 11 additions & 8 deletions narwhals/testing/asserts/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading