diff --git a/.github/workflows/extremes.yml b/.github/workflows/extremes.yml index a13896924a..aff65fab70 100644 --- a/.github/workflows/extremes.yml +++ b/.github/workflows/extremes.yml @@ -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 @@ -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: @@ -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: @@ -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: @@ -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 diff --git a/.github/workflows/pytest-ibis.yml b/.github/workflows/pytest-ibis.yml index b4e2509fd2..a31795883a 100644 --- a/.github/workflows/pytest-ibis.yml +++ b/.github/workflows/pytest-ibis.yml @@ -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 @@ -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" diff --git a/.github/workflows/pytest-modin.yml b/.github/workflows/pytest-modin.yml index 630709c1f0..099b71d186 100644 --- a/.github/workflows/pytest-modin.yml +++ b/.github/workflows/pytest-modin.yml @@ -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 @@ -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]" diff --git a/.github/workflows/pytest-pyspark.yml b/.github/workflows/pytest-pyspark.yml index 0071f5fc8f..554840ffcb 100644 --- a/.github/workflows/pytest-pyspark.yml +++ b/.github/workflows/pytest-pyspark.yml @@ -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 @@ -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' @@ -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' @@ -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() diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 4ebb0ae7f8..537a8d633c 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -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 @@ -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' @@ -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' @@ -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' @@ -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' @@ -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' @@ -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" diff --git a/.github/workflows/random_ci_pytest.yml b/.github/workflows/random_ci_pytest.yml index 4e861110eb..e262ba27d1 100644 --- a/.github/workflows/random_ci_pytest.yml +++ b/.github/workflows/random_ci_pytest.yml @@ -13,6 +13,7 @@ env: PYTEST_ADDOPTS: "--numprocesses=logical" UV_LOCKED: 1 UV_NO_DEV: 1 + COVERAGE_PROCESS_START: pyproject.toml permissions: contents: read @@ -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 diff --git a/.github/workflows/test_sdist.yml b/.github/workflows/test_sdist.yml index 8abc716563..d8d9536acf 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]" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8cc0ff3324..c59025a9e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 `) or groups (`--group `) to `uv run` will transparently sync the environment to include those dependencies before running the command. diff --git a/Makefile b/Makefile index 3a20692e8a..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" @@ -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="" CMD="" [RUN_ONLY=""] +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="" CMD="" [RUN_ONLY=""] 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="" CMD="" [FAIL_UNDER=""] [COV_SOURCE=""] [RUN_ONLY=""] + 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)) diff --git a/docs/api-reference/testing.md b/docs/api-reference/testing.md index db83c6930e..2cc088c941 100644 --- a/docs/api-reference/testing.md +++ b/docs/api-reference/testing.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 5b7cf25672..03bd8ce0f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 @@ -72,7 +76,6 @@ plugins = [ tests = [ "covdefaults", "pytest>=9.0.3", - "pytest-cov", "pytest-env", "pytest-randomly", "pytest-xdist", @@ -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 diff --git a/src/narwhals/testing/__init__.py b/src/narwhals/testing/__init__.py index 649463383f..6eb8c0b0d0 100644 --- a/src/narwhals/testing/__init__.py +++ b/src/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/src/narwhals/testing/constructors.py b/src/narwhals/testing/constructors.py new file mode 100644 index 0000000000..74a8ca9c75 --- /dev/null +++ b/src/narwhals/testing/constructors.py @@ -0,0 +1,723 @@ +"""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. + +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 + +```py +from narwhals.testing import frame_constructor + + +@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 + + return my_backend.from_dict(obj) +``` +""" + +from __future__ import annotations + +import os +import uuid +import warnings +from copy import deepcopy +from functools import lru_cache +from importlib.util import find_spec +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 Callable, Iterable + from typing import Concatenate, TypeAlias + + 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 import DataFrame, LazyFrame + from narwhals._native import NativeDask, NativeDuckDB, NativePySpark, NativeSQLFrame + from narwhals.testing.typing import Data, NarwhalsNamespace + from narwhals.typing import ( + IntoDataFrame, + IntoDataFrameT, + IntoFrame, + IntoLazyFrame, + IntoLazyFrameT, + ) + + +__all__ = ( + "available_backends", + "available_cpu_backends", + "available_default_cpu_backends", + "frame_constructor", + "get_backend_constructor", + "is_backend_available", + "prepare_backends", + "pyspark_session", + "sqlframe_session", +) + +T_co = TypeVar("T_co", covariant=True, bound="IntoFrame") +R = TypeVar("R", bound="IntoFrame") + + +class frame_constructor(Generic[T_co]): # noqa: N801 + """Callable wrapper around a backend frame builder. + + 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. + + Warning: + Instances should be created via [`narwhals.testing.constructors.frame_constructor.register`][], + which is the only supported entry point. + + Direct instantiation is allowed but **does not** register the instance. + """ + + _registry: ClassVar[dict[str, frame_constructor[IntoFrame]]] = {} + + func: Callable[Concatenate[Data, ...], T_co] + + def __init__( + self, + func: Callable[Concatenate[Data, ...], T_co], + /, + *, + name: str, + implementation: Implementation, + requirements: tuple[str, ...] = (), + is_eager: bool = False, + nan_is_null: bool = False, + needs_gpu: bool = False, + default_include: bool = True, + ) -> None: + self.func = func + self.name = name + self.implementation = implementation + self.requirements = requirements + self.is_eager = is_eager + self.nan_is_null = nan_is_null + self.needs_gpu = needs_gpu + self.default_include = default_include + + @classmethod + def register( + cls, + *, + name: str, + implementation: Implementation, + requirements: tuple[str, ...] = (), + is_eager: bool = False, + nan_is_null: bool = False, + needs_gpu: bool = False, + default_include: bool = True, + ) -> Callable[[Callable[Concatenate[Data, ...], 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`). + is_eager: Whether the backend returns an eager dataframe. + 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 + explicit opt-in (e.g. modin, pyspark[connect]). + + Returns: + A decorator that replaces `func` with a `frame_constructor` + instance registered into the shared `_registry`. + """ + + def decorator(func: Callable[Concatenate[Data, ...], R]) -> frame_constructor[R]: + inst: frame_constructor[R] = frame_constructor( + func, + name=name, + implementation=implementation, + requirements=requirements, + is_eager=is_eager, + nan_is_null=nan_is_null, + needs_gpu=needs_gpu, + default_include=default_include, + ) + cls._registry[name] = inst + return inst + + return decorator + + @overload + def __call__( + self: frame_constructor[IntoDataFrameT], + obj: Data, + /, + namespace: NarwhalsNamespace, + **kwds: Any, + ) -> DataFrame[IntoDataFrameT]: ... + @overload + def __call__( + self: frame_constructor[IntoLazyFrameT], + obj: Data, + /, + namespace: NarwhalsNamespace, + **kwds: Any, + ) -> LazyFrame[IntoLazyFrameT]: ... + @overload + def __call__( + self: frame_constructor[IntoFrame], + obj: Data, + /, + namespace: NarwhalsNamespace, + **kwds: Any, + ) -> DataFrame[Any] | LazyFrame[Any]: ... + + def __call__( + self, obj: Data, /, namespace: NarwhalsNamespace, **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: + """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: + # NOTE: This is a temporary hack + # TODO(FBruzzesi): Remove once all the `"backend" in str(constructor)` + # statements in the test suite are properly replaced + return self.func.__name__ + + def __repr__(self) -> str: + return f"{type(self).__name__}(name={self.name!r})" + + def __hash__(self) -> int: + return hash((type(self), self.name)) + + def __eq__(self, other: object) -> bool: + return isinstance(other, frame_constructor) and self.name == other.name + + +# Eager constructors + + +@frame_constructor.register( + name="pandas", + implementation=Implementation.PANDAS, + requirements=("pandas",), + is_eager=True, + nan_is_null=True, +) +def pandas_constructor(obj: Data, /, **kwds: Any) -> pd.DataFrame: + import pandas as pd + + return pd.DataFrame(obj, **kwds) + + +@frame_constructor.register( + name="pandas[nullable]", + implementation=Implementation.PANDAS, + requirements=("pandas",), + 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") + + +@frame_constructor.register( + name="pandas[pyarrow]", + implementation=Implementation.PANDAS, + requirements=("pandas", "pyarrow"), + 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") + + +@frame_constructor.register( + name="pyarrow", + implementation=Implementation.PYARROW, + requirements=("pyarrow",), + is_eager=True, +) +def pyarrow_table_constructor(obj: Data, /, **kwds: Any) -> pa.Table: + import pyarrow as pa + + return pa.table(obj, **kwds) + + +@frame_constructor.register( + name="modin", + implementation=Implementation.MODIN, + requirements=("modin",), + is_eager=True, + nan_is_null=True, + default_include=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))) + + +@frame_constructor.register( + name="modin[pyarrow]", + implementation=Implementation.MODIN, + requirements=("modin", "pyarrow"), + is_eager=True, + default_include=False, +) +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) + + +@frame_constructor.register( + name="cudf", + implementation=Implementation.CUDF, + requirements=("cudf",), + is_eager=True, + needs_gpu=True, +) +def cudf_constructor(obj: Data, /, **kwds: Any) -> IntoDataFrame: # pragma: no cover + import cudf + + return cast("IntoDataFrame", cudf.DataFrame(obj, **kwds)) + + +@frame_constructor.register( + name="polars[eager]", + implementation=Implementation.POLARS, + requirements=("polars",), + is_eager=True, +) +def polars_eager_constructor(obj: Data, /, **kwds: Any) -> pl.DataFrame: + import polars as pl + + return pl.DataFrame(obj, **kwds) + + +# Lazy constructors + + +@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) + + +@frame_constructor.register( + name="dask", + implementation=Implementation.DASK, + requirements=("dask",), + nan_is_null=True, +) +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)) + + +@frame_constructor.register( + name="duckdb", + implementation=Implementation.DUCKDB, + requirements=("duckdb", "pyarrow"), +) +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") + + +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(), strict=True)], 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) + + +@frame_constructor.register( + name="pyspark[connect]", + implementation=Implementation.PYSPARK_CONNECT, + requirements=("pyspark",), + default_include=False, +) +def pyspark_connect_lazy_constructor( + obj: Data, /, **kwds: Any +) -> NativePySpark: # pragma: no cover + return _pyspark_build(obj, **kwds) + + +@frame_constructor.register( + name="sqlframe", + implementation=Implementation.SQLFRAME, + requirements=("sqlframe", "duckdb"), +) +def sqlframe_pyspark_lazy_constructor(obj: Data, /, **kwds: Any) -> NativeSQLFrame: + session = sqlframe_session() + return session.createDataFrame( + [*zip(*obj.values(), strict=True)], schema=[*obj.keys()], **kwds + ) + + +@frame_constructor.register( + name="ibis", + implementation=Implementation.IBIS, + requirements=("ibis", "duckdb", "pyarrow"), +) +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) + + +DEFAULT_BACKENDS: frozenset[str] = frozenset( + { + "pandas", + "pandas[pyarrow]", + "polars[eager]", + "pyarrow", + "duckdb", + "sqlframe", + "ibis", + } +) +"""Subset of backends enabled by default for parametrised tests when the +user does not pass `--nw-backends` (mirrors the historical Narwhals defaults). +""" + + +def available_backends() -> frozenset[str]: + """Return the names of every constructor whose backend is importable. + + Examples: + >>> from narwhals.testing.constructors import available_backends + >>> "pandas" in available_backends() + True + """ + return frozenset( + name for name, c in frame_constructor._registry.items() if c.is_available + ) + + +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_backends + >>> "pandas" in available_cpu_backends() + True + """ + return frozenset( + name + for name, c in frame_constructor._registry.items() + if c.is_available and not c.needs_gpu + ) + + +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]", + "pandas[pyarrow]", + "modin", + "modin[pyarrow]", + "cudf", + "polars[eager]", + "pyarrow", +] +LazyName: TypeAlias = Literal[ + "polars[lazy]", "dask", "duckdb", "pyspark", "pyspark[connect]", "sqlframe", "ibis" +] + + +@overload +def get_backend_constructor(name: EagerName) -> frame_constructor[IntoDataFrame]: ... +@overload +def get_backend_constructor(name: LazyName) -> frame_constructor[IntoLazyFrame]: ... +@overload +def get_backend_constructor(name: str) -> frame_constructor[IntoFrame]: ... + + +def get_backend_constructor(name: str) -> frame_constructor[IntoFrame]: + """Return the registered 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_backend_constructor + >>> get_backend_constructor("pandas") + frame_constructor(name='pandas') + """ + try: + return frame_constructor._registry[name] + except KeyError as exc: + valid = sorted(frame_constructor._registry) + msg = f"Unknown constructor {name!r}. Expected one of: {valid}." + raise ValueError(msg) from exc + + +def prepare_backends( + *, include: Iterable[str] | None = None, exclude: Iterable[str] | None = None +) -> list[frame_constructor[IntoFrame]]: + """Return available constructors, optionally filtered. + + Note: + `exclude` is given precedence in the selection. + + Arguments: + 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_backends + >>> backends = prepare_backends(include=["pandas", "polars[eager]"]) + """ + available = available_backends() + candidates: list[frame_constructor[Any]] = [ + c for name, c in frame_constructor._registry.items() if name in available + ] + + 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) + + 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) + + +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/src/narwhals/testing/pytest_plugin.py b/src/narwhals/testing/pytest_plugin.py new file mode 100644 index 0000000000..7c0a02e600 --- /dev/null +++ b/src/narwhals/testing/pytest_plugin.py @@ -0,0 +1,131 @@ +"""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. + +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 + +import os +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from collections.abc import Callable + + import pytest + + from narwhals.testing.typing import FrameConstructor + + +_MIN_PANDAS_NULLABLE_VERSION: tuple[int, ...] = (2, 0, 0) +"""`pandas.convert_dtypes(dtype_backend=...)` requires pandas >= 2.0.0.""" + + +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__) + + +def _default_backend_ids() -> list[str]: + """Resolve the default `--nw-backends` value for the current environment. + + Honours `NARWHALS_DEFAULT_BACKENDS` if set, otherwise restricts + [`DEFAULT_BACKENDS`][] to backends whose libraries are importable. + """ + if env := os.environ.get("NARWHALS_DEFAULT_BACKENDS"): + return env.split(",") + 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_BACKENDS + ] + + +def pytest_addoption(parser: pytest.Parser) -> None: + from narwhals.testing.constructors import DEFAULT_BACKENDS + + group = parser.getgroup("narwhals", "narwhals-testing") + defaults = ", ".join(f"'{c}'" for c in sorted(DEFAULT_BACKENDS)) + group.addoption( + "--nw-backends", + action="store", + default=",".join(_default_backend_ids()), + type=str, + help=( + "Comma-separated list of (data|lazy) frame backend constructors to" + f"parametrise. Defaults to the installed subset of ({defaults})" + ), + ) + group.addoption( + "--all-nw-backends", + action="store_true", + default=False, + help=("Run tests against every installed CPU backend (overrides --nw-backends)."), + ) + # 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-nw-backend", + action="store_true", + default=False, + help=( + "Skip narwhals-testing's parametrisation and let another plugin " + "provide the `nw_*frame_constructor` fixtures." + ), + ) + + +def _select_backends(config: pytest.Config) -> list[FrameConstructor]: # pragma: no cover + from narwhals.testing.constructors import ( + available_default_cpu_backends, + prepare_backends, + ) + + if config.getoption("all_nw_backends"): + 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] + selected = prepare_backends(include=names) + + if _pandas_version() < _MIN_PANDAS_NULLABLE_VERSION: + _pandas_nullables = {"pandas[nullable]", "pandas[pyarrow]"} + selected = [c for c in selected if c.name not in _pandas_nullables] + return selected + + +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 (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) + 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]) diff --git a/src/narwhals/testing/typing.py b/src/narwhals/testing/typing.py new file mode 100644 index 0000000000..a5d5144bd7 --- /dev/null +++ b/src/narwhals/testing/typing.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Protocol + +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 + + +__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.""" + +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 af1f5e529d..5cb6d489f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,80 +1,41 @@ 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, cast 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 -if TYPE_CHECKING: - from collections.abc import Callable, Sequence - from typing import TypeAlias +# `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 `--nw-backends`, `--all-nw-backends`, and `--use-external-constructor` +# CLI options as well as parametrising the `constructor*` fixtures. - import ibis - import pandas as pd - import polars as pl - import pyarrow as pa - from ibis.backends.duckdb import Backend as IbisDuckDBBackend +if TYPE_CHECKING: + from collections.abc import Sequence - 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.dataframe import DataFrame, LazyFrame + from narwhals.testing.constructors import frame_constructor + from narwhals.testing.typing import ( + Data, + DataFrameConstructor, + FrameConstructor, + NarwhalsNamespace, ) - - Data: TypeAlias = "dict[str, list[Any]]" + from narwhals.typing import IntoFrame, NonNestedDType + from tests.utils import NestedOrEnumDType -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 +46,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,237 +53,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(), strict=False)], 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(), strict=False)], schema=list(obj)) - - -@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] = { # pyrefly: ignore[bad-assignment] - "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, strict=False): - 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 () @@ -393,3 +122,64 @@ 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`, +# 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)) + + +@pytest.fixture +def constructor(nw_frame: FrameConstructor) -> _PatchedFrameConstructor: + return _PatchedFrameConstructor(nw_frame) + + +@pytest.fixture +def constructor_eager(nw_dataframe: DataFrameConstructor) -> _PatchedDataFrameConstructor: + return _PatchedDataFrameConstructor(nw_dataframe) + + +@pytest.fixture +def constructor_pandas_like( + nw_pandas_like_frame: DataFrameConstructor, +) -> _PatchedDataFrameConstructor: + return _PatchedDataFrameConstructor(nw_pandas_like_frame) 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"] 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 f43ff85362..5c2b475a36 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 if TYPE_CHECKING: @@ -12,5 +11,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/dtypes/dtypes_test.py b/tests/dtypes/dtypes_test.py index ed675ceceb..4e1fef1ce7 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/arithmetic_test.py b/tests/expr_and_series/arithmetic_test.py index 3e9130fbf4..4ae7a14db6 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}) @@ -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_dataframe(data, nw) 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_dataframe(data, nw) 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_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) @@ -160,31 +160,27 @@ 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( - nw.col("a") // right - ) + result = nw_dataframe({"a": [left]}, nw).select(nw.col("a") // right) assert_equal_data(result, expected) @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( - 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}) @@ -240,16 +236,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_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/corr_test.py b/tests/expr_and_series/corr_test.py index 4617e52ecd..1e8df866b4 100644 --- a/tests/expr_and_series/corr_test.py +++ b/tests/expr_and_series/corr_test.py @@ -25,7 +25,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( @@ -51,7 +51,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() @@ -75,7 +75,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( @@ -97,7 +97,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( 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/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/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..f74310cd82 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,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 any(constructor is c for c in NON_NULLABLE_CONSTRUCTORS): - # 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,): @@ -55,8 +41,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): - # 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 16c59536ca..8e51264897 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.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 = [ @@ -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.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 = [ @@ -199,7 +187,7 @@ def test_is_close_expr_with_expr( ) .sort("idx") ) - if constructor in NON_NULLABLE_CONSTRUCTORS: + 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 = [ @@ -240,7 +228,7 @@ def test_is_close_expr_with_scalar( ) .sort("idx") ) - if constructor in NON_NULLABLE_CONSTRUCTORS: + 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 eb07b2a41e..bd6dc588fd 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.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 27790e27b2..1fe5e714e6 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.nan_is_null: # 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.nan_is_null: # Null values are coerced to NaN for non-nullable datatypes expected = { "int": [False, False, True], 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/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( diff --git a/tests/expr_and_series/over_test.py b/tests/expr_and_series/over_test.py index 5f83b95e93..0cee2ed6ba 100644 --- a/tests/expr_and_series/over_test.py +++ b/tests/expr_and_series/over_test.py @@ -472,10 +472,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/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/expr_and_series/str/to_time_test.py b/tests/expr_and_series/str/to_time_test.py index fceed2688d..29af4c0225 100644 --- a/tests/expr_and_series/str/to_time_test.py +++ b/tests/expr_and_series/str/to_time_test.py @@ -21,7 +21,7 @@ def requires_time_support( Skip or mark tests as expected failures depending on backend capabilities, version, and pyarrow availability when testing Time dtype support. """ - if constructor.__name__.startswith(("pandas", "modin")): + if str(constructor).startswith(("pandas", "modin")): if PANDAS_VERSION < (2, 2, 0): pytest.skip( "pandas < 2.2.0 has no pyarrow dtype support (and therefore does not support the Time dtype)" 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/group_by_test.py b/tests/frame/group_by_test.py index 85388c6dda..08d64804b4 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]} def test_group_by_complex() -> None: diff --git a/tests/frame/interchange_native_namespace_test.py b/tests/frame/interchange_native_namespace_test.py index 79a92ef6c9..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 TYPE_CHECKING, Any - import pytest import narwhals.stable.v1 as nw_v1 @@ -9,10 +7,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 = {"a": [1, 2, 3], "b": [4.5, 6.7, 8.9], "z": ["x", "y", "w"]} def test_interchange() -> None: @@ -60,9 +55,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..045ad257d6 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 = {"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/join_test.py b/tests/frame/join_test.py index 07a9f24add..51ff17c316 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, @@ -512,16 +500,27 @@ 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 = 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] + 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) + + 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) + assert_equal_data(result.sort(by="antananarivo"), expected) assert_equal_data(result_on.sort(by="antananarivo"), expected) @@ -581,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": [ @@ -592,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": [ @@ -606,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 ) @@ -622,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], @@ -630,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 = { @@ -657,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" ) @@ -675,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, @@ -688,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, @@ -754,13 +765,16 @@ 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): + frame = constructor(data).lazy() + + if constructor.is_lazy: with pytest.raises(ValueError, match=message): - df.join_asof(df, on=on, by_left=by_left, by_right=by_right, by=by) + frame.join_asof(frame, 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) + frame.collect().join_asof( + frame.collect(), on=on, by_left=by_left, by_right=by_right, by=by + ) def test_join_duplicate_column_names( @@ -777,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 = nw.from_native(constructor(data)) + lf = constructor(data).lazy() if any( x in str(constructor) for x in ("pandas", "pandas[pyarrow]", "pandas[nullable]", "dask") @@ -796,10 +810,12 @@ def test_join_duplicate_column_names( request.applymarker(pytest.mark.xfail) else: exception = nw.exceptions.DuplicateError - if isinstance(df, nw.LazyFrame): + + if constructor.is_lazy: with pytest.raises(exception): # pyrefly: ignore[unbound-name] - 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): # pyrefly: ignore[unbound-name] df.join(df, on=["a"]).join(df, on=["a"]) @@ -875,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"] @@ -896,8 +912,8 @@ def test_join_on_null_values_single_key( # incorrectly matched `null == null` here (the two-key case in # `test_join_on_null_values` only triggers it on 0.20.21). # See https://github.com/narwhals-dev/narwhals/issues/3307 - df_left = from_native_lazy(constructor({"a": [1, 2, None], "x": [10, 20, 30]})) - df_right = from_native_lazy(constructor({"a": [1, None], "y": [1.2, 3.4]})) + df_left = constructor({"a": [1, 2, None], "x": [10, 20, 30]}).lazy() + df_right = constructor({"a": [1, None], "y": [1.2, 3.4]}).lazy() result = df_left.join(df_right, on="a", how=how).sort("a", nulls_last=True) assert_equal_data(result, expected) @@ -920,8 +936,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/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/frame/sample_test.py b/tests/frame/sample_test.py index 40d38ce701..7ef63a04f3 100644 --- a/tests/frame/sample_test.py +++ b/tests/frame/sample_test.py @@ -56,9 +56,7 @@ def test_sample_raises( 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) @@ -67,11 +65,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 892559b99e..0246904493 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"] @@ -581,7 +581,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 @@ -591,8 +591,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"], @@ -606,7 +606,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 473b685c19..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 constructor_eager.__name__.startswith("pandas"): - expected = cast("pd.DataFrame", constructor_eager(data)) + if str(constructor_eager).startswith("pandas"): + 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/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 115154ea61..c18f860872 100644 --- a/tests/hypothesis/getitem_test.py +++ b/tests/hypothesis/getitem_test.py @@ -7,23 +7,24 @@ from hypothesis import assume, given import narwhals as nw -from tests.conftest import pandas_constructor, pyarrow_table_constructor +from narwhals.testing.constructors import get_backend_constructor from tests.utils import assert_equal_data if TYPE_CHECKING: - from collections.abc import Callable, Sequence + from collections.abc import Sequence - from narwhals.typing import IntoDataFrame + from narwhals.testing.typing import DataFrameConstructor pytest.importorskip("pandas") pytest.importorskip("polars") import polars as pl -@pytest.fixture(params=[pandas_constructor, pyarrow_table_constructor], scope="module") -def pandas_or_pyarrow_constructor( - request: pytest.FixtureRequest, -) -> Callable[[Any], IntoDataFrame]: +@pytest.fixture( + 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] @@ -117,7 +118,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: Any, selector: Any) -> None: +def test_getitem( + pandas_or_pyarrow_constructor: DataFrameConstructor, 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 +128,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_pyarrow and isinstance(selector, slice) and selector.step is not None ) @@ -134,7 +137,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_pyarrow and isinstance(selector, tuple) and ( (isinstance(selector[0], slice) and selector[0].step is not None) @@ -155,7 +158,7 @@ def test_getitem(pandas_or_pyarrow_constructor: Any, selector: Any) -> None: # 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 14a93c8ef8..a03ef07fba 100644 --- a/tests/ibis_test.py +++ b/tests/ibis_test.py @@ -1,30 +1,14 @@ 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 tests.utils 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 get_backend_constructor -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: + ibis_constructor = get_backend_constructor("ibis") + 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/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/namespace_test.py b/tests/namespace_test.py index 97b5670b7b..26f96f890c 100644 --- a/tests/namespace_test.py +++ b/tests/namespace_test.py @@ -74,7 +74,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 70e7d06e24..546b388f67 100644 --- a/tests/preserve_pandas_like_columns_name_attr_test.py +++ b/tests/preserve_pandas_like_columns_name_attr_test.py @@ -7,13 +7,11 @@ import narwhals as nw if TYPE_CHECKING: - from collections.abc import Callable - - 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( @@ -24,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/read_scan_test.py b/tests/read_scan_test.py index 532502691c..372779f93e 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") @@ -31,7 +26,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" ) diff --git a/tests/series_only/hist_test.py b/tests/series_only/hist_test.py index 183c0a13ff..7db42c31bc 100644 --- a/tests/series_only/hist_test.py +++ b/tests/series_only/hist_test.py @@ -11,11 +11,14 @@ 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 DataFrameConstructor + + rnd = Random(0) # noqa: S311 data: dict[str, Any] = { @@ -43,7 +46,8 @@ 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", ["pandas", "polars[eager]", "pyarrow"]) + SHIFT_BINS_BY = 10 """shift bins property""" @@ -63,34 +67,14 @@ ], ids=str, ) -@param_library def test_hist_bin( - library: str, + nw_dataframe: DataFrameConstructor, 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 - - 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} @@ -115,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: @@ -130,25 +112,13 @@ def test_hist_bin( @pytest.mark.parametrize("params", counts_and_expected) @param_include_breakpoint -@param_library def test_hist_count( - library: str, *, params: dict[str, Any], include_breakpoint: bool + nw_dataframe: DataFrameConstructor, + *, + 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 - 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"] @@ -168,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: @@ -186,22 +154,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 +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]} @@ -229,23 +184,12 @@ 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 - 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 @@ -262,21 +206,8 @@ 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 - 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 @@ -284,11 +215,11 @@ def test_hist_small_bins(library: str) -> 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]) @@ -323,33 +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_library @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] + nw_dataframe: DataFrameConstructor, 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 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() @@ -376,28 +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_library @pytest.mark.slow def test_hist_count_hypothesis( - library: str, 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 - 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 - 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) @@ -418,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/is_sorted_test.py b/tests/series_only/is_sorted_test.py index 046669aac0..4efddf542f 100644 --- a/tests/series_only/is_sorted_test.py +++ b/tests/series_only/is_sorted_test.py @@ -16,7 +16,7 @@ ) def test_is_sorted( constructor_eager: ConstructorEager, - input_data: str, + input_data: list[int], descending: bool, # noqa: FBT001 expected: bool, # noqa: FBT001 ) -> None: 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 c1b3b4e357..3e9d453592 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.typing import IntoSchema - from tests.conftest import Data + from narwhals.testing.typing import Data + from narwhals.typing import IntoFrame, 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 = constructor({"a": [1, 2, 3]}) + frame: IntoFrame = constructor({"a": [1, 2, 3]}).to_native() msg = re.escape( "Expected `narwhals.DataFrame` or `narwhals.LazyFrame` instance, found" ) @@ -40,11 +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( - nw.from_native(pd.DataFrame({"a": [1]})), - nw.from_native(pa.table({"a": [1]})), # type: ignore[type-var] # pyright: ignore[reportArgumentType] - ) + assert_frame_equal(left, right) # type: ignore[type-var] # pyright: ignore[reportArgumentType] def test_check_same_input_type(constructor_eager: ConstructorEager) -> None: diff --git a/tests/testing/assert_series_equal_test.py b/tests/testing/assert_series_equal_test.py index 778a314c80..f42ef1de73 100644 --- a/tests/testing/assert_series_equal_test.py +++ b/tests/testing/assert_series_equal_test.py @@ -14,8 +14,8 @@ from collections.abc import Callable from typing 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]]] @@ -407,7 +407,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 new file mode 100644 index 0000000000..20b8b31af2 --- /dev/null +++ b/tests/testing/constructors_test.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +import narwhals as nw +from narwhals._utils import Implementation +from narwhals.testing.constructors import ( + available_backends, + get_backend_constructor, + prepare_backends, +) + +if TYPE_CHECKING: + from typing import TypeAlias + + PropertyName: TypeAlias = str + TrueNames: TypeAlias = set[str] + FalseNames: TypeAlias = set[str] + + +def test_eager_returns_eager_frame() -> None: + c = get_backend_constructor("pandas") + if not c.is_available: + pytest.skip() + + df = c({"x": [1, 2, 3]}, nw) + assert isinstance(df, nw.DataFrame) + + +def test_lazy_returns_lazy_frame() -> None: + c = get_backend_constructor("polars[lazy]") + if not c.is_available: + pytest.skip() + + lf = c({"x": [1, 2, 3]}, nw) + assert isinstance(lf, nw.LazyFrame) + + +_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"}), + ("nan_is_null", {"pandas", "modin", "dask"}, {"polars[eager]"}), +] + + +@pytest.mark.parametrize(("prop", "true_names", "false_names"), _IS_PROPERTY_CASES) +def test_constructor_is_properties( + prop: str, true_names: TrueNames, false_names: FalseNames +) -> None: + for name in true_names: + c = get_backend_constructor(name) + assert getattr(c, prop), f"{name}.{prop} should be True" + for name in false_names: + c = get_backend_constructor(name) + assert not getattr(c, prop), f"{name}.{prop} should be False" + + +def test_constructor_implementation() -> None: + assert get_backend_constructor("pandas").implementation is Implementation.PANDAS + assert ( + 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_backend_constructor("pandas") + c2 = get_backend_constructor("pandas") + assert c1.identifier == "pandas" + assert c1 == c2 + assert hash(c1) == hash(c2) + assert c1 != get_backend_constructor("polars[eager]") + assert c1 != "not a constructor" + + +def test_get_backend_constructor_invalid_name() -> None: + with pytest.raises(ValueError, match="Unknown constructor"): + get_backend_constructor("not_a_backend") + + +@pytest.mark.parametrize( + ("include", "exclude", "expected"), + [ + (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_backends( + include: list[str] | None, exclude: list[str] | None, expected: frozenset[str] +) -> None: + for name in (*(include or ()), *(exclude or ())): + if not get_backend_constructor(name).is_available: + pytest.skip(f"{name} not installed") + result = prepare_backends(include=include, exclude=exclude) + assert {c.name for c in result} == expected + + +@pytest.mark.parametrize("kwarg", ["include", "exclude"]) +def test_prepare_backends_unknown_name_raises(kwarg: str) -> None: + with pytest.raises(ValueError, match="not known constructors"): + prepare_backends(**{kwarg: ["not_a_backend"]}) diff --git a/tests/testing/plugin_test.py b/tests/testing/plugin_test.py new file mode 100644 index 0000000000..e8b4ba5ff6 --- /dev/null +++ b/tests/testing/plugin_test.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from unittest import mock + +import pytest + +from narwhals.testing.pytest_plugin import _default_backend_ids + +pytest_plugins = ["pytester"] + + +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 + from narwhals.testing.typing import DataFrameConstructor + + def test_shape(nw_dataframe: DataFrameConstructor) -> None: + df = nw_dataframe({"x": [1, 2, 3]}, namespace=nw) + assert df.shape == (3, 1) + """) + result = pytester.runpytest_subprocess( + "-v", "-p", "no:randomly", "--nw-backends=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: + pytest.importorskip("pandas") + pytest.importorskip("polars") + pytest.importorskip("duckdb") + + pytester.makeconftest("") + pytester.makepyfile(""" + import narwhals as nw + from narwhals.testing.typing import FrameConstructor + + def test_columns(nw_frame: FrameConstructor) -> None: + df = nw_frame({"x": [1, 2, 3]}, namespace=nw) + assert df.collect_schema().names() == ["x"] + """) + result = pytester.runpytest_subprocess( + "-v", "--nw-backends=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 DataFrameConstructor + + def test_unparam(nw_dataframe: DataFrameConstructor) -> None: + pass + """) + result = pytester.runpytest_subprocess("--use-external-nw-backend") + # Without external parametrisation in place, the fixture is missing. + 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]"} + ): + assert _default_backend_ids() == ["pandas", "polars[eager]"] diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 9f87b5220b..6c72eb9fa6 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 import get_backend_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 = sqlframe_pyspark_lazy_constructor(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) # pyright: ignore[reportArgumentType, reportCallIssue] # pyrefly: ignore[no-matching-overload] + nw.from_native(df, series_only=True) # type: ignore[call-overload] # pyrefly: ignore[no-matching-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 = sqlframe_pyspark_lazy_constructor(data) + df = get_backend_constructor("sqlframe")(data, nw).to_native() with context: res = nw.from_native(df, eager_only=eager_only) @@ -509,7 +509,7 @@ def test_from_native_roundtrip_identity(native: Any, kwds: dict[str, Any]) -> No def test_pyspark_connect_deps_2517(constructor: Constructor) -> None: # pragma: no cover if not ("pyspark" in str(constructor) and "sqlframe" not in str(constructor)): - # Only run this slow test if `--constructors=pyspark` is passed + # Only run this slow test if `--nw-backends=pyspark` is passed return pytest.importorskip("pyspark") # Don't delete this! It's crucial for the test that @@ -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/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) diff --git a/tests/utils.py b/tests/utils.py index 4e4b4e133f..ffa167d5f7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,10 +4,9 @@ import os import sys import warnings -from collections.abc import Callable from datetime import date, datetime from pathlib import Path -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any import pytest @@ -16,16 +15,25 @@ 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, +) + if TYPE_CHECKING: from collections.abc import Mapping, Sequence from typing import TypeAlias import pandas as pd - from pyspark.sql import SparkSession - from sqlframe.duckdb import DuckDBSession - from narwhals._native import NativeLazyFrame - from narwhals.typing import Frame, IntoDataFrame, TimeUnit + 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", "ConstructorPandasLike") def get_module_version_as_tuple(module_name: str) -> tuple[int, ...]: @@ -45,11 +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") -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" """`DType`s which **cannot** be used as bare types.""" @@ -176,34 +179,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 ( - # Don't remove pyrefly-ignore, needed in CI when pyspark is installed. - builder.config("spark.default.parallelism", "1") # pyrefly: ignore[bad-return] - .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: @@ -233,10 +208,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 b7319cda6f..fb45f129fd 100644 --- a/tests/v1_test.py +++ b/tests/v1_test.py @@ -338,11 +338,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: @@ -479,7 +479,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 = ( @@ -489,7 +489,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) @@ -917,7 +917,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 @@ -926,7 +926,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) @@ -1186,7 +1186,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) @@ -1194,7 +1194,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) @@ -1203,7 +1203,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()) @@ -1226,7 +1226,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()) @@ -1234,7 +1234,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 42ecb790a3..083d73a22a 100644 --- a/tests/v2_test.py +++ b/tests/v2_test.py @@ -367,7 +367,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 @@ -523,7 +523,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) @@ -531,7 +531,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) @@ -540,7 +540,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()) @@ -555,7 +555,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()) @@ -563,7 +563,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() 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, 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", + }, } 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) diff --git a/uv.lock b/uv.lock index e70b975a5f..fe3a987491 100644 --- a/uv.lock +++ b/uv.lock @@ -321,105 +321,100 @@ wheels = [ [[package]] name = "coverage" -version = "7.14.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b4/91/0a7c28934e50d8ac9a7b117712d176f2953c3170bccced5eaacfa3e96175/coverage-7.14.3.tar.gz", hash = "sha256:1a7563a443f3d53fdeb040ec8c9f7466aed7ca3dc5891aa09d3ca3625fa4387f", size = 924398, upload-time = "2026-06-22T23:10:25.584Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/bd/b01188f0de73ee8b6597cf20c63fccd898ad31405772f15165cb61a62c00/coverage-7.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:360bec1f58e7243e3405d3bdf7a1a8115aa9b448d54dc7cd6f7b7e0e9406b62e", size = 220378, upload-time = "2026-06-22T23:07:38.925Z" }, - { url = "https://files.pythonhosted.org/packages/33/eb/f7aa3cb46500b709070c8d12335446971ec8b8c2ea155fea05d2000b4b1f/coverage-7.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed68faa5e85de2f3e400bc3f122e5c82735a58c8bb24b9f63a2215954ba17b2d", size = 220895, upload-time = "2026-06-22T23:07:41.536Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c0/b41b8499fc9060ca40ad2a197d301155be1ead398f0f0bfdb27b2b4a660f/coverage-7.14.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:830c1fca669c572dec37ce9c838224ee45aac5be0f6961edf871e82e49d6537c", size = 247631, upload-time = "2026-06-22T23:07:43.244Z" }, - { url = "https://files.pythonhosted.org/packages/da/bb/e9ecea1307c6a549c223842cccbd5d55193cc27b82f26338782d4355047c/coverage-7.14.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a64caee2193563601dbaaa55fe2dcf597debef04a2f8f1fa8a07aa4bb7ac7a1e", size = 249460, upload-time = "2026-06-22T23:07:45.147Z" }, - { url = "https://files.pythonhosted.org/packages/59/cb/3821542809b7b726296fd364ed1c23d10a5770f1469957010c3b4bc5d408/coverage-7.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0096fd7559178f0cc9cf088f2dbd2a02ef85bacaa69732c633517286b4494610", size = 251324, upload-time = "2026-06-22T23:07:46.875Z" }, - { url = "https://files.pythonhosted.org/packages/76/27/f34f66f0ff152189ccc7b3f0582cf7909e239cb3b8c214362ed2149719b8/coverage-7.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6197e5a00183c11a8ce7c6abd18be1a9189fd8399084ffc95196f4f0db4f2137", size = 253237, upload-time = "2026-06-22T23:07:48.352Z" }, - { url = "https://files.pythonhosted.org/packages/22/81/aa363fa95d14fc892bd5de80edadc8d7cce584a0f6376f6336e492618e67/coverage-7.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7dfe427045520d6abca33687dfef767b4f635015893a1816c5decb12eb72ce18", size = 248344, upload-time = "2026-06-22T23:07:49.896Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/dc8a149441a3fea611cbbaf46bb12099adbe08f69903df1794581b0504b8/coverage-7.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9a3f142070eb7b82fc4085a55d887396f9c4e21250bccebe2ba22502c45b9647", size = 249365, upload-time = "2026-06-22T23:07:51.464Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a2/0004127deee122e020be24a4d86ce72fa14ae28198811b945aabf91293b5/coverage-7.14.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64b2055bb6e0dc945af35cdeceb3633e6ed9273475ef3af85592410fd6803803", size = 247369, upload-time = "2026-06-22T23:07:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/1e/72/3654c004f4df4f0c5a9643d9abaed5b26e5d3c1d0ecabe788786cb425efa/coverage-7.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1551b4caac3e3ec9f2bfcec6bf3776e01c0edbdd2e240431a50ca1a1aac72c27", size = 251182, upload-time = "2026-06-22T23:07:54.789Z" }, - { url = "https://files.pythonhosted.org/packages/a5/2f/7bdcdf1e7c4d0632648852768063c25582a0a747bb5f8036a04e211e7eb7/coverage-7.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:583d50d59142f8549470bd6390471d0fe8b8c8d69d6a0f28ac71e05380cef640", size = 247639, upload-time = "2026-06-22T23:07:56.254Z" }, - { url = "https://files.pythonhosted.org/packages/03/dc/0e01b071f69021d262a51ce39345dd6bc194465db0acfc7b34fd89e6b787/coverage-7.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0bb8a6bc7015efdf8a928753b25da1b9ca2d6f24ef04d2ee0688e486f32aae7", size = 248242, upload-time = "2026-06-22T23:07:57.692Z" }, - { url = "https://files.pythonhosted.org/packages/1c/51/08279e6ebe3479bf705db5fdc1a968e44ba1567e4cbc567f76b45f5e646e/coverage-7.14.3-cp310-cp310-win32.whl", hash = "sha256:d48400185564042287dc487c1f016a3397f18ab4f4c5d5ec36edc218f7ffa35b", size = 222431, upload-time = "2026-06-22T23:07:59.094Z" }, - { url = "https://files.pythonhosted.org/packages/40/2f/5c56670781fee5722ef0c415a74750c9a033bfacdb9d07b1493a0308108d/coverage-7.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:eadea7aba74e40adee867a8c0eec17b820b061d308a4b014f7a0e118c2b0aa61", size = 223059, upload-time = "2026-06-22T23:08:00.662Z" }, - { url = "https://files.pythonhosted.org/packages/f1/24/efb17eb94018dd3415d0e8a76a4786a866e8964aa9c50f033399d23939c2/coverage-7.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e574801e1d643561594aa021206c46d80b257e9853087090ba97bed8b0a509d3", size = 220501, upload-time = "2026-06-22T23:08:02.182Z" }, - { url = "https://files.pythonhosted.org/packages/76/93/32f1bfca6cdd34259c8af42820a034b7a28dfb44969a13ed38c17e0ba5b0/coverage-7.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f82b6bb7d75a2613e85d07cefa3a8c973d0544a8993337f6e2728e4a1e94c305", size = 221008, upload-time = "2026-06-22T23:08:03.701Z" }, - { url = "https://files.pythonhosted.org/packages/eb/88/0d0f974855ff905d15a64f7873d00bdc4182e2736267486c6634f4af293c/coverage-7.14.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2335ea5fed26af2e831094964fa3f8fae60b45f7e37fcc2d3b615b2add3ad87", size = 251420, upload-time = "2026-06-22T23:08:05.211Z" }, - { url = "https://files.pythonhosted.org/packages/39/7f/117dd2ec65e4140576f8ef991d88220f9b806769f7a8c20e0550c0f924e2/coverage-7.14.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbb8c3a98e779013786ae01d229662aeacbc77100efbd3f2f245219ace5af700", size = 253331, upload-time = "2026-06-22T23:08:06.672Z" }, - { url = "https://files.pythonhosted.org/packages/87/55/f0bd6d6538e3f16829fb8a44b6c0d2fe9da638bbfdd6a20f8b5da8f4fa81/coverage-7.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac082660de8f429ba0ea363595abb838998570b9a7546777c60f413ab902bbde", size = 255441, upload-time = "2026-06-22T23:08:08.208Z" }, - { url = "https://files.pythonhosted.org/packages/1e/98/aa71f7879019c846a8a9662579ea4484b0202cf1e252ffeed647075e7eca/coverage-7.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac012839ff7e396030f1e94e10553a431d14e4de2ab65cb3acb72bbd5628ca2", size = 257398, upload-time = "2026-06-22T23:08:09.749Z" }, - { url = "https://files.pythonhosted.org/packages/f3/4f/5fd367e59844190f5965015d7bee899e67a89d13eb2760118479bf836f2f/coverage-7.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5952f8c1bda2a5347154450379316e6dfa4d934d62ca35f6784451e6f55074fb", size = 251558, upload-time = "2026-06-22T23:08:11.37Z" }, - { url = "https://files.pythonhosted.org/packages/8f/de/5383a6ee5a6376701fe07d980fa8e4a66c0c377fead16712720340d701a3/coverage-7.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8cf0f2509acb4619e2471a1951089054dd58ebea7a912066d2ea56dd4c24ca4a", size = 253134, upload-time = "2026-06-22T23:08:13.04Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/09542b1a99f788e3daec7f0fadc288821e71aca9ea298d51bfa1ba79fed5/coverage-7.14.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2e41fd3aab806770008279a93879b0924b16247e09ab537c043d08bbca53b4ab", size = 251195, upload-time = "2026-06-22T23:08:14.606Z" }, - { url = "https://files.pythonhosted.org/packages/02/9d/722fe8c13f0fbb064491b9e8656e56a606286792e5068c47ca1042e773e8/coverage-7.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f0a47095963cfe054e0df178daca95aec21e680d6076da807c3add28dfe920f7", size = 254959, upload-time = "2026-06-22T23:08:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/fb/58/943627179ff1d82da9e54d0a5b0bb907bb19cf19515599ccd921de50b469/coverage-7.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a090cbf9521e78ffdb2fcf448b72902afe9f5923ff6a12d5c0d0120200348af9", size = 250914, upload-time = "2026-06-22T23:08:18.03Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d4/803efcbf9ae5567454a0c71e983589529448e2704ee0da2dc0163d482f18/coverage-7.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d310baf69a4fbe8a098ce727e4808a34866ac718a6f759ae659cbd3221358bc", size = 251824, upload-time = "2026-06-22T23:08:19.704Z" }, - { url = "https://files.pythonhosted.org/packages/32/79/3f78ea9563132746eed5cecb75d2e576f9d8fec45a47242b5ae0950b82a3/coverage-7.14.3-cp311-cp311-win32.whl", hash = "sha256:74fdd718d88fe144f4579b8747873a07ec3f04cb837d5faec5a25d9e22fa31a8", size = 222594, upload-time = "2026-06-22T23:08:21.311Z" }, - { url = "https://files.pythonhosted.org/packages/85/22/9ebbc5a2ab42ac5d0eea1f48648629e1de9bbe41ec243ed6b93d55a5a53f/coverage-7.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:cc96aa922e21d4bc5d5ed3c915cef27dfcbc13686f47d5e378d647fbfba655a2", size = 223073, upload-time = "2026-06-22T23:08:23.318Z" }, - { url = "https://files.pythonhosted.org/packages/71/af/69d5fcc16cb555153f99cec5467922f226be0369f7335a9506856d2a7bd0/coverage-7.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:c66f9f9d4f1e9712eb9b1de5310f881d4e2188cfcba5065e1a8490f38687f2c4", size = 222617, upload-time = "2026-06-22T23:08:25.054Z" }, - { url = "https://files.pythonhosted.org/packages/bd/b0/8a911f6ffe6974dac4df95b468ab9a2899d0e59f0f99a489afeec39f00bc/coverage-7.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d74ff26299c4879ce3a4d826f9d3d4d556fd285fde7bbce3c0ef5a8ab1cec24", size = 220672, upload-time = "2026-06-22T23:08:26.621Z" }, - { url = "https://files.pythonhosted.org/packages/36/16/0fc0cb52538783dbbae0934b834f5a58fd5354380ee6cad4a07b15dc845d/coverage-7.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:96150a9cf3468ea20f0bc5d0e21b3df8972c31480ef90fa7614b773cc6429665", size = 221035, upload-time = "2026-06-22T23:08:28.372Z" }, - { url = "https://files.pythonhosted.org/packages/77/e2/421ccfbb48335ac49e93301478cf5d623b0c2bf1c0cadd8e2b2fc6c0c710/coverage-7.14.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:27d07a46500ba23515b838dbcf52512026af04090755cf6cc64166d88c9b9a1a", size = 252540, upload-time = "2026-06-22T23:08:30.226Z" }, - { url = "https://files.pythonhosted.org/packages/06/c2/05b8c890097c61a7f4406b35396b997a635200ded0339eda83dfbe526c5f/coverage-7.14.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:621e13c6108234d7960aaf5762ab5c3c00f33c30c15af06dcbff0c73bf112727", size = 255274, upload-time = "2026-06-22T23:08:31.876Z" }, - { url = "https://files.pythonhosted.org/packages/dc/be/b6d9efe447f8ba3c3c854195f326bd64c54b907d936cd2fdebf8767ec72e/coverage-7.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b60ca6d8af70473491a15a343cbabab2e8f9ea66a4376e81c7aa24876a6f977", size = 256389, upload-time = "2026-06-22T23:08:33.843Z" }, - { url = "https://files.pythonhosted.org/packages/d4/3c/f26e50acc429e608bc534ac06f0a3c169019c798178ec5e9de3dbc0df9c9/coverage-7.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c90a7cdd5e380e1ce02f19792e2ac2fbfbf177e35a27e69fd3e873b30d895c0c", size = 258648, upload-time = "2026-06-22T23:08:35.481Z" }, - { url = "https://files.pythonhosted.org/packages/9e/a2/01c1fabf816c8e1dae197e258edf878a3d3ddc86fbda34b76e5794277d8f/coverage-7.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d788e5fd55347eef06ca0732c77d04a264de67e8ff24631270cdff3767a60cf", size = 252949, upload-time = "2026-06-22T23:08:37.562Z" }, - { url = "https://files.pythonhosted.org/packages/89/c6/941166dd79c31fd44a13063780ae8d552eee0089a0a0930b9bdb7df554ed/coverage-7.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62c7f79db2851c95ef020e5d28b97afde3daf9f7febcd35b53e05638f729063f", size = 254310, upload-time = "2026-06-22T23:08:39.174Z" }, - { url = "https://files.pythonhosted.org/packages/10/31/80b1fd028201a961033ce95be3cd1e39e521b3762e6b4a1ac1616cb291e7/coverage-7.14.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:90f7608aeb5d9b60b523b9fb2a4ee1973867cc4865a3f26fe6c7577073b70205", size = 252453, upload-time = "2026-06-22T23:08:40.84Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/c3d9addd94c4b524f3f4af0232075f5fe7170ce99a1386edff803e5934db/coverage-7.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e3b91f9c4740aeb571ecf82e5e8d8e4ab62d34fcb5a5d4e5baa38c6f7d2857c", size = 256522, upload-time = "2026-06-22T23:08:42.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/14/e5a0575f73795af3a7a9ae13dadf812e17d32422896839987dc3f86947e1/coverage-7.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c946099774a7699de03cbd0ff0a64e21aed4525eed9d959adde4afe6d15758ef", size = 252023, upload-time = "2026-06-22T23:08:44.243Z" }, - { url = "https://files.pythonhosted.org/packages/38/9b/9652ee531937ce3b8a63a8896885b2b4a2d56adc30e53c9540c666286d88/coverage-7.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16b206e521feb8b7133a45754643dead0538489cf8b783b90cf5f4e3299625fd", size = 253893, upload-time = "2026-06-22T23:08:46.113Z" }, - { url = "https://files.pythonhosted.org/packages/b1/05/42678841c8c38e4b08bdfc48269f5a16dfbf5806000fe6a89b4cece3c691/coverage-7.14.3-cp312-cp312-win32.whl", hash = "sha256:ea3169c7116eb6cdf7608c6c7da9ecfcb3da40688e3a510fac2d1d2bafd6dc35", size = 222734, upload-time = "2026-06-22T23:08:47.858Z" }, - { url = "https://files.pythonhosted.org/packages/df/87/07a4fcee55177a25f1b52331a8e92cf4f2c53b1a9c75ce2981fd59c684ad/coverage-7.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:7ea52fc08f007bcc494d4bb3df3851e95843d881860ba38fe2c64dc100db5e7d", size = 223266, upload-time = "2026-06-22T23:08:49.494Z" }, - { url = "https://files.pythonhosted.org/packages/aa/34/2b8b66a989282ea7b370beb49f50bab29470dc30bb0b03935b6b802782f7/coverage-7.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:8cec0ad652ec57790970d817490105bd917d783c2f7b38d6b58a0ca312e1a336", size = 222655, upload-time = "2026-06-22T23:08:51.766Z" }, - { url = "https://files.pythonhosted.org/packages/a9/83/7fefbf5df23ed2b7f489907564a7b34b9b07098128e12e0fdfa92626e456/coverage-7.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47968988b367990ae4ab17523790c38cd125e02c6bfd379b6022be2d40bdc38c", size = 220699, upload-time = "2026-06-22T23:08:53.522Z" }, - { url = "https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee68f5c34812780f3a7063382c0a9fcbb99985b7ddcdcaa626e4f3fb2e0783a", size = 221068, upload-time = "2026-06-22T23:08:55.571Z" }, - { url = "https://files.pythonhosted.org/packages/20/86/4f5c45d51c5cd10a128933f0fd235393c9146abbfd2ce2dfa68b3267ead3/coverage-7.14.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa9e5c6857a7e80fa22ace5cf3550ae392bbfc322f1d8dd2d2d5a8be38cec027", size = 252060, upload-time = "2026-06-22T23:08:57.464Z" }, - { url = "https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98a0859b0e98e43e1178a9402e19c8127766b14f7109a374d976e5a62c0e5c73", size = 254657, upload-time = "2026-06-22T23:08:59.453Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d2/639ceb1bc8038fd0d66768278d5dc22df3391918b8278c2a21aa2602a531/coverage-7.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69918344541ed9c8368566c2adc03c0e33d4550d7faa87d1b35e49b6a3286ea9", size = 255892, upload-time = "2026-06-22T23:09:01.291Z" }, - { url = "https://files.pythonhosted.org/packages/8b/96/002094a10e113512500dc1e10430a449417e17b0f90f7d496bcb820208b7/coverage-7.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f300ac92cd4b570724c8ffbbd0c130fee298d2447f41d5a3abf58976fae1de", size = 258026, upload-time = "2026-06-22T23:09:03.017Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ec/286a5d2fad9c4bee59bd724feeb7d5bf8303c6c9200b51d1dd945a9c72b0/coverage-7.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a7ec9f97ab950f4c5af62229befc7faf208fdbc0116d3902d7e306cf2c5abd", size = 252285, upload-time = "2026-06-22T23:09:04.773Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7d/a17753a0b12dd48d0d50f5fab079ad99d3be1eac790494d89f3a417ca0b9/coverage-7.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a571bd889cd36c5922ce8e42e059f9d37d02301531d11374afa4c87a578625d5", size = 254023, upload-time = "2026-06-22T23:09:06.513Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/a76c6ceba6a2c313f905310abf2701d534cada22d372db11731831e9e209/coverage-7.14.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de76caefc8deabb0dd1678b6a980be97d14c8d87e213ac194dbf8b09e96d63fb", size = 251989, upload-time = "2026-06-22T23:09:08.382Z" }, - { url = "https://files.pythonhosted.org/packages/d9/39/353013a75fec0fb49f7553519f9d52b4441e902e5178c93f38eb6c07cedb/coverage-7.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d20a15c622194234161535459affa8f7905830391c9ccfa060d495dbfe3a1c7f", size = 256144, upload-time = "2026-06-22T23:09:10.369Z" }, - { url = "https://files.pythonhosted.org/packages/29/0e/613878555d734def11c5b20a2701a15cb3781b9e9ea749da27c5f436e928/coverage-7.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b488bd4b23397db62e7a9459129d01ff06a846582a732efd24834b24a6ada498", size = 251808, upload-time = "2026-06-22T23:09:12.057Z" }, - { url = "https://files.pythonhosted.org/packages/af/76/359c058c9cfdcf1e8b107663881225b03b364a320017eda24a2a66e55102/coverage-7.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a3693b4153394d265f44fb855fdc80e72403024d4d6f91c4871b334d028e4e0", size = 253579, upload-time = "2026-06-22T23:09:13.858Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d9/4ba2f060933a30ebe363cef9f67a365b0a317e580c0d5d9169d56a73ef1c/coverage-7.14.3-cp313-cp313-win32.whl", hash = "sha256:338b19131ab1a6b767b462bfcbaa692e7ae22f24463e39d49b02a83410ff6b37", size = 222741, upload-time = "2026-06-22T23:09:15.636Z" }, - { url = "https://files.pythonhosted.org/packages/76/e8/196ebc25d8f34c06d43a6e9c8513c9266ef8dbf3b5672beb1a00cf5e29fa/coverage-7.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:b3d77f7f196abdef7e01415de1bce09f216189e83e58159cfeef2b92d0464994", size = 223283, upload-time = "2026-06-22T23:09:17.478Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/51d2aac6417523a286f10fb25f09eb9518a84df9f1151e93ff6871f34849/coverage-7.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:e6230e688c7c3e65cedd41a774eb4ec221adc6bfee13768231015b702d5e4150", size = 222678, upload-time = "2026-06-22T23:09:19.7Z" }, - { url = "https://files.pythonhosted.org/packages/61/56/14e3b97facbfa1304dd19e676e26599ad359f04714bed32f7f1c5a88efdc/coverage-7.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:605ab2b566a22bd94834529d66d295c364aba84afd3e5498285c7a524017b1fc", size = 220741, upload-time = "2026-06-22T23:09:21.616Z" }, - { url = "https://files.pythonhosted.org/packages/12/1d/db378b5cca433b90b893f26dab728b280ddd89f272a1fdfed4aeaa05c686/coverage-7.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3c2134809e80fac091bfed18a6991b5a5eb5df5ae32b17ac4f4f99864b73dd7", size = 221068, upload-time = "2026-06-22T23:09:23.452Z" }, - { url = "https://files.pythonhosted.org/packages/47/f0/3f8421b20d9c4fcd39be9a8ca3c3fda8bc204b44efbd09fede153afd3e2f/coverage-7.14.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c02efd507227bde9969cab0db8f48890eb3b5dcad6afac57a4792df4133543ce", size = 252117, upload-time = "2026-06-22T23:09:25.458Z" }, - { url = "https://files.pythonhosted.org/packages/27/ca/59ea35fb99743549ec8b37eff141ece4431fea590c89e536ed8032ef45cf/coverage-7.14.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5", size = 254622, upload-time = "2026-06-22T23:09:27.523Z" }, - { url = "https://files.pythonhosted.org/packages/c8/25/ec6de51ae7493b92a1cf74d1b763121c29636759167e2a593ba4db5881e4/coverage-7.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f502e948e03e866538048bba081c075caaa62e5bda6ea5b7432e45f587eb462a", size = 255968, upload-time = "2026-06-22T23:09:29.43Z" }, - { url = "https://files.pythonhosted.org/packages/5d/05/c8bfc77823f42b4664fb25842f13b567022f6f84a4c83c8ecbb16734b7cb/coverage-7.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9973ef2463f8e6cfb61a6324126bb3e17d67a85f22f58d856e583ea2e3ca6501", size = 258284, upload-time = "2026-06-22T23:09:31.397Z" }, - { url = "https://files.pythonhosted.org/packages/f6/15/1d1b242027124a32b26ef01f82018b8c4ef34ef174aa6aeba7b1eeef48e8/coverage-7.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9be4e7d4c5ca0427889f8f9d614bd630c2be741b1de7699bca3b2b6c0e41003e", size = 252143, upload-time = "2026-06-22T23:09:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/74/b6/d2a9842fd2a5d7d27f1ac851c043a734a494ad75402c5331db3da79ed691/coverage-7.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a574912f3bde4b0619f6e97d01aa590b70998859244793769eb3a6df78ee56d3", size = 253976, upload-time = "2026-06-22T23:09:35.351Z" }, - { url = "https://files.pythonhosted.org/packages/fd/30/e1600ddf7e226db5558bb5323d2186fff00f505c4b764643ec89ce5d8175/coverage-7.14.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e343fb086c9cd780b38622fea7c369acd64c1a0724312149b5d769c387a2b1f5", size = 251942, upload-time = "2026-06-22T23:09:37.313Z" }, - { url = "https://files.pythonhosted.org/packages/d9/2c/9159de64f9dd648e324328d588a44cfab1e331eb5259ce1141afe2a92dfb/coverage-7.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3c68df8e61f1e09633fefc7538297145623957a048534368c9d212782aa5e845", size = 256220, upload-time = "2026-06-22T23:09:39.165Z" }, - { url = "https://files.pythonhosted.org/packages/91/67/b7f536cc2c124f48e91b22fbb741d2261f4e3d310faf6f76007f47566e5d/coverage-7.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3e5b550a128419373c2f6cec28a244207013ef15f5cbcff6a5ca09d1dfaaf027", size = 251756, upload-time = "2026-06-22T23:09:41.056Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ec/f3718038e2d4860c715a55428377ca7f6c75872caf98cabd982e1d76967d/coverage-7.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2bfc4dd0a912329eccc7484a7d0b2a38032b38c40663b1e1ac595f10c457954b", size = 253413, upload-time = "2026-06-22T23:09:43.306Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a5/91f11efeef89b3cc9b30461128db15b0511ef813ab889a7b7ab636b3a497/coverage-7.14.3-cp314-cp314-win32.whl", hash = "sha256:0423d64c013057a06e70f070f073cec4b0cbc7d2b27f3c7007292f2ff1d52965", size = 222946, upload-time = "2026-06-22T23:09:45.261Z" }, - { url = "https://files.pythonhosted.org/packages/58/fd/98ac9f524d9ec378de831c034dbdeb544ca7ef7d2d9c9996daf232a037fd/coverage-7.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3", size = 223436, upload-time = "2026-06-22T23:09:47.177Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a0/7cd612d650a772a0ae80144443406bf61981c896c3d57c9e6e79fb2cdbd1/coverage-7.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:41de778bd41780586e2b04912079c73089ab5d839624e28db3bdb26de638da92", size = 222861, upload-time = "2026-06-22T23:09:49.384Z" }, - { url = "https://files.pythonhosted.org/packages/55/57/017353fab573779c0d00448e47d102edd36c792f7b6f233a4d89a7a08384/coverage-7.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8427f370ca67db4c975d2a26acfc0e5783ca0b52444dbc50278ace0f35445949", size = 221474, upload-time = "2026-06-22T23:09:51.417Z" }, - { url = "https://files.pythonhosted.org/packages/69/92/90cf1f1a5c468a9c1b7ba2716e0e205293ad9b02f5f573a6de4318b15ba1/coverage-7.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8e88f335544a47e22ae2e45b344772925ec65166555c958720d5ed971880891", size = 221738, upload-time = "2026-06-22T23:09:53.487Z" }, - { url = "https://files.pythonhosted.org/packages/a4/c0/4df964fa539f8399fd7679c09c472d73744de334686fd3f01e3a2465ce4e/coverage-7.14.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:beaab199b9e5ceaf5a225e16a9d4df136f2a1eae0a5c20de1e277c8a5225f388", size = 263101, upload-time = "2026-06-22T23:09:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/06/76/e5d33b2576ae3bf2be2058cd1cae57774b61e400f2c3c58f3783dc2ffb4a/coverage-7.14.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ff255799f5a1676c71c1c32ec01fd043aa09d57b3d95764b24992757184784", size = 265225, upload-time = "2026-06-22T23:09:57.904Z" }, - { url = "https://files.pythonhosted.org/packages/61/d2/e52419afe391a39ba27fdefaf0737d8e34bf03faef6ab3b3006545bbd0d0/coverage-7.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:878832eaac515b62decfa76965aed558775f86bf1fc8cca76993c0c84ae31aed", size = 267643, upload-time = "2026-06-22T23:09:59.938Z" }, - { url = "https://files.pythonhosted.org/packages/58/7a/f2625d8d5006b6b20fba5afaef00b24a763fe96476ea798a3076cbc1f84e/coverage-7.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:611e62cb9386096d81b63e0a05330750268617231e7bd598e1fe77482a2c58a5", size = 268762, upload-time = "2026-06-22T23:10:01.943Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bf/908024006bba57127354d74e938954b9c3cd765cc2e0412dc9c37b415cda/coverage-7.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:02c41de2a88011b893050fc9830267d927a50a215f7ad5ec17349db7090ccf26", size = 262208, upload-time = "2026-06-22T23:10:03.954Z" }, - { url = "https://files.pythonhosted.org/packages/34/a0/d4f9296441b909817442fdb26bd77a698f08272ec683a7394b00eb2e47a0/coverage-7.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:526ce9721116af23b1065089f0b75046fe521e7772ab94b641cd66b7a0421889", size = 265096, upload-time = "2026-06-22T23:10:05.936Z" }, - { url = "https://files.pythonhosted.org/packages/e8/da/4ae4f3f4e477b56a4ce1e5c48a35eff38a94b50130ce5bdc897024741cfc/coverage-7.14.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e4ed44705ca4bead6fc977a8b741f2145608289b33c8a9b42a95d0f15aedbf4d", size = 262699, upload-time = "2026-06-22T23:10:07.973Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7a/6927148073ff32856d78baa77b4ddc07a9be7e90020f9db0661c4ca523a1/coverage-7.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2415902f385a23dcc4ccd26e0ba803249a169af6a930c003a4c715eeb9a5444e", size = 266433, upload-time = "2026-06-22T23:10:10.145Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a7/774f658dbe9c4c3f5daa86a87e0459ac3832e4e3cc67affe078547f727b9/coverage-7.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b75ee850fc2d7c831e883220c445b035f2224de2ba6103f1e56dbd237ab913f7", size = 261547, upload-time = "2026-06-22T23:10:12.191Z" }, - { url = "https://files.pythonhosted.org/packages/3d/14/a0c18c0376c43cbf973f43ef6ca20019c950597180e6396232f7b6a27102/coverage-7.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc9b4e35e7c3920e925ba7f14886fd5fbe481232754624e832ddba66c7535635", size = 263859, upload-time = "2026-06-22T23:10:14.492Z" }, - { url = "https://files.pythonhosted.org/packages/10/ac/43a3d0f460af524b131a6191805bc5d18b806ab4e828fbf82e8c8c3af446/coverage-7.14.3-cp314-cp314t-win32.whl", hash = "sha256:7b27c822a8161afbe48e99f1adfb098d270ae7e0f7d7b0555ce110529bdb69cc", size = 223250, upload-time = "2026-06-22T23:10:16.758Z" }, - { url = "https://files.pythonhosted.org/packages/3f/5f/d5e5c56b0712e96ce8f69fe7dbf229ff938b437bc50862743c8a0d2cea84/coverage-7.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:39e1dbbb6ff2c338e0196a482558a792a1de3aa64261196f5cdb3da016ad9cda", size = 224082, upload-time = "2026-06-22T23:10:19.23Z" }, - { url = "https://files.pythonhosted.org/packages/62/35/947cbd5be1d3bcbbdc43d6791de8a56c6501903311d42915ae06a82815f0/coverage-7.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:68520c90babfa2d560eca6d497921ed3a4f469623bd709733124491b2aa8ef3f", size = 223400, upload-time = "2026-06-22T23:10:21.24Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e3/a0aa32bfa3a081951f60a23bc0e7b512891ef0eecda1153cf1d8ba36c6b1/coverage-7.14.3-py3-none-any.whl", hash = "sha256:fb7e18afb6e903c1a92401a2f0501ac277dca527bb9ca6fe1f691a8a0026a0e8", size = 212469, upload-time = "2026-06-22T23:10:23.405Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11' or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-dask') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-modin') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-pandas') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-dask' and extra == 'extra-8-narwhals-modin') or (extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'extra-8-narwhals-pandas') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-local-dev' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-min-pyspark-version' and extra == 'group-8-narwhals-typing')" }, +version = "7.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/97/c52dc440c390b6cfa87be9432b141a956e2d56d9b9f5fc8bd71c5f471722/coverage-7.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50913d4bf5ddafa6ca3693da5e4dd833dd1b772e0283c99ca7f7d287db67331a", size = 220539, upload-time = "2026-07-02T13:08:19.252Z" }, + { url = "https://files.pythonhosted.org/packages/3f/26/602de8c2aec7e2e3e99ebfb8e04ba65598f746275396eea5f6794ff4673f/coverage-7.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:359e141ccd33893ce3f1ad5525f8b96083003677c82182e5907d62d4ea5799fc", size = 221058, upload-time = "2026-07-02T13:08:21.013Z" }, + { url = "https://files.pythonhosted.org/packages/fc/13/ebab0743138891c1d646d61e247ec29639afcbb6c4e1905e6a0f0c75291a/coverage-7.15.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3200b6204935f928c64b2ca1f923ab8c1acb7c9de45ec61569711b34d25cccaf", size = 247797, upload-time = "2026-07-02T13:08:22.474Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/b6ffb9e042aa48dc4144a8a65529affaec8dca0685309353614a2a7386ad/coverage-7.15.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be616bf61346883b2cfdc5178669647e03531d81ab761a7e378558b7e8bcb628", size = 249626, upload-time = "2026-07-02T13:08:23.803Z" }, + { url = "https://files.pythonhosted.org/packages/9c/06/243ff05b652333d8e3d060c11223efc2723b19cacf6605e433fa686ab5d4/coverage-7.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7bafc3fe1059463a8fdd97ca79972d6e2bf819d775c7d54991b5b1971201d6", size = 251493, upload-time = "2026-07-02T13:08:25.397Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2b/867faa17030a806114dae388b32a3fa929d8cd4bf39226fbc11f6e6bb705/coverage-7.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b713aa7fcf325a01d4184d848acb46fd84f78fdb0978470c636b23a06a753d91", size = 253406, upload-time = "2026-07-02T13:08:26.842Z" }, + { url = "https://files.pythonhosted.org/packages/94/c0/d789ce18f6605afc4895db75723424be2ef494282f77f61d8e5832923183/coverage-7.15.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38e6fba2d56652fdfaf0231f8f78aeb805234a912de25dc291ee5cce5b8faa4", size = 248512, upload-time = "2026-07-02T13:08:28.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/b2673c30739f4a2e06649a0a38ad8b093c4d865462dc7bab0e9524a2c3b1/coverage-7.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:884499f42e382675be80770391983b90e0c0c774d87dbeeebf5f991cf6612b20", size = 249532, upload-time = "2026-07-02T13:08:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/acd79e9a41beabee92b623afe4f30b549916f48566271475f2907e752828/coverage-7.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:840481b12e083dbcbafab14794a8781a958edf327c8d3d70b4eee42f9b8253aa", size = 247537, upload-time = "2026-07-02T13:08:31.173Z" }, + { url = "https://files.pythonhosted.org/packages/12/d4/2d301c4d1b3238d7c88b70ab9d13fd53ed9505662a7ff1b46ba1e2e4e3c3/coverage-7.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:276646e9481703d09f854f3b2f018f24e19fd7049ae670a92570043eb97203b1", size = 251348, upload-time = "2026-07-02T13:08:32.63Z" }, + { url = "https://files.pythonhosted.org/packages/35/bb/c67708b2bc00f32e12805ec23d5fa677a0a51652f449341a89f9d6b1b715/coverage-7.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4de4b4d3f5545aa6c60dc4efd9c63b5b5dcc3bf00fe83146b2bdfffb8f6613bd", size = 247806, upload-time = "2026-07-02T13:08:33.931Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6c/57c4f653c47a6e917748f8938e389e72fbcae44e3643cd906664f0477a13/coverage-7.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5c504097b2a89b1e85bc6070d920df77daec701337e3aeef2c17775a5dd0ca90", size = 248410, upload-time = "2026-07-02T13:08:35.189Z" }, + { url = "https://files.pythonhosted.org/packages/6c/94/bb083041aef828903668f134273f319f2bd49224962875359c52faa5497f/coverage-7.15.0-cp310-cp310-win32.whl", hash = "sha256:f6e80ed91f98316e86b9c137206b04b2bcfbffccbdff49bd2eb09dddb1cf14e0", size = 222588, upload-time = "2026-07-02T13:08:36.486Z" }, + { url = "https://files.pythonhosted.org/packages/ef/94/a09d8ee618956f626741b0734854bac4425a00e10c0565f5abca64e7e751/coverage-7.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3b3e22030f3f6f5e01a5ce69936552a5c0f6992b7698777377b99041961031f", size = 223214, upload-time = "2026-07-02T13:08:37.885Z" }, + { url = "https://files.pythonhosted.org/packages/ae/23/82e910835ef4b8391047025e1d53aa48d66029f444eb8b25373c849bf503/coverage-7.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:003fff99412ea848c0aaebcc78ed2b6ce7d8a1227ed17e68470672770b78a02a", size = 220662, upload-time = "2026-07-02T13:08:39.205Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0d/c7b213dde2f1579de5231062b386d8413f79c11667eb58c39319b25991da/coverage-7.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cbd804bf2784ce7b45114516050f346ecd50f960c4bb630a7ee9e1d78fa2118", size = 221168, upload-time = "2026-07-02T13:08:40.471Z" }, + { url = "https://files.pythonhosted.org/packages/33/77/d000aeedfac085088337b3c7becdad328474b1f8a9e4c9368a0c99605d68/coverage-7.15.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8773e15c23305b58882a4611fb9b2755977eae0dc2e515366a1b6c98866cc4c2", size = 251587, upload-time = "2026-07-02T13:08:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e0/86787c56b9df17afd370d5e293515dd4d9a107a561d13054873eefad8ecc/coverage-7.15.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f50e40081494c1dc4239ebb202014cbcc3306ea96fb6302a34c8cc0967fc5ae8", size = 253497, upload-time = "2026-07-02T13:08:43.387Z" }, + { url = "https://files.pythonhosted.org/packages/3f/02/181bc917359299c07dead6270f94e411151c8b60cec905c33499da69afe6/coverage-7.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daf96f37f5fc3a7b6c6da862eb4aee61c426bd63da236ed4a73ef0e503b4bca5", size = 255607, upload-time = "2026-07-02T13:08:44.897Z" }, + { url = "https://files.pythonhosted.org/packages/b9/35/ca5e7427699913da6788c4f910e73ab16c5f4b59ec5d3a999dce2a45112f/coverage-7.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:51aa20f6ae2788fd197747766edf4cd8234fd9423309b934257fa6b21a592723", size = 257563, upload-time = "2026-07-02T13:08:46.334Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4d/b8220bacc2fc3c4e9078e27c32e99fb411479a4718a72bdd00036a9891c8/coverage-7.15.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03d1f922757662eb7af586e77834792274cff776bc7b1d1a0b66a49ea9d84735", size = 251726, upload-time = "2026-07-02T13:08:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e4/2e145da1991d72189b9c3cf7eca05c716ee7080d099aaea6757cfc7df008/coverage-7.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a6d6acc9a7666245e6133dd15144ca038a85a9cd5026bb06d6bbae9e77440dc9", size = 253301, upload-time = "2026-07-02T13:08:49.5Z" }, + { url = "https://files.pythonhosted.org/packages/72/28/d2c841d698bf762e481f08bd4839d370246b6d9b61dab085a7b20b201a08/coverage-7.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ac2c4c27c7df851dc9a017c2d7de00b69147e84ba3d96f37a530b0b6fb51035", size = 251361, upload-time = "2026-07-02T13:08:51.304Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ed/55d9ffde994fba3897c0c783f77a7d053b0c18787f6892ed5b0aed73f469/coverage-7.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b761a1d504fd4bd1f20f418753964dca9f5862a511fc854dac58296b3b223671", size = 255129, upload-time = "2026-07-02T13:08:52.661Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c0/ecbf33b8c460ea2718aeb813e2df8140d0370e5f67261c31524ceb0a2a8d/coverage-7.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e43b045e11c16e897895758ae90e4a90cf99e93d58549e2f90c0e2272e155695", size = 251081, upload-time = "2026-07-02T13:08:54.188Z" }, + { url = "https://files.pythonhosted.org/packages/a9/de/fb87b4261f54448dd2b9504ef19a58be42cef0d9520595fbfe1219b15234/coverage-7.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:589b54513e901739f4b4582c705ce96b80c96f57641b1464607e2367a270e540", size = 251988, upload-time = "2026-07-02T13:08:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/df/27/3494d5f291b9a4cb868f73c11221a8bd2d5bd761a8f9acea61ff57128dd1/coverage-7.15.0-cp311-cp311-win32.whl", hash = "sha256:106781b8482749162d0b47056937ba0933508e5d9447f65a5e7d5c422f0d6bb4", size = 222754, upload-time = "2026-07-02T13:08:57.091Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ee/cd4847ebc9be6a9c0123d763645a6f1f3be6b8c58c962706368b79cbac07/coverage-7.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:821e92b3631d762a339695824cadbbc73020354eba2a23a551a99ad34938fbe6", size = 223225, upload-time = "2026-07-02T13:08:58.594Z" }, + { url = "https://files.pythonhosted.org/packages/57/37/5011581aa7f2be498b97dcc7c9902192442a42f4f9a748aeadb3d6506b42/coverage-7.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:309990eb5fb8014b9f67cb211f7fd41876ec8a88a88d3ae76de0ed1d611e3640", size = 222774, upload-time = "2026-07-02T13:09:00.074Z" }, + { url = "https://files.pythonhosted.org/packages/2a/74/fd4c0901137c4f8d81a76ada99e43c65163b4c94a02ece107a4ec0c6b615/coverage-7.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b75ee5e8cb7575636ac598719b4307ac529ec8fcd79608a35c3cd4d4dada812d", size = 220838, upload-time = "2026-07-02T13:09:02.084Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2e/2347583467bd7f0402635101a916961915cc68fce652cd0db5f173ea04fc/coverage-7.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffb31267816b93b075302248cc1737506081b4f163df4401e9df1a6424aafabe", size = 221197, upload-time = "2026-07-02T13:09:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/f0/17/99fa688541ae1d6e84543a0e544f83de0c944815b63e9e7b1ed411d15036/coverage-7.15.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4d0bb73455bf97ab243a8f12c37c686ccf1c13bb614b7b85f1d062f06f42b2c", size = 252705, upload-time = "2026-07-02T13:09:05.059Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/6a95a5cd83b74839017ef9cf48d2d8c9ae60af919e17a3f336e6f9f1b7bd/coverage-7.15.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:20d9ccc4ebd0edc434d86dfd2a1dd2a8efa6b6b3073d0485a394fee86459ebb4", size = 255441, upload-time = "2026-07-02T13:09:06.559Z" }, + { url = "https://files.pythonhosted.org/packages/67/f2/406f6c57d600f68185942422c4c00f1a3255d60aee6e5fd961425cd9987e/coverage-7.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c8a976c365c8cb12f0cbd099508772ea41fb5fa80657a8506df0e11bd278c5", size = 256556, upload-time = "2026-07-02T13:09:08.197Z" }, + { url = "https://files.pythonhosted.org/packages/74/8e/d3fa48489c15ecdec1ba48fd61f68798555dddd2f6716f9ad42adeb1a2a9/coverage-7.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f948fd5ba1b9cbca91f0ae08b4c1ce2b139509149a435e2585d056d57d70bf01", size = 258815, upload-time = "2026-07-02T13:09:09.691Z" }, + { url = "https://files.pythonhosted.org/packages/47/2e/2d40ddd110462c6a2769677cf7f1c119a52b45f568978fc6c98e4cc0dd0f/coverage-7.15.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f58185f06edf6ad68ec9fb155d63ef650c82f3fbd7e1770e2867751fb13158f4", size = 253117, upload-time = "2026-07-02T13:09:11.212Z" }, + { url = "https://files.pythonhosted.org/packages/51/c0/310782f0d7c3cb2b5ac05ba8d205fe91f24a36f6bf3256098f1782181c38/coverage-7.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02adc79a920c73c647c5d117f55747df7f2de94571884758ce8bc58e04f0a796", size = 254475, upload-time = "2026-07-02T13:09:13.029Z" }, + { url = "https://files.pythonhosted.org/packages/86/f7/702da6c275f8ae6ade423d2877243122932c9b27f5403003b9ef8c927d12/coverage-7.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6eb7c300fbed667fd6e3588eba71c1904cdb06110ca6fdf908c26bdd88b8e382", size = 252619, upload-time = "2026-07-02T13:09:14.699Z" }, + { url = "https://files.pythonhosted.org/packages/fb/84/c5b15a7e5ecba4e56218d772d99fe80a63e63f8d11f12783723a6005ab45/coverage-7.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b5fb23fa2de9dce1f5c36c09066d8fcda16cd96e8e26686caa2d7cb9b567d65c", size = 256689, upload-time = "2026-07-02T13:09:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/95/2f/c8b07559b57701230c61b23a953858c052890c12ef568d81780c6c46e92e/coverage-7.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cec79341dbe6281484024979976d0c7f22beae08b4a254655decd25d42cbe766", size = 252189, upload-time = "2026-07-02T13:09:17.828Z" }, + { url = "https://files.pythonhosted.org/packages/6b/80/6d2f049dd3fd3dbfd60b62ba6b2162a04009e2c002ce70b24cf3878dec7a/coverage-7.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c664c5444b1d970b1b2a450e21fb19ee5c9cfdf151ded2dda37260031cca0da", size = 254059, upload-time = "2026-07-02T13:09:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/ce/92/b0287a2c42031d25c628f815f89a3cd9f8268ee78bb1252c9356cda1c689/coverage-7.15.0-cp312-cp312-win32.whl", hash = "sha256:5f764a3fa339bde6b3aa97657f5a6a3a9451e4a5b4ea98a2892c773a43525f77", size = 222893, upload-time = "2026-07-02T13:09:20.812Z" }, + { url = "https://files.pythonhosted.org/packages/a9/69/e34c481915fecb499b3146975061dac528752e37706edc1804f32c822469/coverage-7.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:52f9a4d2c4c56c8848bc2f524916698354b0211488b38c49ad9ae54f6cafbff6", size = 223429, upload-time = "2026-07-02T13:09:22.315Z" }, + { url = "https://files.pythonhosted.org/packages/fe/98/6e878f0b571d32684ef3f38d7c03db241ca5b82a5da8a5391596a8f209c4/coverage-7.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:31e5c3e70c85307ea35a12964e2e40f56ca2ee4b1c8c721ccf4609d17071080b", size = 222810, upload-time = "2026-07-02T13:09:23.812Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/145a3748098bcc86b631a85408d2c3dc5c104e0bd86d605468239b25b6c4/coverage-7.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5be4caf3b28836f078abe700f8944dac4a65d78f16d6c600c89cb624e5535782", size = 220863, upload-time = "2026-07-02T13:09:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5c/4ed55708fed2c64b63c9bc5715daef670872202101938869b7fe5d5fbb8f/coverage-7.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd58ad1404704303ca8d4f4b8a1095e7cbc7040ef17a66df1e6619aa10176430", size = 221230, upload-time = "2026-07-02T13:09:26.897Z" }, + { url = "https://files.pythonhosted.org/packages/7b/19/3a80b97d3b2a5c77a01ae359c6bed20c13738fe3d9380f08616d4fec0281/coverage-7.15.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bbcbb317c2e5ded5b21104af81c29f391be2af98d065693ffbe8d23949b948e5", size = 252227, upload-time = "2026-07-02T13:09:28.543Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/b70062750686bd7da454da27927622f48bbac6990ac7a4c4a4653e7b0036/coverage-7.15.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27f31ecb458da3f859aab3f15ada871eb7a7768807d88df4a9f186bb17737970", size = 254823, upload-time = "2026-07-02T13:09:30.177Z" }, + { url = "https://files.pythonhosted.org/packages/a9/09/dad6a75a2e561b9dc5086a8c5257a7591d584246f67e23e70d2995b89ab6/coverage-7.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fb759be317fdc62e0f56bffdf61cfcb45c7761ad6b71e3e583e71a67ae753c", size = 256059, upload-time = "2026-07-02T13:09:31.979Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/b5d2941fa9564573d44b693a871ff3156f0c42cbefe977a09fa7fdc59971/coverage-7.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5cf007add5ab4bb8fa9f4c77e3732127c9e6cad501d7db43355fbfafca0be84", size = 258190, upload-time = "2026-07-02T13:09:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1d/8e895bcde3c57ccd46d896dda5f2b3d5df761a1b0c6c9d450d175dedc632/coverage-7.15.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc78d9843bd576fbe2118248258d485e968dc535f95ed504a7b0867ba9b51389", size = 252456, upload-time = "2026-07-02T13:09:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/14/4c/f6997da343ddeb959be82c3b05322793f92c071ad45f7cb8a96336e2dd5f/coverage-7.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a263060f1de0b4b74b4e089c2a70b8003b3781c733329a9c8fd54995328f9950", size = 254192, upload-time = "2026-07-02T13:09:37.445Z" }, + { url = "https://files.pythonhosted.org/packages/17/27/a0bc09d032267b9da89d95a2d874cfbef2a5aebbf0e87cf7aba221d79a99/coverage-7.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c48decf16e0dfd5b049c7d5e82200c23c08126719142998d4f172444e3d0529e", size = 252153, upload-time = "2026-07-02T13:09:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/54/c0/77fc233d9fba07b244c40948c53fe27308b8f21732fb3417f87fbd6fd992/coverage-7.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08fb028000ed0aaa0a4cbdfbb98be7cb42f370db973fbbb469733505ab20e13e", size = 256310, upload-time = "2026-07-02T13:09:41.006Z" }, + { url = "https://files.pythonhosted.org/packages/d5/24/601cecfb5825becacb8d45219a018a3b55b9dbaec624efdb0ea249d08be2/coverage-7.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb7dc0c3b7d8a1077abea0b8546ebc5e26d6ef6ecefc2f0f5ad2b8a53bdad837", size = 251974, upload-time = "2026-07-02T13:09:42.733Z" }, + { url = "https://files.pythonhosted.org/packages/47/1e/6f45e5a5b3d5484318d368702af6716b5ab8913b0428bec981a562fcf296/coverage-7.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cb3602054ccbe9f0d8c2dc04bbeba90d5719236e2cd06e042ddd6d3fc7b6e37", size = 253745, upload-time = "2026-07-02T13:09:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4df027a77bd11d0e527f44c53557c76e54ad027413d0304252ea3a78d67e/coverage-7.15.0-cp313-cp313-win32.whl", hash = "sha256:0bf781da64326b677be344df505171435b6f58716108606621d5d27d964fff8b", size = 222902, upload-time = "2026-07-02T13:09:46.122Z" }, + { url = "https://files.pythonhosted.org/packages/a0/10/0355894d34e231f2c5449e71287e81a50793a325df2e2b027b7bcd9dfd19/coverage-7.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:2c57a275078ee3fa185f83e400f765bc764a549de66d99b47881645cbd4ea629", size = 223444, upload-time = "2026-07-02T13:09:47.687Z" }, + { url = "https://files.pythonhosted.org/packages/06/ef/bb725f263befaaff851203ab338e68af15e195d7f7b5f323162532d9b6a8/coverage-7.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:3812c61afc6685c7999b39320779ab8f43b7a3081fdb0def39976e56fbdb9a21", size = 222839, upload-time = "2026-07-02T13:09:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9c/1e3ca54f72a3185ece06c58d871099898c48f0ed6430d17b6ab75f0d180a/coverage-7.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:41cb79af843222e11da87127ad0ecbfa878abadd0f770a4a99391a27d3887324", size = 220906, upload-time = "2026-07-02T13:09:51.339Z" }, + { url = "https://files.pythonhosted.org/packages/09/37/f718613d83b274880382f6b67e78f3802549ae39b0b3e65ae5b5974df56e/coverage-7.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d2008989ef8fe54188d3f3bfa2e3099b025af11e90a6a1b9e7dc433d04263d8", size = 221239, upload-time = "2026-07-02T13:09:53.138Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/22bae91e0b75445f68d365c7643ed0aa4880bbf77450ee74ca65bdae53a7/coverage-7.15.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:769e8ece11a596315ebf5aa7ec383aeeed016c091d2bf6363ffb996d41529092", size = 252286, upload-time = "2026-07-02T13:09:54.996Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/bec5e32aa508615d9d7a2790effb25fb4dc28606e995816afe400b25ece3/coverage-7.15.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65a6b6164ee5c39e2f3803f314292d6c61a607ba7fee253d1e03c42dc3903502", size = 254789, upload-time = "2026-07-02T13:09:56.678Z" }, + { url = "https://files.pythonhosted.org/packages/17/29/0e865435b4354e4a7c03b1b7920046d31d0a273d55decefea27e011cb9bf/coverage-7.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75128817f95a5c45bb01d65fd2d8b9cb54bbe03d81608fb70e3e14b437ad56c2", size = 256135, upload-time = "2026-07-02T13:09:58.343Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/33a870b58a13325d62fc0a6c8f01fa0ff667cef60c7498e2382a147dfa18/coverage-7.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9887bb428fe2d4cd4bee89bac1a6c9932f484afd5b36fbd4ff6ea5f825bb1f5e", size = 258449, upload-time = "2026-07-02T13:10:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/18/7b/6fffe596bf3ddba8462758d02c5dad730fd91055a6634aa2e4226229181a/coverage-7.15.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bfc0be1f702042207a93a00523b1065ee1fe951e96edf311581c0bbc2e34888", size = 252313, upload-time = "2026-07-02T13:10:01.946Z" }, + { url = "https://files.pythonhosted.org/packages/58/1b/11468dd6c1676ab831a70cb9a8d4e198e8607fa0b7220ab918b73fe9bfbd/coverage-7.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f64627d55def5a43282d70e08396672692f77e4da610a5bb8bb4060b432b6859", size = 254142, upload-time = "2026-07-02T13:10:04.065Z" }, + { url = "https://files.pythonhosted.org/packages/79/41/29328e21d16b1b95092c30dd700e08cf915bd3734f836df8f3bdb0e8fa9f/coverage-7.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2c6f0fa473003905c6d5bac328ee4eba9fbea654f15bc24b8a3274b23363fa99", size = 252108, upload-time = "2026-07-02T13:10:06.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/de/05ccfb990439655b35afbfd8e0d13fe66677565a7d4eb38c3f5ef2635e1c/coverage-7.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2bcf9afaf064172c6ec3c58a325a9957ad1178c05dd934e25f253321776e0676", size = 256385, upload-time = "2026-07-02T13:10:08.141Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/486828a3d2695ea7a2609f17ff572f6b01905e608379440a11da4b8dffbe/coverage-7.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:baf06bc987115d6fb938d403f7eab684a057766c490367999a2b71a6883110c6", size = 251923, upload-time = "2026-07-02T13:10:10.179Z" }, + { url = "https://files.pythonhosted.org/packages/18/c7/03582b6715f078e5e558354c87616d945b9894cda2dace8e4009b17035e4/coverage-7.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0405f2ff97b1c4c0e782cb32e02f32369bcf2e6b618b591d67e1ea754575dfe", size = 253580, upload-time = "2026-07-02T13:10:12.052Z" }, + { url = "https://files.pythonhosted.org/packages/db/dc/9e578bbaf2ecb4959a81b7e7601ad8cca772cba2892e8d144cb749b4a71a/coverage-7.15.0-cp314-cp314-win32.whl", hash = "sha256:ab282853ed5fbd64bbb162f19cb8fcb7087187508a6374b4f9c34ec1577c4e8f", size = 223107, upload-time = "2026-07-02T13:10:13.994Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3e/c8c3b75d8dbe0e35f7b0cc3ff5e949fc59500f70b21d0398813f66740664/coverage-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bb3040e9f4bbe26fcb0cd7cc85ac63e630d3f3a9c74f027abf4caa27e706663", size = 223597, upload-time = "2026-07-02T13:10:15.906Z" }, + { url = "https://files.pythonhosted.org/packages/cd/bc/3cbc9fb036eb388519bccd521f783499c39b64256013fbc362782f196fe1/coverage-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:346771144d34f7fa84ec28386f78e0f31653f33cf35e19d253d5b35f9e8201da", size = 223020, upload-time = "2026-07-02T13:10:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/28/00/199c4a8d656dff63102577a056c0fce2ff6a79e40adac092fc986c49cbf1/coverage-7.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d34a010905fb6401324ba016b5da03d574967f7b21ce48ea41e66f0f1f95f641", size = 221638, upload-time = "2026-07-02T13:10:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/9d0092c96a3d3a26951ecc7020826aa57bcb1b119ca81acbba996884ab13/coverage-7.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bb25d825d885ca8036795dacfc3924d33091fc76d71ebc99420c6b79e77d96fa", size = 221903, upload-time = "2026-07-02T13:10:21.514Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b4/c0ca3028f42c9a08e51feb4561ef1192e5de99797cd1db5b04590c215bda/coverage-7.15.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:94c9686bfe8a9a6810297aecbd99beaa3445f9e8dc2f80b1382cca0d86b64461", size = 263267, upload-time = "2026-07-02T13:10:23.261Z" }, + { url = "https://files.pythonhosted.org/packages/5f/aa/a375e3846e5d3c013dc600b2a3231089055c73d77f5393dd2192a8d64da6/coverage-7.15.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bd671c25f9d85f09d7ec481d0e43d5139f486c06a37139847a7ce569788af72", size = 265390, upload-time = "2026-07-02T13:10:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/92/e1/5783cdabb797305e1c9e4809fea496d31834c51fa772514f73dc148bcfc9/coverage-7.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:110cbdf8d2e216577312cf06ccf85539c0e5a5420ef747e4a4719b5e483c88cd", size = 267811, upload-time = "2026-07-02T13:10:27.249Z" }, + { url = "https://files.pythonhosted.org/packages/85/31/96d8bbf58b8e9193bc8389574a91a0db48355ee98feb66aa6bf8d1b32eea/coverage-7.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c5d4619214f1d9993e7b00a8600d14614b7e9d84e89507460b126aa5e6559e5", size = 268928, upload-time = "2026-07-02T13:10:29.242Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7a/5294567e811a1cb7eda93140c628fa050d66189da28da320f93d1d815c73/coverage-7.15.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:781a704516e2d8346fbbd5be6c6f3412dd824785146528b3a01816f26c081007", size = 262378, upload-time = "2026-07-02T13:10:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/69/3f/3f48538421f899f28946f90a3d272136a4686e1abf461cc9249a783ee0f3/coverage-7.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd4a1b44bcb65ee29e947ac92bbee04956df3a6bfc6143641bb6cae7ede00fc9", size = 265263, upload-time = "2026-07-02T13:10:32.942Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d3/092df15efcab8a9c1467ee960eb8019bbad3f9300d115d89ea6195f369ff/coverage-7.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0e4950c9d6d3e39c64c991814ff315e2d0b9cb8152363594212c9e55208c0a8f", size = 262866, upload-time = "2026-07-02T13:10:35.104Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ab/0254d2b88665efb2c57ad368cc77ab5de3435bd8d5add4729c1b0e79431e/coverage-7.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:fe9c87ff42e5472d80d21704972e1f96e104a0a599d77c5e35db5a3c562e2571", size = 266599, upload-time = "2026-07-02T13:10:37.05Z" }, + { url = "https://files.pythonhosted.org/packages/a8/79/1cfa4023e489ce6fbc7be4a5d442dbc375edb4f4fda39a352cedb53263c2/coverage-7.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f00d5ae1dd2fe13fb8186e3e7d37bcbd8b25c0d764ff7d1b32cef9be058510a8", size = 261714, upload-time = "2026-07-02T13:10:38.966Z" }, + { url = "https://files.pythonhosted.org/packages/b7/eb/fee5c8665656be63f497418d410484637c438172568688e8ac92e06574e7/coverage-7.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363ab38cc78b615f11c9cac3cf1d7eef950c18b9fdedfb9066f59461dcf84d68", size = 264025, upload-time = "2026-07-02T13:10:40.789Z" }, + { url = "https://files.pythonhosted.org/packages/ab/99/63005db722f91edc81abc16302f9cc2f6228c1679e46e15be9ae144b14d0/coverage-7.15.0-cp314-cp314t-win32.whl", hash = "sha256:54fd9c53a5fafff509195f1b6a3f9be615d8e8362a3629ff1de23d270c03c86b", size = 223413, upload-time = "2026-07-02T13:10:42.597Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e8/2bc6181c4fb06f1a6b981eb85330cc57bfad7e3f710fc9c9d350013ba228/coverage-7.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:87b47553097ba185ed964866078e7e63adea9f5f51b5f39691c34f30afd21080", size = 224245, upload-time = "2026-07-02T13:10:44.47Z" }, + { url = "https://files.pythonhosted.org/packages/79/b8/4d959bf9cc45d0cfed2f4d35cafcab978cdb6ea02eb5100009cd740632a3/coverage-7.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeefb2dd178fe7eee79f0ad25d75855cb35ee9ed472db2c5ea06f5b4fd00cec5", size = 223558, upload-time = "2026-07-02T13:10:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" }, ] [[package]] @@ -1937,7 +1932,6 @@ core-tests = [ { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'group-8-narwhals-core') or (python_full_version < '3.11' and extra == 'group-8-narwhals-core-tests') or (python_full_version < '3.11' and extra == 'group-8-narwhals-dev-core') or (python_full_version < '3.11' and extra != 'group-8-narwhals-extreme-minimum-versions' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra != 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-local-dev') or (python_full_version < '3.11' and extra != 'group-8-narwhals-extreme-minimum-versions' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra != 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (python_full_version < '3.11' and extra != 'group-8-narwhals-extreme-minimum-versions' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra != 'group-8-narwhals-extreme-pretty-old-versions' and extra != 'group-8-narwhals-typing') or extra == 'extra-8-narwhals-cudf' or (extra == 'extra-8-narwhals-dask' and extra == 'extra-8-narwhals-modin') or (extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'extra-8-narwhals-pandas') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-core') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-core-tests') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-dev-core') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-local-dev') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-local-dev' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-min-pyspark-version' and extra == 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-dask' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'extra-8-narwhals-modin' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra != 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-dask' and extra != 'group-8-narwhals-core' and extra != 'group-8-narwhals-core-tests' and extra != 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions')" }, { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-core') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-core-tests') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-dev-core') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-local-dev') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-min-pyspark-version') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra != 'group-8-narwhals-typing') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-core') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-core-tests') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-dev-core') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-local-dev') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-min-pyspark-version') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra != 'group-8-narwhals-extreme-minimum-versions' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra != 'group-8-narwhals-extreme-pretty-old-versions' and extra != 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-dask') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-modin') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-pandas') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-dask' and extra == 'extra-8-narwhals-modin') or (extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'extra-8-narwhals-pandas') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-local-dev' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-min-pyspark-version' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-typing')" }, { name = "pytest" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -1947,7 +1941,6 @@ dev = [ { name = "hypothesis" }, { name = "prek" }, { name = "pytest" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -1962,7 +1955,6 @@ dev-core = [ { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-core') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-core-tests') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-dev-core') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-local-dev') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-min-pyspark-version') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra != 'group-8-narwhals-typing') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-core') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-core-tests') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-dev-core') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-local-dev') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-min-pyspark-version') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra != 'group-8-narwhals-extreme-minimum-versions' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra != 'group-8-narwhals-extreme-pretty-old-versions' and extra != 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-dask') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-modin') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-pandas') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-dask' and extra == 'extra-8-narwhals-modin') or (extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'extra-8-narwhals-pandas') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-local-dev' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-min-pyspark-version' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-typing')" }, { name = "prek" }, { name = "pytest" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -1992,7 +1984,6 @@ extreme-minimum-versions = [ { name = "pyarrow", version = "13.0.0", source = { registry = "https://pypi.org/simple" } }, { name = "pyarrow-stubs", version = "10.0.1.9", source = { registry = "https://pypi.org/simple" } }, { name = "pytest" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2012,7 +2003,6 @@ extreme-not-so-old-versions = [ { name = "pyarrow", version = "15.0.0", source = { registry = "https://pypi.org/simple" } }, { name = "pyarrow-stubs", version = "10.0.1.9", source = { registry = "https://pypi.org/simple" } }, { name = "pytest" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2031,7 +2021,6 @@ extreme-pretty-old-versions = [ { name = "pyarrow", version = "14.0.0", source = { registry = "https://pypi.org/simple" } }, { name = "pyarrow-stubs", version = "10.0.1.9", source = { registry = "https://pypi.org/simple" } }, { name = "pytest" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2054,7 +2043,6 @@ local-dev = [ { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-core') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-core-tests') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-dev-core') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-local-dev') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-min-pyspark-version') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra != 'group-8-narwhals-typing') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-core') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-core-tests') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-dev-core') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-local-dev') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-min-pyspark-version') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra != 'group-8-narwhals-extreme-minimum-versions' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra != 'group-8-narwhals-extreme-pretty-old-versions' and extra != 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-dask') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-modin') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-pandas') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-dask' and extra == 'extra-8-narwhals-modin') or (extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'extra-8-narwhals-pandas') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-local-dev' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-min-pyspark-version' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-typing')" }, { name = "prek" }, { name = "pytest" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2071,7 +2059,6 @@ min-pyspark-version = [ { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-core') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-core-tests') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-dev-core') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-local-dev') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-min-pyspark-version') or (python_full_version >= '3.11' and extra == 'extra-8-narwhals-dask' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra != 'group-8-narwhals-typing') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-core') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-core-tests') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-dev-core') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-local-dev') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-min-pyspark-version') or (python_full_version >= '3.11' and extra != 'extra-8-narwhals-cudf' and extra != 'extra-8-narwhals-modin' and extra != 'group-8-narwhals-extreme-minimum-versions' and extra != 'group-8-narwhals-extreme-not-so-old-versions' and extra != 'group-8-narwhals-extreme-pretty-old-versions' and extra != 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-dask') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-modin') or (extra == 'extra-8-narwhals-cudf' and extra == 'extra-8-narwhals-pandas') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'extra-8-narwhals-cudf' and extra == 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-dask' and extra == 'extra-8-narwhals-modin') or (extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-dask' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'extra-8-narwhals-pandas') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-core-tests' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-dev-core' and extra == 'group-8-narwhals-typing') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-extreme-minimum-versions') or (extra == 'extra-8-narwhals-modin' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-local-dev' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-min-pyspark-version' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-extreme-not-so-old-versions') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-minimum-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-extreme-pretty-old-versions') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-not-so-old-versions' and extra == 'group-8-narwhals-typing') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-local-dev') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-min-pyspark-version') or (extra == 'group-8-narwhals-extreme-pretty-old-versions' and extra == 'group-8-narwhals-typing')" }, { name = "pyspark", version = "3.5.0", source = { registry = "https://pypi.org/simple" } }, { name = "pytest" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2089,7 +2076,6 @@ tests = [ { name = "covdefaults" }, { name = "hypothesis" }, { name = "pytest" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2144,7 +2130,6 @@ core-tests = [ { name = "narwhals", extras = ["duckdb", "polars", "pyarrow", "sqlframe", "sql"] }, { name = "pandas", specifier = ">=1.3.4" }, { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2154,7 +2139,6 @@ dev = [ { name = "hypothesis", specifier = ">=6.0.0" }, { name = "prek" }, { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2168,7 +2152,6 @@ dev-core = [ { name = "pandas", specifier = ">=1.3.4" }, { name = "prek" }, { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2194,7 +2177,6 @@ extreme-minimum-versions = [ { name = "pyarrow", specifier = "==13.0.0" }, { name = "pyarrow-stubs", specifier = "<17" }, { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2214,7 +2196,6 @@ extreme-not-so-old-versions = [ { name = "pyarrow", specifier = "==15.0.0" }, { name = "pyarrow-stubs", specifier = "<17" }, { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2233,7 +2214,6 @@ extreme-pretty-old-versions = [ { name = "pyarrow", specifier = "==14.0.0" }, { name = "pyarrow-stubs", specifier = "<17" }, { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2256,7 +2236,6 @@ local-dev = [ { name = "pandas", specifier = ">=1.3.4" }, { name = "prek" }, { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2272,7 +2251,6 @@ min-pyspark-version = [ { name = "pandas", specifier = ">=1.3.4" }, { name = "pyspark", specifier = "==3.5.0" }, { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -2283,7 +2261,6 @@ tests = [ { name = "covdefaults" }, { name = "hypothesis", specifier = ">=6.0.0" }, { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-randomly" }, { name = "pytest-xdist" }, @@ -4032,15 +4009,15 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "11.0" +version = "11.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/67/f1e79672a5f91985577c7984c9709ca110e4fd37fe7fd167b60422e6ccc2/pymdown_extensions-11.0.tar.gz", hash = "sha256:8269cef0247f9e2d0a62fcea10860aba05c1cbab5470fd4b63230b96434dc589", size = 857049, upload-time = "2026-06-23T02:27:45.146Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b6/1ae53367e28b9cffa3be7574e13fbe4589694272fd47710fbdbafd3d63c6/pymdown_extensions-11.0-py3-none-any.whl", hash = "sha256:fbc4acb641814fa9d17521bbd21a5240ef739a662f11c06330c4b78c93e954d6", size = 269415, upload-time = "2026-06-23T02:27:43.826Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, ] [[package]] @@ -4256,20 +4233,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] -[[package]] -name = "pytest-cov" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pluggy" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, -] - [[package]] name = "pytest-env" version = "1.6.0"