Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 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
3f6beb6
merge main, resolve conflicts
FBruzzesi May 3, 2026
6355f0a
feat: Let constructor fixtures return narwhals object directly (#3569)
FBruzzesi May 5, 2026
6faf05a
merge main
FBruzzesi May 5, 2026
b6ba91e
fixup testing.md
FBruzzesi May 5, 2026
985eb40
merge main
FBruzzesi May 8, 2026
73fd0fc
fixup _select_backends
FBruzzesi May 8, 2026
141a537
modules as implementation of protocols
FBruzzesi May 8, 2026
7144039
merge main
FBruzzesi May 9, 2026
5f57dd1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 9, 2026
6c7f625
fix up formatting and linting
FBruzzesi May 9, 2026
f909afb
fix up formatting and linting
FBruzzesi May 9, 2026
8a605aa
Merge branch 'main' into feat/testing-constructors
FBruzzesi May 12, 2026
496c7e5
merge main, hopefully solve conflicts
FBruzzesi Jun 1, 2026
fc34512
convert to native
FBruzzesi Jun 1, 2026
ef90f44
add cov_source flag in make command
FBruzzesi Jun 1, 2026
b1359d2
merge main, solve conflicts
FBruzzesi Jun 3, 2026
13c6dde
constructors -> nw-backends
FBruzzesi Jun 3, 2026
d52403d
add available_default_cpu_backends (#3673)
EdAbati Jun 4, 2026
62ba007
Refactor pytest_addoption with Edo suggestion
FBruzzesi Jun 4, 2026
1db05c9
add test for _default_backend_ids NARWHALS_DEFAULT_BACKENDS path
FBruzzesi Jun 4, 2026
e18ca1c
add test for multiple narwhals fixtures at once
FBruzzesi Jun 4, 2026
91a0fd4
rollback typing changes... as I cannot recall their reason
FBruzzesi Jun 4, 2026
ef3a00a
Move coverage patches for Windows users into Makefile
FBruzzesi Jun 12, 2026
87cc74e
Rename is_nullable to nan_is_null
FBruzzesi Jun 12, 2026
81f0c73
Apply most typing suggestions
FBruzzesi Jun 12, 2026
2a92a22
merge main
FBruzzesi Jun 12, 2026
c36d780
Merge branch 'main' into feat/testing-constructors
FBruzzesi Jun 15, 2026
589dfb4
merge main, solve conflicts
FBruzzesi Jul 2, 2026
349be76
--group extra -> sklearn
FBruzzesi Jul 2, 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
13 changes: 8 additions & 5 deletions .github/workflows/extremes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
env:
PY_COLORS: 1
PYTEST_ADDOPTS: "--numprocesses=logical"
COVERAGE_PROCESS_START: pyproject.toml
UV_LOCKED: 1
UV_NO_DEV: 1

Expand All @@ -35,7 +36,7 @@ jobs:
cache-suffix: min-versions-${{ matrix.python-version }}
cache-dependency-glob: "pyproject.toml"
- name: Run pytest
run: make run-ci DEPS="--group extreme-minimum-versions --group plugins" CMD="pytest tests --cov=src --cov=tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb"
run: make run-ci-coverage DEPS="--group extreme-minimum-versions --group plugins" FAIL_UNDER="50" CMD="pytest tests --runslow --nw-backends=pandas,pyarrow,polars[eager],polars[lazy],duckdb"

pretty_old_versions:
strategy:
Expand All @@ -53,7 +54,7 @@ jobs:
cache-suffix: pretty-old-versions-${{ matrix.python-version }}
cache-dependency-glob: "pyproject.toml"
- name: Run pytest
run: make run-ci DEPS="--group extreme-pretty-old-versions --group plugins" CMD="pytest tests --cov=src --cov=tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],duckdb"
run: make run-ci-coverage DEPS="--group extreme-pretty-old-versions --group plugins" FAIL_UNDER="50" CMD="pytest tests --runslow --nw-backends=pandas,pyarrow,polars[eager],polars[lazy],duckdb"

not_so_old_versions:
strategy:
Expand All @@ -71,7 +72,7 @@ jobs:
cache-suffix: not-so-old-versions-${{ matrix.python-version }}
cache-dependency-glob: "pyproject.toml"
- name: Run pytest
run: make run-ci DEPS="--group extreme-not-so-old-versions --group plugins" CMD="pytest tests --cov=src --cov=tests --cov-fail-under=50 --runslow --constructors=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb"
run: make run-ci-coverage DEPS="--group extreme-not-so-old-versions --group plugins" FAIL_UNDER="50" CMD="pytest tests --runslow --nw-backends=pandas,pyarrow,polars[eager],polars[lazy],dask,duckdb"

nightlies:
strategy:
Expand Down Expand Up @@ -131,5 +132,7 @@ jobs:
echo "$DEPS" | grep 'dask.*@'
- name: Run pytest
run: |
uv run --no-sync pytest tests --cov=src --cov=tests --cov-fail-under=50 --runslow \
--constructors="pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb"
uv run --no-sync coverage run -m pytest tests --runslow \
--nw-backends="pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb"
uv run --no-sync coverage combine
uv run --no-sync coverage report --fail-under=50
3 changes: 2 additions & 1 deletion .github/workflows/pytest-ibis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ on:
env:
PY_COLORS: 1
PYTEST_ADDOPTS: "--numprocesses=logical"
COVERAGE_PROCESS_START: pyproject.toml
UV_LOCKED: 1
UV_NO_DEV: 1

Expand All @@ -39,4 +40,4 @@ jobs:
cache-suffix: ibis-${{ matrix.python-version }}
cache-dependency-glob: "pyproject.toml"
- name: Run pytest
run: make run-ci DEPS="--group core-tests --extra ibis --extra pandas" CMD="pytest tests --constructors=ibis"
run: make run-ci DEPS="--group core-tests --extra ibis --extra pandas" CMD="pytest tests --nw-backends=ibis"
3 changes: 2 additions & 1 deletion .github/workflows/pytest-modin.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_LOCKED: 1
UV_NO_DEV: 1

Expand All @@ -38,4 +39,4 @@ jobs:
cache-suffix: modin-${{ matrix.python-version }}
cache-dependency-glob: "pyproject.toml"
- name: Run pytest
run: make run-ci DEPS="--group core-tests --extra modin" CMD="pytest tests --constructors=modin[pyarrow]"
run: make run-ci DEPS="--group core-tests --extra modin" CMD="pytest tests --nw-backends=modin[pyarrow]"
10 changes: 7 additions & 3 deletions .github/workflows/pytest-pyspark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ on:
env:
PY_COLORS: 1
PYTEST_ADDOPTS: "--numprocesses=logical"
COVERAGE_PROCESS_START: pyproject.toml
UV_LOCKED: 1
UV_NO_DEV: 1

Expand All @@ -42,7 +43,7 @@ jobs:
cache-suffix: pyspark-${{ matrix.python-version }}
cache-dependency-glob: "pyproject.toml"
- name: Run pytest
run: make run-ci DEPS="--group core-tests --extra pyspark --extra pandas" CMD="pytest tests --cov=src/narwhals/_spark_like --cov-fail-under=95 --runslow --constructors=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'
Expand All @@ -61,7 +62,7 @@ jobs:
cache-suffix: pyspark-min-${{ matrix.python-version }}
cache-dependency-glob: "pyproject.toml"
- name: Run pytest
run: make run-ci DEPS="--group min-pyspark-version --extra pandas" CMD="pytest tests --cov=src/narwhals/_spark_like --cov-fail-under=95 --runslow --constructors=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'
Expand Down Expand Up @@ -109,7 +110,10 @@ jobs:
echo "Spark Connect server started"

- name: Run pytest
run: uv run --no-sync pytest tests --cov=src/narwhals/_spark_like --cov-fail-under=95 --runslow --constructors "pyspark[connect]"
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 --include="src/narwhals/_spark_like/*" --fail-under=95

- name: Stop Spark Connect server
if: always()
Expand Down
18 changes: 10 additions & 8 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
env:
PY_COLORS: 1
PYTEST_ADDOPTS: "--numprocesses=logical"
COVERAGE_PROCESS_START: pyproject.toml
UV_LOCKED: 1
UV_NO_DEV: 1

Expand All @@ -37,7 +38,7 @@ jobs:
cache-dependency-glob: "pyproject.toml"
- name: Run pytest
shell: bash
run: make run-ci DEPS="--group tests --group plugins --extra pandas --extra polars --extra pyarrow" CMD="pytest tests --cov=src --cov=tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy]"
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:
if: github.head_ref != 'bump-version'
Expand All @@ -58,7 +59,8 @@ jobs:
# we are not testing pyspark, modin, or dask on Windows here because nobody got time for that
- name: Run pytest
shell: bash
run: make run-ci DEPS="--group core-tests --group sklearn --group plugins --extra pandas" CMD="pytest tests --cov=src --cov=tests --runslow --cov-fail-under=95 --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe"
run: make run-ci-coverage DEPS="--group core-tests --group sklearn --group plugins --extra pandas" FAIL_UNDER="95" CMD="pytest tests --runslow --durations=30 --nw-backends=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe"


pytest-full-coverage:
if: github.head_ref != 'bump-version'
Expand All @@ -82,7 +84,7 @@ jobs:
cache-suffix: pytest-full-coverage-${{ matrix.python-version }}
cache-dependency-glob: "pyproject.toml"
- name: Run pytest
run: make run-ci DEPS="--extra pandas --extra dask --group core-tests --group sklearn --group plugins" CMD="pytest tests --cov=src --cov=tests --cov-fail-under=100 --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe"
run: make run-ci-coverage DEPS="--extra pandas --extra dask --group core-tests --group sklearn --group plugins" FAIL_UNDER="100" CMD="pytest tests --runslow --durations=30 --nw-backends=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe"
- name: Run doctests
# reprs differ between versions, so we only run doctests on the latest Python
if: matrix.python-version == '3.13'
Expand All @@ -108,11 +110,11 @@ jobs:
cache-suffix: pytest-narrower-deps-${{ matrix.python-version }}
cache-dependency-glob: "pyproject.toml"
- name: Run pytest (pandas and pandas[nullable])
run: make run-ci DEPS="-U --group tests --extra pandas" RUN_ONLY="--isolated" CMD="pytest tests --runslow --constructors=pandas,pandas[nullable]"
run: make run-ci DEPS="-U --group tests --extra pandas" RUN_ONLY="--isolated" CMD="pytest tests --runslow --nw-backends=pandas,pandas[nullable]"
- name: Run pytest (pandas[pyarrow] and pyarrow)
run: make run-ci DEPS="-U --group tests --extra pandas --extra pyarrow" RUN_ONLY="--isolated" CMD="pytest tests --runslow --constructors=pandas[pyarrow],pyarrow"
run: make run-ci DEPS="-U --group tests --extra pandas --extra pyarrow" RUN_ONLY="--isolated" CMD="pytest tests --runslow --nw-backends=pandas[pyarrow],pyarrow"
- name: Run pytest (polars)
run: make run-ci DEPS="-U --group tests --extra polars" RUN_ONLY="--isolated" CMD="pytest tests --runslow --constructors=polars[eager],polars[lazy]"
run: make run-ci DEPS="-U --group tests --extra polars" RUN_ONLY="--isolated" CMD="pytest tests --runslow --nw-backends=polars[eager],polars[lazy]"

python-314:
if: github.head_ref != 'bump-version'
Expand All @@ -131,7 +133,7 @@ jobs:
cache-suffix: python-314-${{ matrix.python-version }}
cache-dependency-glob: "pyproject.toml"
- name: Run pytest
run: make run-ci DEPS="--group core-tests --group plugins --extra pandas" CMD="pytest tests --cov=src --cov=tests --cov-fail-under=50 --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe"
run: make run-ci-coverage DEPS="--group core-tests --group plugins --extra pandas" FAIL_UNDER="50" CMD="pytest tests --runslow --durations=30 --nw-backends=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],duckdb,sqlframe"

python-314t:
if: github.head_ref != 'bump-version'
Expand All @@ -153,4 +155,4 @@ jobs:
cache-suffix: python-314t-${{ matrix.python-version }}
cache-dependency-glob: "pyproject.toml"
- name: Run pytest
run: make run-ci DEPS="--pre --group tests --group plugins --extra pandas --extra pyarrow" CMD="pytest tests --cov=src --cov=tests --cov-fail-under=50 --runslow --durations=30 --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow"
run: make run-ci-coverage DEPS="--pre --group tests --group plugins --extra pandas --extra pyarrow" FAIL_UNDER="50" CMD="pytest tests --runslow --durations=30 --nw-backends=pandas,pandas[nullable],pandas[pyarrow],pyarrow"
7 changes: 5 additions & 2 deletions .github/workflows/random_ci_pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ env:
PYTEST_ADDOPTS: "--numprocesses=logical"
UV_LOCKED: 1
UV_NO_DEV: 1
COVERAGE_PROCESS_START: pyproject.toml

permissions:
contents: read
Expand Down Expand Up @@ -41,5 +42,7 @@ jobs:
- name: Run pytest with random versions
run: |
uv run --group tests --group plugins --with-requirements random-requirements.txt \
pytest tests --cov=src --cov=tests --cov-fail-under=75 \
--constructors="pandas,pyarrow,polars[eager],polars[lazy]"
coverage run -m pytest tests \
--nw-backends="pandas,pyarrow,polars[eager],polars[lazy]"
uv run --group tests --group plugins --with-requirements random-requirements.txt coverage combine
uv run --group tests --group plugins --with-requirements random-requirements.txt coverage report --fail-under=75
2 changes: 1 addition & 1 deletion .github/workflows/test_sdist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,12 @@ If you add code that should be tested, please add tests.
- To run tests on the doctests, use `uv run pytest src --doctest-modules`
- To run unit tests and doctests at the same time, run `uv run pytest src tests --cov=src --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 can use the `--constructors` flag:
- To only run tests for pandas, Polars, and PyArrow, use `uv run pytest --constructors="pandas,pyarrow,polars[eager]"`
- To run tests for all CPU constructors, use `uv run --extra modin --extra pyspark pytest --all-cpu-constructors`
- To choose which backends to run tests with, you can use the `--nw-backends` flag:
- To only run tests for pandas, Polars, and PyArrow, use `uv run pytest --nw-backends="pandas,pyarrow,polars[eager]"`
- To run tests for all CPU constructors, use `uv run --extra modin --extra pyspark pytest --all-nw-backends`
- By default, tests run for pandas, pandas (PyArrow dtypes), PyArrow, and Polars.
- To run tests using `cudf.pandas`, run `NARWHALS_DEFAULT_CONSTRUCTORS=pandas uv run --extra cudf --module cudf.pandas --module pytest`
- To run tests using `polars[gpu]`, run `NARWHALS_POLARS_GPU=1 uv run pytest --constructors="polars[lazy]"`
- To run tests using `polars[gpu]`, run `NARWHALS_POLARS_GPU=1 uv run pytest --nw-backends="polars[lazy]"`

Tip: passing extras (`--extra <name>`) or groups (`--group <name>`) to `uv run` will transparently sync the environment to include those dependencies before running the command.

Expand Down
17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -31,6 +39,13 @@ docs-clean-serve: ## Rebuild docs from a clean state and serve them locally
$(MAKE) docs-serve

.PHONY: run-ci
run-ci: ## Print resolved deps, then run a command via uv. Usage: make run-ci DEPS="<groups/extras>" CMD="<command>" [RUN_ONLY="<uv-run-only flags, e.g. --isolated, --with X, --no-sync>"]
run-ci: ## Print resolved deps, then run a command via uv (no coverage; used by doctests, narrow-dep runs, tpch, ibis, modin). Usage: make run-ci DEPS="<groups/extras>" CMD="<command>" [RUN_ONLY="<uv-run-only flags, e.g. --isolated, --with X, --no-sync>"]
uv export --no-annotate --no-hashes $(DEPS)
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="<groups/extras>" CMD="<command, e.g. pytest tests --nw-backends=...>" [FAIL_UNDER="<int>"] [COV_SOURCE="<path to scope coverage to, e.g. src/narwhals/_spark_like; drives both 'run --source' and 'report --include'>"] [RUN_ONLY="<uv-run-only flags>"]
uv export --no-annotate --no-hashes $(DEPS)
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 $(COV_SOURCE),--include=$(COV_SOURCE)/*) $(if $(FAIL_UNDER),--fail-under=$(FAIL_UNDER))
80 changes: 80 additions & 0 deletions docs/api-reference/testing.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,88 @@
# `narwhals.testing`

## Assertions

::: narwhals.testing
handler: python
options:
show_root_heading: false
heading_level: 3
members:
- assert_frame_equal
- assert_series_equal

## `pytest` plugin

Narwhals register a pytest plugin that exposes parametrized fixtures with callables
to build Narwhals frames from a column-oriented python `dict`.

### Available fixtures

| Fixture | 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 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
another plugin provide the `constructor*` fixtures.

Set the `NARWHALS_DEFAULT_BACKENDS` 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
import narwhals.stable.v2 as nw_v2

if TYPE_CHECKING:
from narwhals.testing.typing import Data, DataFrameConstructor, LazyFrameConstructor


def test_shape(nw_dataframe: DataFrameConstructor) -> None:
data: Data = {"x": [1, 2, 3]}
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
in the current environment. Filter the matrix on the command line:

```bash
pytest --nw-backends="pandas,polars[lazy]"
pytest --all-nw-backends
```

## Type aliases

::: narwhals.testing.typing
handler: python
options:
show_root_heading: false
heading_level: 3
members:
- Data
- FrameConstructor
- DataFrameConstructor
- LazyFrameConstructor
- NarwhalsNamespace
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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
Expand Down Expand Up @@ -72,7 +76,6 @@ plugins = [
tests = [
"covdefaults",
"pytest>=9.0.3",
"pytest-cov",
"pytest-env",
"pytest-randomly",
"pytest-xdist",
Expand Down Expand Up @@ -423,7 +426,12 @@ env = [
]

[tool.coverage.run]
# 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

[tool.coverage.report]
fail_under = 80 # This is just for local development, in CI we set it to 100
Expand Down
3 changes: 2 additions & 1 deletion src/narwhals/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Loading
Loading