diff --git a/src/narwhals/_typing.py b/src/narwhals/_typing.py index 8361d96303..189a1543b6 100644 --- a/src/narwhals/_typing.py +++ b/src/narwhals/_typing.py @@ -1,7 +1,18 @@ +"""Type aliases describing the backends Narwhals dispatches to. + +Built-in backends are enumerated as `Literal` unions (`Backend`, `EagerAllowed`, `LazyAllowed`, ...). +Plugin backends are discovered at runtime and cannot join those unions, so `PluginName` uses a +[`NewType`](https://typing.python.org/en/latest/spec/aliases.html#newtype) instead: +type checkers treat it as a distinct subtype of `str`, therefore: + +- an arbitrary `str` is still rejected where a `backend` is expected, and +- a value explicitly wrapped as `PluginName("...")` is accepted. +""" + from __future__ import annotations from types import ModuleType -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Literal, NewType from narwhals._typing_compat import TypeVar from narwhals._utils import Implementation, _NoDefault @@ -91,7 +102,17 @@ - An Implementation, such as: `Implementation.DASK`, `Implementation.PYSPARK`, ... """ -BackendT = TypeVar("BackendT", bound=Backend) +PluginName = NewType("PluginName", str) +"""Name of a plugin's [entry point](https://packaging.python.org/en/latest/specifications/entry-points/). + +- Wrap an entry point name to pass it wherever a `backend` is expected, e.g. `PluginName("my-plugin")`. +- Add it to a signature's `IntoBackend` parameter to advertise plugin support, e.g. `IntoBackend[EagerAllowed | PluginName]`. + +See the `narwhals.plugins` module for how plugin authors register entry points +and the contract a wrapped name must satisfy. +""" + +BackendT = TypeVar("BackendT", bound=Backend | PluginName) IntoBackend: TypeAlias = BackendT | ModuleType """Anything that can be converted into a [`narwhals.Implementation`][]. diff --git a/src/narwhals/_utils.py b/src/narwhals/_utils.py index 4bb942f91a..d625613f66 100644 --- a/src/narwhals/_utils.py +++ b/src/narwhals/_utils.py @@ -100,6 +100,7 @@ from narwhals._typing import ( Backend, IntoBackend, + PluginName, _ArrowImpl, _CuDFImpl, _DaskImpl, @@ -140,8 +141,6 @@ _SliceNone, ) - UnknownBackendName: TypeAlias = str - FrameOrSeriesT = TypeVar( "FrameOrSeriesT", bound=LazyFrame[Any] | DataFrame[Any] | Series[Any] ) @@ -395,7 +394,7 @@ def from_string(cls: type[Self], backend_name: str) -> Implementation: @classmethod def from_backend( - cls: type[Self], backend: IntoBackend[Backend] | UnknownBackendName + cls: type[Self], backend: IntoBackend[Backend | PluginName] ) -> Implementation: """Instantiate from native namespace module, string, or Implementation. diff --git a/src/narwhals/dataframe.py b/src/narwhals/dataframe.py index 3c7fb9ea15..46fdad0050 100644 --- a/src/narwhals/dataframe.py +++ b/src/narwhals/dataframe.py @@ -72,7 +72,13 @@ from narwhals._compliant.typing import CompliantExprAny from narwhals._expression_parsing import ExprMetadata from narwhals._translate import IntoArrowTable - from narwhals._typing import EagerAllowed, IntoBackend, LazyAllowed, Polars + from narwhals._typing import ( + EagerAllowed, + IntoBackend, + LazyAllowed, + PluginName, + Polars, + ) from narwhals.group_by import GroupBy, LazyGroupBy from narwhals.typing import ( AsofJoinStrategy, @@ -502,7 +508,10 @@ def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> @classmethod def from_arrow( - cls, native_frame: IntoArrowTable, *, backend: IntoBackend[EagerAllowed] + cls, + native_frame: IntoArrowTable, + *, + backend: IntoBackend[EagerAllowed | PluginName], ) -> DataFrame[Any]: """Construct a DataFrame from an object which supports the PyCapsule Interface. @@ -559,7 +568,7 @@ def from_dict( data: Mapping[str, Any], schema: IntoSchema | Mapping[str, IntoDType | None] | None = None, *, - backend: IntoBackend[EagerAllowed] | None = None, + backend: IntoBackend[EagerAllowed | PluginName] | None = None, ) -> DataFrame[Any]: """Instantiate DataFrame from dictionary. @@ -622,7 +631,7 @@ def from_dicts( data: Sequence[Mapping[str, Any]], schema: IntoSchema | Mapping[str, IntoDType | None] | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> DataFrame[Any]: """Instantiate DataFrame from a sequence of dictionaries representing rows. @@ -696,7 +705,7 @@ def from_numpy( data: _2DArray, schema: IntoSchema | Sequence[str] | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> DataFrame[Any]: """Construct a DataFrame from a NumPy ndarray. diff --git a/src/narwhals/functions.py b/src/narwhals/functions.py index c528ebf60c..a576c6b33d 100644 --- a/src/narwhals/functions.py +++ b/src/narwhals/functions.py @@ -43,7 +43,7 @@ from narwhals._native import NativeDataFrame, NativeLazyFrame, NativeSeries from narwhals._translate import IntoArrowTable - from narwhals._typing import Backend, EagerAllowed, IntoBackend + from narwhals._typing import Backend, EagerAllowed, IntoBackend, PluginName from narwhals.dataframe import DataFrame, LazyFrame from narwhals.series import Series from narwhals.typing import ( @@ -169,7 +169,7 @@ def new_series( values: Any, dtype: IntoDType | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> Series[Any]: """Instantiate Narwhals Series from iterable (e.g. list or array). @@ -211,7 +211,7 @@ def _new_series_impl( values: Any, dtype: IntoDType | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> Series[Any]: implementation = Implementation.from_backend(backend) if is_eager_allowed(implementation): @@ -241,7 +241,7 @@ def from_dict( data: Mapping[str, Any], schema: IntoSchema | Mapping[str, IntoDType | None] | None = None, *, - backend: IntoBackend[EagerAllowed] | None = None, + backend: IntoBackend[EagerAllowed | PluginName] | None = None, native_namespace: ModuleType | None = None, # noqa: ARG001 ) -> DataFrame[Any]: """Instantiate DataFrame from dictionary. @@ -332,7 +332,7 @@ def from_dicts( data: Sequence[Mapping[str, Any]], schema: IntoSchema | Mapping[str, IntoDType | None] | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> DataFrame[Any]: """Instantiate DataFrame from a sequence of dictionaries representing rows. @@ -393,7 +393,7 @@ def from_numpy( data: _2DArray, schema: IntoSchema | Sequence[str] | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> DataFrame[Any]: """Construct a DataFrame from a NumPy ndarray. @@ -482,7 +482,7 @@ def _is_into_schema(obj: Any) -> TypeIs[_IntoSchema]: def from_arrow( - native_frame: IntoArrowTable, *, backend: IntoBackend[EagerAllowed] + native_frame: IntoArrowTable, *, backend: IntoBackend[EagerAllowed | PluginName] ) -> DataFrame[Any]: # pragma: no cover """Construct a DataFrame from an object which supports the PyCapsule Interface. @@ -652,7 +652,7 @@ def _validate_separator_pyarrow(separator: str, **kwargs: Any) -> Any: def read_csv( source: FileSource, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], separator: str = ",", **kwargs: Any, ) -> DataFrame[Any]: @@ -727,7 +727,7 @@ def read_csv( def scan_csv( source: FileSource, *, - backend: IntoBackend[Backend], + backend: IntoBackend[Backend | PluginName], separator: str = ",", **kwargs: Any, ) -> LazyFrame[Any]: @@ -813,7 +813,7 @@ def scan_csv( def read_parquet( - source: FileSource, *, backend: IntoBackend[EagerAllowed], **kwargs: Any + source: FileSource, *, backend: IntoBackend[EagerAllowed | PluginName], **kwargs: Any ) -> DataFrame[Any]: """Read into a DataFrame from a parquet file. @@ -886,7 +886,7 @@ def read_parquet( def scan_parquet( - source: FileSource, *, backend: IntoBackend[Backend], **kwargs: Any + source: FileSource, *, backend: IntoBackend[Backend | PluginName], **kwargs: Any ) -> LazyFrame[Any]: """Lazily read from a parquet file. diff --git a/src/narwhals/plugins.py b/src/narwhals/plugins.py index a247fc6b22..ae823eec59 100644 --- a/src/narwhals/plugins.py +++ b/src/narwhals/plugins.py @@ -1,3 +1,16 @@ +"""Runtime discovery of, and dispatch to, Narwhals plugin backends. + +A plugin backend registers an [entry point](https://packaging.python.org/en/latest/specifications/entry-points/) +in the `narwhals.plugins` group. Plugins are discovered at runtime, so their names +cannot be enumerated in the `Literal` unions that describe built-in backends. + +`PluginName` bridges that gap: a plugin's entry point name, wrapped as +`PluginName("my-plugin")`, is accepted wherever a `backend` is expected. + +The contract for plugin authors is that the wrapped string **must** name an +installed plugin's entry point in the `narwhals.plugins` group. +""" + from __future__ import annotations import sys @@ -5,6 +18,7 @@ from typing import TYPE_CHECKING, Any, Protocol from narwhals._compliant import CompliantNamespace +from narwhals._typing import PluginName from narwhals._typing_compat import TypeVar if TYPE_CHECKING: @@ -23,7 +37,7 @@ from narwhals.utils import Version -__all__ = ["Plugin", "from_native"] +__all__ = ["Plugin", "PluginName", "from_native"] CompliantAny: TypeAlias = ( "CompliantDataFrameAny | CompliantLazyFrameAny | CompliantSeriesAny" diff --git a/src/narwhals/series.py b/src/narwhals/series.py index c6f9d9ba5b..d0688cba41 100644 --- a/src/narwhals/series.py +++ b/src/narwhals/series.py @@ -45,7 +45,7 @@ from typing_extensions import Self from narwhals._compliant import CompliantSeries - from narwhals._typing import EagerAllowed, IntoBackend, NoDefault + from narwhals._typing import EagerAllowed, IntoBackend, NoDefault, PluginName from narwhals.dataframe import DataFrame, MultiIndexSelector from narwhals.dtypes import DType from narwhals.typing import ( @@ -121,7 +121,7 @@ def from_numpy( values: _1DArray, dtype: IntoDType | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> Series[Any]: """Construct a Series from a NumPy ndarray. @@ -186,7 +186,7 @@ def from_iterable( values: Iterable[Any], dtype: IntoDType | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> Series[Any]: """Construct a Series from an iterable. diff --git a/src/narwhals/stable/v1/__init__.py b/src/narwhals/stable/v1/__init__.py index a730b927c7..6b89027a1f 100644 --- a/src/narwhals/stable/v1/__init__.py +++ b/src/narwhals/stable/v1/__init__.py @@ -103,6 +103,7 @@ IntoBackend, LazyAllowed, Pandas, + PluginName, Polars, ) from narwhals.dataframe import MultiColSelector, MultiIndexSelector @@ -138,7 +139,10 @@ def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> @classmethod def from_arrow( - cls, native_frame: IntoArrowTable, *, backend: IntoBackend[EagerAllowed] + cls, + native_frame: IntoArrowTable, + *, + backend: IntoBackend[EagerAllowed | PluginName], ) -> DataFrame[Any]: result = super().from_arrow(native_frame, backend=backend) return cast("DataFrame[Any]", result) @@ -149,7 +153,7 @@ def from_dict( data: Mapping[str, Any], schema: IntoSchema | Mapping[str, IntoDType | None] | None = None, *, - backend: IntoBackend[EagerAllowed] | None = None, + backend: IntoBackend[EagerAllowed | PluginName] | None = None, ) -> DataFrame[Any]: result = super().from_dict(data, schema, backend=backend) return cast("DataFrame[Any]", result) @@ -160,7 +164,7 @@ def from_dicts( data: Sequence[Any], schema: IntoSchema | Mapping[str, IntoDType | None] | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> DataFrame[Any]: result = super().from_dicts(data, schema, backend=backend) return cast("DataFrame[Any]", result) @@ -171,7 +175,7 @@ def from_numpy( data: _2DArray, schema: IntoSchema | Sequence[str] | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> DataFrame[Any]: result = super().from_numpy(data, schema, backend=backend) return cast("DataFrame[Any]", result) @@ -329,7 +333,7 @@ def from_numpy( values: _1DArray, dtype: IntoDType | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> Series[Any]: result = super().from_numpy(name, values, dtype, backend=backend) return cast("Series[Any]", result) @@ -341,7 +345,7 @@ def from_iterable( values: Iterable[Any], dtype: IntoDType | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> Series[Any]: result = super().from_iterable(name, values, dtype, backend=backend) return cast("Series[Any]", result) @@ -950,7 +954,7 @@ def new_series( values: Any, dtype: IntoDType | None = None, *, - backend: IntoBackend[EagerAllowed] | None = None, + backend: IntoBackend[EagerAllowed | PluginName] | None = None, native_namespace: ModuleType | None = None, # noqa: ARG001 ) -> Series[Any]: """Instantiate Narwhals Series from iterable (e.g. list or array). @@ -959,7 +963,7 @@ def new_series( an is the same as `backend` but only accepts module types - for new code, we recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ - backend = cast("IntoBackend[EagerAllowed]", backend) + backend = cast("IntoBackend[EagerAllowed | PluginName]", backend) return _stableify(_new_series_impl(name, values, dtype, backend=backend)) @@ -967,7 +971,7 @@ def new_series( def from_arrow( native_frame: IntoArrowTable, *, - backend: IntoBackend[EagerAllowed] | None = None, + backend: IntoBackend[EagerAllowed | PluginName] | None = None, native_namespace: ModuleType | None = None, # noqa: ARG001 ) -> DataFrame[Any]: """Construct a DataFrame from an object which supports the PyCapsule Interface. @@ -976,7 +980,7 @@ def from_arrow( an is the same as `backend` but only accepts module types - for new code, we recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ - backend = cast("IntoBackend[EagerAllowed]", backend) + backend = cast("IntoBackend[EagerAllowed | PluginName]", backend) return _stableify(nw_f.from_arrow(native_frame, backend=backend)) @@ -985,7 +989,7 @@ def from_dict( data: Mapping[str, Any], schema: IntoSchema | Mapping[str, IntoDType | None] | None = None, *, - backend: IntoBackend[EagerAllowed] | None = None, + backend: IntoBackend[EagerAllowed | PluginName] | None = None, native_namespace: ModuleType | None = None, # noqa: ARG001 ) -> DataFrame[Any]: """Instantiate DataFrame from dictionary. @@ -1005,7 +1009,7 @@ def from_numpy( data: _2DArray, schema: IntoSchema | Sequence[str] | None = None, *, - backend: IntoBackend[EagerAllowed] | None = None, + backend: IntoBackend[EagerAllowed | PluginName] | None = None, native_namespace: ModuleType | None = None, # noqa: ARG001 ) -> DataFrame[Any]: """Construct a DataFrame from a NumPy ndarray. @@ -1014,7 +1018,7 @@ def from_numpy( an is the same as `backend` but only accepts module types - for new code, we recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ - backend = cast("IntoBackend[EagerAllowed]", backend) + backend = cast("IntoBackend[EagerAllowed | PluginName]", backend) return _stableify(nw_f.from_numpy(data, schema, backend=backend)) @@ -1022,7 +1026,7 @@ def from_numpy( def read_csv( source: FileSource, *, - backend: IntoBackend[EagerAllowed] | None = None, + backend: IntoBackend[EagerAllowed | PluginName] | None = None, native_namespace: ModuleType | None = None, # noqa: ARG001 **kwargs: Any, ) -> DataFrame[Any]: @@ -1032,7 +1036,7 @@ def read_csv( an is the same as `backend` but only accepts module types - for new code, we recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ - backend = cast("IntoBackend[EagerAllowed]", backend) + backend = cast("IntoBackend[EagerAllowed | PluginName]", backend) return _stableify(nw_f.read_csv(source, backend=backend, **kwargs)) @@ -1040,7 +1044,7 @@ def read_csv( def scan_csv( source: FileSource, *, - backend: IntoBackend[Backend] | None = None, + backend: IntoBackend[Backend | PluginName] | None = None, native_namespace: ModuleType | None = None, # noqa: ARG001 **kwargs: Any, ) -> LazyFrame[Any]: @@ -1050,7 +1054,7 @@ def scan_csv( an is the same as `backend` but only accepts module types - for new code, we recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ - backend = cast("IntoBackend[Backend]", backend) + backend = cast("IntoBackend[Backend | PluginName]", backend) return _stableify(nw_f.scan_csv(source, backend=backend, **kwargs)) @@ -1058,7 +1062,7 @@ def scan_csv( def read_parquet( source: FileSource, *, - backend: IntoBackend[EagerAllowed] | None = None, + backend: IntoBackend[EagerAllowed | PluginName] | None = None, native_namespace: ModuleType | None = None, # noqa: ARG001 **kwargs: Any, ) -> DataFrame[Any]: @@ -1068,7 +1072,7 @@ def read_parquet( an is the same as `backend` but only accepts module types - for new code, we recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ - backend = cast("IntoBackend[EagerAllowed]", backend) + backend = cast("IntoBackend[EagerAllowed | PluginName]", backend) return _stableify(nw_f.read_parquet(source, backend=backend, **kwargs)) @@ -1076,7 +1080,7 @@ def read_parquet( def scan_parquet( source: FileSource, *, - backend: IntoBackend[Backend] | None = None, + backend: IntoBackend[Backend | PluginName] | None = None, native_namespace: ModuleType | None = None, # noqa: ARG001 **kwargs: Any, ) -> LazyFrame[Any]: @@ -1086,7 +1090,7 @@ def scan_parquet( an is the same as `backend` but only accepts module types - for new code, we recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ - backend = cast("IntoBackend[Backend]", backend) + backend = cast("IntoBackend[Backend | PluginName]", backend) return _stableify(nw_f.scan_parquet(source, backend=backend, **kwargs)) diff --git a/src/narwhals/stable/v2/__init__.py b/src/narwhals/stable/v2/__init__.py index f45c1e2a1b..04ec4d6c36 100644 --- a/src/narwhals/stable/v2/__init__.py +++ b/src/narwhals/stable/v2/__init__.py @@ -92,6 +92,7 @@ IntoBackend, LazyAllowed, Pandas, + PluginName, Polars, ) from narwhals.dataframe import MultiColSelector, MultiIndexSelector @@ -125,7 +126,10 @@ def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> @classmethod def from_arrow( - cls, native_frame: IntoArrowTable, *, backend: IntoBackend[EagerAllowed] + cls, + native_frame: IntoArrowTable, + *, + backend: IntoBackend[EagerAllowed | PluginName], ) -> DataFrame[Any]: result = super().from_arrow(native_frame, backend=backend) return cast("DataFrame[Any]", result) @@ -136,7 +140,7 @@ def from_dict( data: Mapping[str, Any], schema: IntoSchema | Mapping[str, IntoDType | None] | None = None, *, - backend: IntoBackend[EagerAllowed] | None = None, + backend: IntoBackend[EagerAllowed | PluginName] | None = None, ) -> DataFrame[Any]: result = super().from_dict(data, schema, backend=backend) return cast("DataFrame[Any]", result) @@ -147,7 +151,7 @@ def from_dicts( data: Sequence[Mapping[str, Any]], schema: IntoSchema | Mapping[str, IntoDType | None] | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> DataFrame[Any]: result = super().from_dicts(data, schema, backend=backend) return cast("DataFrame[Any]", result) @@ -158,7 +162,7 @@ def from_numpy( data: _2DArray, schema: IntoSchema | Sequence[str] | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> DataFrame[Any]: result = super().from_numpy(data, schema, backend=backend) return cast("DataFrame[Any]", result) @@ -282,7 +286,7 @@ def from_numpy( values: _1DArray, dtype: IntoDType | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> Series[Any]: result = super().from_numpy(name, values, dtype, backend=backend) return cast("Series[Any]", result) @@ -294,7 +298,7 @@ def from_iterable( values: Iterable[Any], dtype: IntoDType | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> Series[Any]: result = super().from_iterable(name, values, dtype, backend=backend) return cast("Series[Any]", result) @@ -927,7 +931,7 @@ def new_series( values: Any, dtype: IntoDType | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> Series[Any]: """Instantiate Narwhals Series from iterable (e.g. list or array). @@ -949,7 +953,7 @@ def new_series( def from_arrow( - native_frame: IntoArrowTable, *, backend: IntoBackend[EagerAllowed] + native_frame: IntoArrowTable, *, backend: IntoBackend[EagerAllowed | PluginName] ) -> DataFrame[Any]: """Construct a DataFrame from an object which supports the PyCapsule Interface. @@ -971,7 +975,7 @@ def from_dict( data: Mapping[str, Any], schema: IntoSchema | Mapping[str, IntoDType | None] | None = None, *, - backend: IntoBackend[EagerAllowed] | None = None, + backend: IntoBackend[EagerAllowed | PluginName] | None = None, ) -> DataFrame[Any]: """Instantiate DataFrame from dictionary. @@ -1009,7 +1013,7 @@ def from_numpy( data: _2DArray, schema: IntoSchema | Sequence[str] | None = None, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], ) -> DataFrame[Any]: """Construct a DataFrame from a NumPy ndarray. @@ -1038,7 +1042,7 @@ def from_numpy( def read_csv( source: str, *, - backend: IntoBackend[EagerAllowed], + backend: IntoBackend[EagerAllowed | PluginName], separator: str = ",", **kwargs: Any, ) -> DataFrame[Any]: @@ -1064,7 +1068,11 @@ def read_csv( def scan_csv( - source: str, *, backend: IntoBackend[Backend], separator: str = ",", **kwargs: Any + source: str, + *, + backend: IntoBackend[Backend | PluginName], + separator: str = ",", + **kwargs: Any, ) -> LazyFrame[Any]: """Lazily read from a CSV file. @@ -1091,7 +1099,7 @@ def scan_csv( def read_parquet( - source: str, *, backend: IntoBackend[EagerAllowed], **kwargs: Any + source: str, *, backend: IntoBackend[EagerAllowed | PluginName], **kwargs: Any ) -> DataFrame[Any]: """Read into a DataFrame from a parquet file. @@ -1112,7 +1120,7 @@ def read_parquet( def scan_parquet( - source: str, *, backend: IntoBackend[Backend], **kwargs: Any + source: str, *, backend: IntoBackend[Backend | PluginName], **kwargs: Any ) -> LazyFrame[Any]: """Lazily read from a parquet file. diff --git a/tests/plugins_test.py b/tests/plugins_test.py index 6de21e53f1..11bcd813e3 100644 --- a/tests/plugins_test.py +++ b/tests/plugins_test.py @@ -8,10 +8,12 @@ import narwhals.stable.v1.dependencies as nw_v1_dependencies import narwhals.stable.v2.dependencies as nw_v2_dependencies from narwhals import dependencies as nw_dependencies +from narwhals.plugins import PluginName if TYPE_CHECKING: from typing_extensions import Self + from narwhals._typing import EagerAllowed, IntoBackend from narwhals.plugins import Plugin from narwhals.utils import Version @@ -132,3 +134,43 @@ def test_typing() -> None: import test_plugin _plugin: Plugin = test_plugin + + +def test_plugin_name_runtime() -> None: + # `PluginName` is a `NewType`: identity at runtime, nominal for type checkers. + name = PluginName("some-plugin") + assert name == "some-plugin" + assert nw.Implementation.from_backend(name) is nw.Implementation.UNKNOWN + + +if TYPE_CHECKING: + # Static-only regression guards for `PluginName` + def typing_backend_plugin_name( + plugin_name: PluginName, + dynamic_string: str, + eager_or_plugin: IntoBackend[EagerAllowed | PluginName], + df: nw.DataFrame[Any], + ) -> None: + data = {"a": [1, 2]} + + # Accepted: an explicitly wrapped plugin name, everything `IntoBackend[EagerAllowed | PluginName]` covers. + nw.from_dict(data, backend=plugin_name) + nw.from_dict(data, backend=eager_or_plugin) + nw.new_series("a", [1, 2], backend=plugin_name) + nw.scan_csv("file.csv", backend=plugin_name) + nw.DataFrame.from_dict(data, backend=plugin_name) + nw.Implementation.from_backend(plugin_name) + + # Rejected: opaque strings do not satisfy `PluginName`. + nw.from_dict(data, backend=dynamic_string) # type: ignore[arg-type] + nw.new_series("a", [1, 2], backend=dynamic_string) # type: ignore[arg-type] + nw.Implementation.from_backend(dynamic_string) # type: ignore[arg-type] + + # Rejected: lazy-only literals on eager constructors (no regression). + nw.from_dict(data, backend="duckdb") # type: ignore[arg-type] + + # Rejected: `.lazy` does not dispatch to plugins (yet). + df.lazy(plugin_name) # type: ignore[arg-type] + lf = df.lazy() + # Rejected: `.collect` does not dispatch to plugins (yet). + lf.collect(plugin_name) # type: ignore[arg-type]