From 6d36b4ed850241f5062972a4129959f039c5517b Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 13 Aug 2025 12:22:25 +0100 Subject: [PATCH 01/88] attempted outline of how things could look --- daft-plugin/setup.py | 13 +++++++++++++ daft-plugin/src/__init__.py | 11 +++++++++++ daft-plugin/src/on_by_default.py | 19 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 daft-plugin/setup.py create mode 100644 daft-plugin/src/__init__.py create mode 100644 daft-plugin/src/on_by_default.py diff --git a/daft-plugin/setup.py b/daft-plugin/setup.py new file mode 100644 index 0000000000..b432085bfa --- /dev/null +++ b/daft-plugin/setup.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +import setuptools + +setuptools.setup( + name="daft-plugin", + entry_points={ + "daft.extension": [ + "X1 = daft-plugin:ExampleOne" + # "X2 = daft-plugin:ExampleTwo", + ] + }, +) diff --git a/daft-plugin/src/__init__.py b/daft-plugin/src/__init__.py new file mode 100644 index 0000000000..a8af1be724 --- /dev/null +++ b/daft-plugin/src/__init__.py @@ -0,0 +1,11 @@ +"""Module for an example Flake8 plugin.""" + +from __future__ import annotations + +# from .off_by_default import ExampleTwo +from .on_by_default import ExampleOne + +__all__ = ( + "ExampleOne", + # "ExampleTwo", +) diff --git a/daft-plugin/src/on_by_default.py b/daft-plugin/src/on_by_default.py new file mode 100644 index 0000000000..b7ab8158b1 --- /dev/null +++ b/daft-plugin/src/on_by_default.py @@ -0,0 +1,19 @@ +"""Our first example plugin.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Generator + + +class ExampleOne: + """First Example Plugin.""" + + def __init__(self, tree: Any) -> None: + self.tree = tree + + def run(self) -> Generator[Any, Any, None]: + """Do nothing.""" + yield from [] From 1062d9984227a0d1956bf749bc2da53711cfc17f Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 13 Aug 2025 14:41:59 +0100 Subject: [PATCH 02/88] attempting to read in plugins --- narwhals/temp_plugin_test.py | 10 ++++++++++ narwhals/translate.py | 12 ++++++++++++ {daft-plugin => plugins/daft-plugin}/setup.py | 0 {daft-plugin => plugins/daft-plugin}/src/__init__.py | 0 .../daft-plugin}/src/on_by_default.py | 0 pyproject.toml | 3 +++ 6 files changed, 25 insertions(+) create mode 100644 narwhals/temp_plugin_test.py rename {daft-plugin => plugins/daft-plugin}/setup.py (100%) rename {daft-plugin => plugins/daft-plugin}/src/__init__.py (100%) rename {daft-plugin => plugins/daft-plugin}/src/on_by_default.py (100%) diff --git a/narwhals/temp_plugin_test.py b/narwhals/temp_plugin_test.py new file mode 100644 index 0000000000..b7c4f17ed9 --- /dev/null +++ b/narwhals/temp_plugin_test.py @@ -0,0 +1,10 @@ +import sys + +if sys.version_info < (3, 10): + from importlib_metadata import entry_points +else: + from importlib.metadata import entry_points + +discovered_plugins = entry_points(group='narwhals.plugins') + +print(discovered_plugins) \ No newline at end of file diff --git a/narwhals/translate.py b/narwhals/translate.py index 41aa01750d..0e3a463533 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys import datetime as dt from decimal import Decimal from functools import wraps @@ -531,6 +532,17 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 ) raise TypeError(msg) return Version.V1.dataframe(InterchangeFrame(native_object), level="interchange") + + # TODO @mp: this should be connection point to plugin + + if sys.version_info < (3, 10): + from importlib_metadata import entry_points + else: + from importlib.metadata import entry_points + + discovered_plugins = entry_points(group='narwhals.plugins') + + print(discovered_plugins) if not pass_through: msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(native_object)}" diff --git a/daft-plugin/setup.py b/plugins/daft-plugin/setup.py similarity index 100% rename from daft-plugin/setup.py rename to plugins/daft-plugin/setup.py diff --git a/daft-plugin/src/__init__.py b/plugins/daft-plugin/src/__init__.py similarity index 100% rename from daft-plugin/src/__init__.py rename to plugins/daft-plugin/src/__init__.py diff --git a/daft-plugin/src/on_by_default.py b/plugins/daft-plugin/src/on_by_default.py similarity index 100% rename from daft-plugin/src/on_by_default.py rename to plugins/daft-plugin/src/on_by_default.py diff --git a/pyproject.toml b/pyproject.toml index 8ba95a77eb..b49cd034e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -342,3 +342,6 @@ ignore = [ "../../../**/Lib", # stdlib "../../../**/typeshed*" # typeshed-fallback ] + +[project.entry-points.'narwhals.plugins'] +a = 'daft-plugin' From 8d4cda6b06a33848646b117e53ba45f03c3f008c Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 13 Aug 2025 15:12:38 +0100 Subject: [PATCH 03/88] linting --- narwhals/temp_plugin_test.py | 6 ++++-- narwhals/translate.py | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/narwhals/temp_plugin_test.py b/narwhals/temp_plugin_test.py index b7c4f17ed9..408d0c8c2a 100644 --- a/narwhals/temp_plugin_test.py +++ b/narwhals/temp_plugin_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys if sys.version_info < (3, 10): @@ -5,6 +7,6 @@ else: from importlib.metadata import entry_points -discovered_plugins = entry_points(group='narwhals.plugins') +discovered_plugins = entry_points(group="narwhals.plugins") -print(discovered_plugins) \ No newline at end of file +print(discovered_plugins) diff --git a/narwhals/translate.py b/narwhals/translate.py index 0e3a463533..4d1ee56ccf 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -1,7 +1,7 @@ from __future__ import annotations -import sys import datetime as dt +import sys from decimal import Decimal from functools import wraps from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, overload @@ -532,16 +532,16 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 ) raise TypeError(msg) return Version.V1.dataframe(InterchangeFrame(native_object), level="interchange") - + # TODO @mp: this should be connection point to plugin if sys.version_info < (3, 10): from importlib_metadata import entry_points else: from importlib.metadata import entry_points - - discovered_plugins = entry_points(group='narwhals.plugins') - + + discovered_plugins = entry_points(group="narwhals.plugins") + print(discovered_plugins) if not pass_through: From cfd156f50034a8e65cb29da4a9b0352cd46c4229 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 13 Aug 2025 17:27:33 +0100 Subject: [PATCH 04/88] trying to see an effect of plugin --- narwhals/translate.py | 9 +++++++++ plugins/daft-plugin/setup.py | 2 +- plugins/daft-plugin/src/on_by_default.py | 13 ++++--------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index 4d1ee56ccf..06cfc63ca4 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -535,6 +535,7 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 # TODO @mp: this should be connection point to plugin + # not sure first if statement is needed if sys.version_info < (3, 10): from importlib_metadata import entry_points else: @@ -544,6 +545,14 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 print(discovered_plugins) + """ + TODO @mp: need logic to go over all the entry points found, and if daft found, + (others later), we return the daft dataframe from_native. I think the transformation has + to happen inside the daft_plugin, first would just like to see that I can actually read + it in + + """ + if not pass_through: msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(native_object)}" raise TypeError(msg) diff --git a/plugins/daft-plugin/setup.py b/plugins/daft-plugin/setup.py index b432085bfa..a258c231ba 100644 --- a/plugins/daft-plugin/setup.py +++ b/plugins/daft-plugin/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="daft-plugin", entry_points={ - "daft.extension": [ + "daft-plugin.extension": [ "X1 = daft-plugin:ExampleOne" # "X2 = daft-plugin:ExampleTwo", ] diff --git a/plugins/daft-plugin/src/on_by_default.py b/plugins/daft-plugin/src/on_by_default.py index b7ab8158b1..34c3873b8b 100644 --- a/plugins/daft-plugin/src/on_by_default.py +++ b/plugins/daft-plugin/src/on_by_default.py @@ -2,18 +2,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from collections.abc import Generator - class ExampleOne: """First Example Plugin.""" - def __init__(self, tree: Any) -> None: - self.tree = tree + def __init__(self): + pass - def run(self) -> Generator[Any, Any, None]: + def run(self) -> None: """Do nothing.""" - yield from [] + print('ExampleOne just ran!') From e3a2f3b0b21d5208cb5821d2ed902fa85b77c9f5 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Thu, 14 Aug 2025 14:33:40 +0100 Subject: [PATCH 05/88] cleanup after pair session --- narwhals/temp_plugin_test.py | 12 ------------ plugins/daft-plugin/setup.py | 13 ------------- plugins/daft-plugin/src/__init__.py | 11 ----------- plugins/daft-plugin/src/on_by_default.py | 14 -------------- pyproject.toml | 5 +---- t.py | 4 ++++ 6 files changed, 5 insertions(+), 54 deletions(-) delete mode 100644 narwhals/temp_plugin_test.py delete mode 100644 plugins/daft-plugin/setup.py delete mode 100644 plugins/daft-plugin/src/__init__.py delete mode 100644 plugins/daft-plugin/src/on_by_default.py create mode 100644 t.py diff --git a/narwhals/temp_plugin_test.py b/narwhals/temp_plugin_test.py deleted file mode 100644 index 408d0c8c2a..0000000000 --- a/narwhals/temp_plugin_test.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import annotations - -import sys - -if sys.version_info < (3, 10): - from importlib_metadata import entry_points -else: - from importlib.metadata import entry_points - -discovered_plugins = entry_points(group="narwhals.plugins") - -print(discovered_plugins) diff --git a/plugins/daft-plugin/setup.py b/plugins/daft-plugin/setup.py deleted file mode 100644 index a258c231ba..0000000000 --- a/plugins/daft-plugin/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -from __future__ import annotations - -import setuptools - -setuptools.setup( - name="daft-plugin", - entry_points={ - "daft-plugin.extension": [ - "X1 = daft-plugin:ExampleOne" - # "X2 = daft-plugin:ExampleTwo", - ] - }, -) diff --git a/plugins/daft-plugin/src/__init__.py b/plugins/daft-plugin/src/__init__.py deleted file mode 100644 index a8af1be724..0000000000 --- a/plugins/daft-plugin/src/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Module for an example Flake8 plugin.""" - -from __future__ import annotations - -# from .off_by_default import ExampleTwo -from .on_by_default import ExampleOne - -__all__ = ( - "ExampleOne", - # "ExampleTwo", -) diff --git a/plugins/daft-plugin/src/on_by_default.py b/plugins/daft-plugin/src/on_by_default.py deleted file mode 100644 index 34c3873b8b..0000000000 --- a/plugins/daft-plugin/src/on_by_default.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Our first example plugin.""" - -from __future__ import annotations - - -class ExampleOne: - """First Example Plugin.""" - - def __init__(self): - pass - - def run(self) -> None: - """Do nothing.""" - print('ExampleOne just ran!') diff --git a/pyproject.toml b/pyproject.toml index b49cd034e7..1eb5d038d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -341,7 +341,4 @@ ignore = [ "../.venv/", "../../../**/Lib", # stdlib "../../../**/typeshed*" # typeshed-fallback -] - -[project.entry-points.'narwhals.plugins'] -a = 'daft-plugin' +] \ No newline at end of file diff --git a/t.py b/t.py new file mode 100644 index 0000000000..b7d6b6fb99 --- /dev/null +++ b/t.py @@ -0,0 +1,4 @@ + +import daft +import narwhals as nw +nw.from_native(daft.from_pydict({'a': [1,2,3]})) \ No newline at end of file From f65d7bf64fd9222ecf4d3561263d04adb26b7717 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 13:34:27 +0000 Subject: [PATCH 06/88] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyproject.toml | 2 +- t.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1eb5d038d3..8ba95a77eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -341,4 +341,4 @@ ignore = [ "../.venv/", "../../../**/Lib", # stdlib "../../../**/typeshed*" # typeshed-fallback -] \ No newline at end of file +] diff --git a/t.py b/t.py index b7d6b6fb99..b823fbe245 100644 --- a/t.py +++ b/t.py @@ -1,4 +1,7 @@ +from __future__ import annotations import daft + import narwhals as nw -nw.from_native(daft.from_pydict({'a': [1,2,3]})) \ No newline at end of file + +nw.from_native(daft.from_pydict({"a": [1, 2, 3]})) From eee9068571838ee9c469a8ca97e630dd91f27cc5 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Thu, 14 Aug 2025 16:27:00 +0100 Subject: [PATCH 07/88] think we're managing to import the class --- narwhals/translate.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index 06cfc63ca4..35a4c6b721 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -535,15 +535,25 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 # TODO @mp: this should be connection point to plugin - # not sure first if statement is needed - if sys.version_info < (3, 10): - from importlib_metadata import entry_points - else: - from importlib.metadata import entry_points + from importlib.metadata import entry_points discovered_plugins = entry_points(group="narwhals.plugins") - print(discovered_plugins) + for plugin in discovered_plugins: + + obj = plugin.load() + frame = obj.dataframe.DaftLazyFrame + print(type(frame)) + + #from obj.dataframe import DaftLazyFrame + # try: + # df_compliant = LazyFrame(df_native, version=Version.MAIN) + # return df_compliant.to_narwhals() + # except: + # # try the next plugin + # continue + + """ TODO @mp: need logic to go over all the entry points found, and if daft found, From c3a8c4db9aaadea14572b1b5f640efa8d83f963e Mon Sep 17 00:00:00 2001 From: ym-pett Date: Thu, 14 Aug 2025 16:49:09 +0100 Subject: [PATCH 08/88] changes mean we can read daft df, joy --- narwhals/translate.py | 13 ++++++------- t.py | 5 ++++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index 35a4c6b721..ef5ea37cf2 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -543,15 +543,14 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 obj = plugin.load() frame = obj.dataframe.DaftLazyFrame - print(type(frame)) #from obj.dataframe import DaftLazyFrame - # try: - # df_compliant = LazyFrame(df_native, version=Version.MAIN) - # return df_compliant.to_narwhals() - # except: - # # try the next plugin - # continue + try: + df_compliant = frame(native_object, version=Version.MAIN) + return df_compliant.to_narwhals() + except: + # try the next plugin + continue diff --git a/t.py b/t.py index b823fbe245..c4aeb77f7c 100644 --- a/t.py +++ b/t.py @@ -4,4 +4,7 @@ import narwhals as nw -nw.from_native(daft.from_pydict({"a": [1, 2, 3]})) +df = nw.from_native(daft.from_pydict({"a": [1, 2, 3]})) + +print(df) +print(type(df)) From c4611fe99de82a5d750d74e4508f8f5305f3f4ba Mon Sep 17 00:00:00 2001 From: ym-pett Date: Sat, 16 Aug 2025 12:40:07 +0100 Subject: [PATCH 09/88] nicer error handling, not there yet --- narwhals/translate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index ef5ea37cf2..1870d4cd7e 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -548,12 +548,12 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 try: df_compliant = frame(native_object, version=Version.MAIN) return df_compliant.to_narwhals() - except: + # @mp: not sure if correct exception, check. Improve error message + except TypeError as e: + print(f'Cannot read it the dataframe, reason {e}. Currently only supporting daft plugins') # try the next plugin continue - - - + """ TODO @mp: need logic to go over all the entry points found, and if daft found, (others later), we return the daft dataframe from_native. I think the transformation has From 90ad9739749c403a52eb115b8729e4fc5e4eab1d Mon Sep 17 00:00:00 2001 From: ym-pett Date: Sat, 16 Aug 2025 12:43:44 +0100 Subject: [PATCH 10/88] nicer error handling, not there yet --- narwhals/translate.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index 1870d4cd7e..99e8078e9e 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -1,7 +1,6 @@ from __future__ import annotations import datetime as dt -import sys from decimal import Decimal from functools import wraps from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, overload @@ -544,16 +543,16 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 obj = plugin.load() frame = obj.dataframe.DaftLazyFrame - #from obj.dataframe import DaftLazyFrame + #from obj.dataframe import DaftLazyFrame doesn't work directly! try: df_compliant = frame(native_object, version=Version.MAIN) return df_compliant.to_narwhals() # @mp: not sure if correct exception, check. Improve error message - except TypeError as e: - print(f'Cannot read it the dataframe, reason {e}. Currently only supporting daft plugins') + except TypeError as e: + print(f"Cannot read it the dataframe, reason {e}. Currently only supporting daft plugins") # try the next plugin continue - + """ TODO @mp: need logic to go over all the entry points found, and if daft found, (others later), we return the daft dataframe from_native. I think the transformation has From 76232dfb9a35a7362254c2d123cb0fbd67c7e8ea Mon Sep 17 00:00:00 2001 From: ym-pett Date: Sat, 16 Aug 2025 15:13:33 +0100 Subject: [PATCH 11/88] added some thoughts, currently going in circles --- narwhals/_polars/dataframe.py | 4 ++++ narwhals/translate.py | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/narwhals/_polars/dataframe.py b/narwhals/_polars/dataframe.py index 9f7741db6f..8d127e2c4f 100644 --- a/narwhals/_polars/dataframe.py +++ b/narwhals/_polars/dataframe.py @@ -95,6 +95,10 @@ NativePolarsFrame = TypeVar("NativePolarsFrame", pl.DataFrame, pl.LazyFrame) +# @mp: can we make a class here which is more generic than all the other framework specif frames? +# then have the other frames inherit from this, but it should be generic enough that we can use +# it also to read plugins? + class PolarsBaseFrame(Generic[NativePolarsFrame]): drop_nulls: Method[Self] diff --git a/narwhals/translate.py b/narwhals/translate.py index 99e8078e9e..4c5f0244dc 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -506,6 +506,13 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 raise TypeError(msg) return native_object return ns_spark.compliant.from_native(native_object).to_narwhals() + + ''' + @mp: need function here which can read frames as long as they're of the type provided + in the plugins. that could then be called below + how to make it generic enough but not so generic as to break downsteam.. + ''' + # Interchange protocol if _supports_dataframe_interchange(native_object): @@ -549,7 +556,7 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 return df_compliant.to_narwhals() # @mp: not sure if correct exception, check. Improve error message except TypeError as e: - print(f"Cannot read it the dataframe, reason {e}. Currently only supporting daft plugins") + print(f"Cannot read in the dataframe, reason {e}. Currently only supporting daft plugins") # try the next plugin continue From ebc1a8f42a4ab8634bf5a538a892a341e798541b Mon Sep 17 00:00:00 2001 From: ym-pett Date: Sat, 16 Aug 2025 15:26:16 +0100 Subject: [PATCH 12/88] added some thoughts, currently going in circles --- narwhals/translate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/narwhals/translate.py b/narwhals/translate.py index 4c5f0244dc..6a600cc470 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -548,6 +548,10 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 for plugin in discovered_plugins: obj = plugin.load() + + # instead of the below, have something more generic like the + # _load_backend in pandas + # https://github.com/pandas-dev/pandas/blob/d95bf9a04f10590fff41e75de94c321a8743af72/pandas/plotting/_core.py#L1872 frame = obj.dataframe.DaftLazyFrame #from obj.dataframe import DaftLazyFrame doesn't work directly! From 15d9c8cba4a46778629c64a60632bc679039051d Mon Sep 17 00:00:00 2001 From: ym-pett Date: Mon, 18 Aug 2025 14:44:06 +0100 Subject: [PATCH 13/88] work from pair session --- narwhals/_polars/dataframe.py | 4 --- narwhals/translate.py | 59 +++++++++++------------------------ 2 files changed, 19 insertions(+), 44 deletions(-) diff --git a/narwhals/_polars/dataframe.py b/narwhals/_polars/dataframe.py index 8d127e2c4f..9f7741db6f 100644 --- a/narwhals/_polars/dataframe.py +++ b/narwhals/_polars/dataframe.py @@ -95,10 +95,6 @@ NativePolarsFrame = TypeVar("NativePolarsFrame", pl.DataFrame, pl.LazyFrame) -# @mp: can we make a class here which is more generic than all the other framework specif frames? -# then have the other frames inherit from this, but it should be generic enough that we can use -# it also to read plugins? - class PolarsBaseFrame(Generic[NativePolarsFrame]): drop_nulls: Method[Self] diff --git a/narwhals/translate.py b/narwhals/translate.py index 6a600cc470..65c0b0c14c 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -347,6 +347,25 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 msg = "Invalid parameter combination: `eager_only=True` and `eager_or_interchange_only=True`" raise ValueError(msg) + from importlib.metadata import entry_points + + discovered_plugins = entry_points(group="narwhals.plugins") + + for plugin in discovered_plugins: + obj = plugin.load() + + try: + df_compliant = obj.from_native( + native_object, eager_only=eager_only, series_only=series_only + ) + # @mp: not sure if correct exception, check. Improve error message + except TypeError: + # @mp:TODO add good error message + # try the next plugin + continue + else: + return df_compliant.to_narwhals() + # Extensions if is_compliant_dataframe(native_object): if series_only: @@ -506,13 +525,6 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 raise TypeError(msg) return native_object return ns_spark.compliant.from_native(native_object).to_narwhals() - - ''' - @mp: need function here which can read frames as long as they're of the type provided - in the plugins. that could then be called below - how to make it generic enough but not so generic as to break downsteam.. - ''' - # Interchange protocol if _supports_dataframe_interchange(native_object): @@ -539,39 +551,6 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 raise TypeError(msg) return Version.V1.dataframe(InterchangeFrame(native_object), level="interchange") - # TODO @mp: this should be connection point to plugin - - from importlib.metadata import entry_points - - discovered_plugins = entry_points(group="narwhals.plugins") - - for plugin in discovered_plugins: - - obj = plugin.load() - - # instead of the below, have something more generic like the - # _load_backend in pandas - # https://github.com/pandas-dev/pandas/blob/d95bf9a04f10590fff41e75de94c321a8743af72/pandas/plotting/_core.py#L1872 - frame = obj.dataframe.DaftLazyFrame - - #from obj.dataframe import DaftLazyFrame doesn't work directly! - try: - df_compliant = frame(native_object, version=Version.MAIN) - return df_compliant.to_narwhals() - # @mp: not sure if correct exception, check. Improve error message - except TypeError as e: - print(f"Cannot read in the dataframe, reason {e}. Currently only supporting daft plugins") - # try the next plugin - continue - - """ - TODO @mp: need logic to go over all the entry points found, and if daft found, - (others later), we return the daft dataframe from_native. I think the transformation has - to happen inside the daft_plugin, first would just like to see that I can actually read - it in - - """ - if not pass_through: msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(native_object)}" raise TypeError(msg) From 16832f99f66fbf3a8e056f2af0a5eee91e63964c Mon Sep 17 00:00:00 2001 From: ym-pett Date: Mon, 18 Aug 2025 15:25:02 +0100 Subject: [PATCH 14/88] more explicit error handling --- narwhals/translate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index 65c0b0c14c..3db2b2bd9d 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -358,10 +358,11 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 df_compliant = obj.from_native( native_object, eager_only=eager_only, series_only=series_only ) - # @mp: not sure if correct exception, check. Improve error message except TypeError: - # @mp:TODO add good error message - # try the next plugin + if "daft" in str(type(native_object)): + msg = "Hint: you might be missing the `narwhals-daft` plugin" + raise RuntimeError(msg) + # continue looping over the plugins continue else: return df_compliant.to_narwhals() From 9e11a8fac9771a3bfb094e86ede5c074ba360336 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Mon, 18 Aug 2025 15:35:52 +0100 Subject: [PATCH 15/88] error handling now passes ruff check --- narwhals/translate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index 3db2b2bd9d..f8a2521ba9 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -358,10 +358,10 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 df_compliant = obj.from_native( native_object, eager_only=eager_only, series_only=series_only ) - except TypeError: + except RuntimeError as e: if "daft" in str(type(native_object)): msg = "Hint: you might be missing the `narwhals-daft` plugin" - raise RuntimeError(msg) + raise RuntimeError(msg) from e # continue looping over the plugins continue else: From d8663d8fe7421f6e7cbab42e52c601ab25912014 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Tue, 19 Aug 2025 09:52:13 +0100 Subject: [PATCH 16/88] moved plugins back to end, raising general Exception --- narwhals/translate.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index f8a2521ba9..acefc2aa1f 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -347,26 +347,6 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 msg = "Invalid parameter combination: `eager_only=True` and `eager_or_interchange_only=True`" raise ValueError(msg) - from importlib.metadata import entry_points - - discovered_plugins = entry_points(group="narwhals.plugins") - - for plugin in discovered_plugins: - obj = plugin.load() - - try: - df_compliant = obj.from_native( - native_object, eager_only=eager_only, series_only=series_only - ) - except RuntimeError as e: - if "daft" in str(type(native_object)): - msg = "Hint: you might be missing the `narwhals-daft` plugin" - raise RuntimeError(msg) from e - # continue looping over the plugins - continue - else: - return df_compliant.to_narwhals() - # Extensions if is_compliant_dataframe(native_object): if series_only: @@ -552,6 +532,27 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 raise TypeError(msg) return Version.V1.dataframe(InterchangeFrame(native_object), level="interchange") + from importlib.metadata import entry_points + + discovered_plugins = entry_points(group="narwhals.plugins") + + for plugin in discovered_plugins: + obj = plugin.load() + + try: + print(type(native_object)) + df_compliant = obj.from_native( + native_object, eager_only=False, series_only=False + ) + except Exception as e: + if "daft" in str(type(native_object)): + msg = "Hint: you might be missing the `narwhals-daft` plugin" + raise Exception(msg) from e + # continue looping over the plugins + continue + else: + return df_compliant.to_narwhals() + if not pass_through: msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(native_object)}" raise TypeError(msg) From 34e7fb6affe9f6f8c080240b810d224457d90556 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Tue, 19 Aug 2025 10:04:20 +0100 Subject: [PATCH 17/88] silencing ruff errors, deleted t.py --- narwhals/translate.py | 3 +-- t.py | 10 ---------- 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 t.py diff --git a/narwhals/translate.py b/narwhals/translate.py index acefc2aa1f..c622c1d6f1 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -540,14 +540,13 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 obj = plugin.load() try: - print(type(native_object)) df_compliant = obj.from_native( native_object, eager_only=False, series_only=False ) except Exception as e: if "daft" in str(type(native_object)): msg = "Hint: you might be missing the `narwhals-daft` plugin" - raise Exception(msg) from e + raise Exception(msg) from e # noqa: TRY002 # continue looping over the plugins continue else: diff --git a/t.py b/t.py deleted file mode 100644 index c4aeb77f7c..0000000000 --- a/t.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import annotations - -import daft - -import narwhals as nw - -df = nw.from_native(daft.from_pydict({"a": [1, 2, 3]})) - -print(df) -print(type(df)) From 72a5df498199bf21fc41ca3cbb493b5c225933cb Mon Sep 17 00:00:00 2001 From: ym-pett Date: Tue, 19 Aug 2025 15:56:54 +0100 Subject: [PATCH 18/88] checking for version and preventing tests on plugin codeblock --- narwhals/translate.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index c622c1d6f1..1ea201dd4a 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -1,6 +1,7 @@ from __future__ import annotations import datetime as dt +import sys from decimal import Decimal from functools import wraps from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, overload @@ -536,21 +537,22 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 discovered_plugins = entry_points(group="narwhals.plugins") - for plugin in discovered_plugins: - obj = plugin.load() + if sys.version_info >= (3, 10): + for plugin in discovered_plugins: + obj = plugin.load() # pragma: no cover - try: - df_compliant = obj.from_native( - native_object, eager_only=False, series_only=False - ) - except Exception as e: - if "daft" in str(type(native_object)): - msg = "Hint: you might be missing the `narwhals-daft` plugin" - raise Exception(msg) from e # noqa: TRY002 - # continue looping over the plugins - continue - else: - return df_compliant.to_narwhals() + try: + df_compliant = obj.from_native( # pragma: no cover + native_object, eager_only=False, series_only=False + ) + except Exception as e: + if "daft" in str(type(native_object)): # pragma: no cover + msg = "Hint: you might be missing the `narwhals-daft` plugin" + raise Exception(msg) from e # noqa: TRY002 + # continue looping over the plugins + continue # pragma: no cover + else: + return df_compliant.to_narwhals() # pragma: no cover if not pass_through: msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(native_object)}" From e783af73d880d5675991c3eb1e4a93d9f746914f Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 20 Aug 2025 11:21:48 +0100 Subject: [PATCH 19/88] wip implementing marco's proposal --- narwhals/translate.py | 56 +++++++++++++++++++++++++++---------------- t.py | 10 ++++++++ 2 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 t.py diff --git a/narwhals/translate.py b/narwhals/translate.py index 1ea201dd4a..624229cb8e 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -347,6 +347,41 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 if eager_only and eager_or_interchange_only: msg = "Invalid parameter combination: `eager_only=True` and `eager_or_interchange_only=True`" raise ValueError(msg) + + if sys.version_info >= (3, 10): + + print('starting down the if path') + + from importlib.metadata import entry_points + + discovered_plugins = entry_points(group="narwhals.plugins") + + print(discovered_plugins) + + for plugin in discovered_plugins: + obj = plugin.load() # pragma: no cover + if obj.is_native_object(native_object): + native_object = obj.from_native(native_object,version=version) + # df_compliant = obj.from_native(native_object,version=version) + # native_object = df_compliant.to_narwhals() + break + + # if sys.version_info >= (3, 10): + # for plugin in discovered_plugins: + # obj = plugin.load() # pragma: no cover + + # try: + # df_compliant = obj.from_native( # pragma: no cover + # native_object, eager_only=False, series_only=False + # ) + # except Exception as e: + # if "daft" in str(type(native_object)): # pragma: no cover + # msg = "Hint: you might be missing the `narwhals-daft` plugin" + # raise Exception(msg) from e # noqa: TRY002 + # # continue looping over the plugins + # continue # pragma: no cover + # else: + # return df_compliant.to_narwhals() # pragma: no cover # Extensions if is_compliant_dataframe(native_object): @@ -533,27 +568,6 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 raise TypeError(msg) return Version.V1.dataframe(InterchangeFrame(native_object), level="interchange") - from importlib.metadata import entry_points - - discovered_plugins = entry_points(group="narwhals.plugins") - - if sys.version_info >= (3, 10): - for plugin in discovered_plugins: - obj = plugin.load() # pragma: no cover - - try: - df_compliant = obj.from_native( # pragma: no cover - native_object, eager_only=False, series_only=False - ) - except Exception as e: - if "daft" in str(type(native_object)): # pragma: no cover - msg = "Hint: you might be missing the `narwhals-daft` plugin" - raise Exception(msg) from e # noqa: TRY002 - # continue looping over the plugins - continue # pragma: no cover - else: - return df_compliant.to_narwhals() # pragma: no cover - if not pass_through: msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(native_object)}" raise TypeError(msg) diff --git a/t.py b/t.py new file mode 100644 index 0000000000..c4aeb77f7c --- /dev/null +++ b/t.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import daft + +import narwhals as nw + +df = nw.from_native(daft.from_pydict({"a": [1, 2, 3]})) + +print(df) +print(type(df)) From 4cf2f962a765331590fec9f48740c12ebb83fd79 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 20 Aug 2025 12:50:40 +0100 Subject: [PATCH 20/88] new version passes tests --- narwhals/translate.py | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index 624229cb8e..33943f6903 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -347,42 +347,20 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 if eager_only and eager_or_interchange_only: msg = "Invalid parameter combination: `eager_only=True` and `eager_or_interchange_only=True`" raise ValueError(msg) - - if sys.version_info >= (3, 10): - - print('starting down the if path') + if sys.version_info >= (3, 10): from importlib.metadata import entry_points discovered_plugins = entry_points(group="narwhals.plugins") - print(discovered_plugins) - for plugin in discovered_plugins: obj = plugin.load() # pragma: no cover if obj.is_native_object(native_object): - native_object = obj.from_native(native_object,version=version) - # df_compliant = obj.from_native(native_object,version=version) - # native_object = df_compliant.to_narwhals() + native_object = obj.from_native( + native_object, eager_only=False, series_only=False + ) break - # if sys.version_info >= (3, 10): - # for plugin in discovered_plugins: - # obj = plugin.load() # pragma: no cover - - # try: - # df_compliant = obj.from_native( # pragma: no cover - # native_object, eager_only=False, series_only=False - # ) - # except Exception as e: - # if "daft" in str(type(native_object)): # pragma: no cover - # msg = "Hint: you might be missing the `narwhals-daft` plugin" - # raise Exception(msg) from e # noqa: TRY002 - # # continue looping over the plugins - # continue # pragma: no cover - # else: - # return df_compliant.to_narwhals() # pragma: no cover - # Extensions if is_compliant_dataframe(native_object): if series_only: From a52a6bba6e058c2c69e02375c60afe6e7fece437 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 20 Aug 2025 18:32:04 +0100 Subject: [PATCH 21/88] discover_plugins function and fixed pragma no cover --- narwhals/_utils.py | 8 ++++++++ narwhals/translate.py | 15 +++++++++------ t.py | 10 ---------- 3 files changed, 17 insertions(+), 16 deletions(-) delete mode 100644 t.py diff --git a/narwhals/_utils.py b/narwhals/_utils.py index 8073f6a886..6aefa1ead7 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -50,6 +50,7 @@ if TYPE_CHECKING: from collections.abc import Set # noqa: PYI025 + from importlib.metadata import EntryPoints from types import ModuleType import pandas as pd @@ -2037,3 +2038,10 @@ def deep_attrgetter(attr: str, *nested: str) -> attrgetter[Any]: def deep_getattr(obj: Any, name_1: str, *nested: str) -> Any: """Perform a nested attribute lookup on `obj`.""" return deep_attrgetter(name_1, *nested)(obj) + + +@lru_cache(maxsize=64) +def discover_plugins(group: str = "narwhals.plugins") -> EntryPoints: + from importlib.metadata import entry_points + + return entry_points(group=group) diff --git a/narwhals/translate.py b/narwhals/translate.py index 33943f6903..2b643f9e87 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -13,7 +13,12 @@ is_native_polars, is_native_spark_like, ) -from narwhals._utils import Implementation, Version, has_native_namespace +from narwhals._utils import ( + Implementation, + Version, + discover_plugins, + has_native_namespace, +) from narwhals.dependencies import ( get_dask_expr, get_numpy, @@ -349,12 +354,10 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 raise ValueError(msg) if sys.version_info >= (3, 10): - from importlib.metadata import entry_points - - discovered_plugins = entry_points(group="narwhals.plugins") + discovered_plugins = discover_plugins(group="narwhals.plugins") - for plugin in discovered_plugins: - obj = plugin.load() # pragma: no cover + for plugin in discovered_plugins: # pragma: no cover + obj = plugin.load() if obj.is_native_object(native_object): native_object = obj.from_native( native_object, eager_only=False, series_only=False diff --git a/t.py b/t.py deleted file mode 100644 index c4aeb77f7c..0000000000 --- a/t.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import annotations - -import daft - -import narwhals as nw - -df = nw.from_native(daft.from_pydict({"a": [1, 2, 3]})) - -print(df) -print(type(df)) From a4e539a0de9935f113b5d6bd63bc9afec6d3e03b Mon Sep 17 00:00:00 2001 From: ym-pett Date: Fri, 22 Aug 2025 10:37:44 +0100 Subject: [PATCH 22/88] added pragma, removed group argument --- narwhals/_utils.py | 6 +++--- narwhals/translate.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/narwhals/_utils.py b/narwhals/_utils.py index 6aefa1ead7..8561f7a578 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -2040,8 +2040,8 @@ def deep_getattr(obj: Any, name_1: str, *nested: str) -> Any: return deep_attrgetter(name_1, *nested)(obj) -@lru_cache(maxsize=64) -def discover_plugins(group: str = "narwhals.plugins") -> EntryPoints: +@lru_cache(maxsize=8) +def discover_plugins() -> EntryPoints: from importlib.metadata import entry_points - return entry_points(group=group) + return entry_points(group="narwhals.plugins") diff --git a/narwhals/translate.py b/narwhals/translate.py index 2b643f9e87..c7001b4809 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -353,8 +353,8 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 msg = "Invalid parameter combination: `eager_only=True` and `eager_or_interchange_only=True`" raise ValueError(msg) - if sys.version_info >= (3, 10): - discovered_plugins = discover_plugins(group="narwhals.plugins") + if sys.version_info >= (3, 10): # pragma: no cover + discovered_plugins = discover_plugins() for plugin in discovered_plugins: # pragma: no cover obj = plugin.load() From 02d544a2bace19139920f7d88c71569fe7acac4e Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:25:56 +0100 Subject: [PATCH 23/88] wip: add test-plugin --- .pre-commit-config.yaml | 5 +- narwhals/translate.py | 4 +- tests/test-plugin/pyproject.toml | 10 ++++ tests/test-plugin/test_plugin/__init__.py | 20 +++++++ tests/test-plugin/test_plugin/dataframe.py | 62 ++++++++++++++++++++++ 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 tests/test-plugin/pyproject.toml create mode 100644 tests/test-plugin/test_plugin/__init__.py create mode 100644 tests/test-plugin/test_plugin/dataframe.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99a8357830..10c26b09df 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -94,7 +94,10 @@ repos: rev: v5.0.0 hooks: - id: name-tests-test - exclude: ^tests/utils\.py + exclude: | + (?x) + ^(tests/utils\.py) + |^(tests/test-plugin/) - id: no-commit-to-branch - id: end-of-file-fixer exclude: .svg$ diff --git a/narwhals/translate.py b/narwhals/translate.py index ea1e093083..3310cec8c6 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -361,9 +361,7 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 for plugin in discovered_plugins: # pragma: no cover obj = plugin.load() if obj.is_native_object(native_object): - native_object = obj.from_native( - native_object, eager_only=False, series_only=False - ) + native_object = obj.from_native(native_object) break # Extensions diff --git a/tests/test-plugin/pyproject.toml b/tests/test-plugin/pyproject.toml new file mode 100644 index 0000000000..1deb3ef33b --- /dev/null +++ b/tests/test-plugin/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "test_plugin" +version = "0.1.0" + +[project.entry-points.'narwhals.plugins'] +test-plugin = 'test_plugin' diff --git a/tests/test-plugin/test_plugin/__init__.py b/tests/test-plugin/test_plugin/__init__.py new file mode 100644 index 0000000000..5d47feab4b --- /dev/null +++ b/tests/test-plugin/test_plugin/__init__.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from typing_extensions import TypeIs + +from narwhals.utils import Version + +if TYPE_CHECKING: + from test_plugin.dataframe import DictFrame, DictLazyFrame + + +def from_native(native_object: DictFrame) -> DictLazyFrame: + from test_plugin.dataframe import DictLazyFrame + + return DictLazyFrame(native_object, version=Version.MAIN) + + +def is_native_object(obj: Any) -> TypeIs[DictFrame]: + return isinstance(obj, dict) diff --git a/tests/test-plugin/test_plugin/dataframe.py b/tests/test-plugin/test_plugin/dataframe.py new file mode 100644 index 0000000000..60c83977b1 --- /dev/null +++ b/tests/test-plugin/test_plugin/dataframe.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, TypeAlias + +import daft +import daft.exceptions +import daft.functions + +from narwhals._utils import ( + Implementation, + ValidateBackendVersion, + Version, + not_implemented, +) +from narwhals.typing import CompliantLazyFrame + +DictFrame: TypeAlias = dict[str, list[Any]] + +if TYPE_CHECKING: + from typing_extensions import Self, TypeIs + + from narwhals._utils import _LimitedContext + from narwhals.dataframe import LazyFrame + from narwhals.dtypes import DType + + +class DictLazyFrame( + CompliantLazyFrame[Any, "DictFrame", "LazyFrame[DictFrame]"], ValidateBackendVersion +): + _implementation = Implementation.UNKNOWN + + def __init__(self, native_dataframe: DictFrame, *, version: Version) -> None: + self._native_frame: DictFrame = native_dataframe + self._version = version + self._cached_schema: dict[str, DType] | None = None + self._cached_columns: list[str] | None = None + + @staticmethod + def _is_native(obj: daft.DataFrame | Any) -> TypeIs[daft.DataFrame]: + return isinstance(obj, daft.DataFrame) + + @classmethod + def from_native(cls, data: daft.DataFrame, /, *, context: _LimitedContext) -> Self: + return cls(data, version=context._version) + + def to_narwhals(self) -> LazyFrame[daft.DataFrame]: + return self._version.lazyframe(self, level="lazy") + + def __narwhals_lazyframe__(self) -> Self: + return self + + def _with_version(self, version: Version) -> Self: + return self.__class__(self._native_frame, version=version) + + @property + def columns(self) -> list[str]: + return list(self._native_frame.keys()) + + group_by = not_implemented() + join_asof = not_implemented() + explode = not_implemented() + sink_parquet = not_implemented() From 0952cd9b5d2ba35cf2f2e4fee13ce1b088ea58c2 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:43:39 +0100 Subject: [PATCH 24/88] wip --- tests/test-plugin/test_plugin/dataframe.py | 28 +++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test-plugin/test_plugin/dataframe.py b/tests/test-plugin/test_plugin/dataframe.py index 60c83977b1..95f679654a 100644 --- a/tests/test-plugin/test_plugin/dataframe.py +++ b/tests/test-plugin/test_plugin/dataframe.py @@ -56,7 +56,33 @@ def _with_version(self, version: Version) -> Self: def columns(self) -> list[str]: return list(self._native_frame.keys()) + # Dunders + __narwhals_namespace__ = not_implemented() + + # Properties + schema = not_implemented() # type: ignore[assignment] + + # Helpers + _iter_columns = not_implemented() + + # Functions + aggregate = not_implemented() + collect = not_implemented() + collect_schema = not_implemented() + drop = not_implemented() + drop_nulls = not_implemented() + explode = not_implemented() + filter = not_implemented() group_by = not_implemented() + head = not_implemented() + join = not_implemented() join_asof = not_implemented() - explode = not_implemented() + rename = not_implemented() + select = not_implemented() + simple_select = not_implemented() sink_parquet = not_implemented() + sort = not_implemented() + unique = not_implemented() + unpivot = not_implemented() + with_columns = not_implemented() + with_row_index = not_implemented() From de0e5ce57f33e761e619e4526f67d2f594c3251a Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:44:20 +0100 Subject: [PATCH 25/88] wip --- tests/test-plugin/test_plugin/dataframe.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/test-plugin/test_plugin/dataframe.py b/tests/test-plugin/test_plugin/dataframe.py index 95f679654a..f182c70e42 100644 --- a/tests/test-plugin/test_plugin/dataframe.py +++ b/tests/test-plugin/test_plugin/dataframe.py @@ -19,9 +19,7 @@ if TYPE_CHECKING: from typing_extensions import Self, TypeIs - from narwhals._utils import _LimitedContext from narwhals.dataframe import LazyFrame - from narwhals.dtypes import DType class DictLazyFrame( @@ -32,17 +30,11 @@ class DictLazyFrame( def __init__(self, native_dataframe: DictFrame, *, version: Version) -> None: self._native_frame: DictFrame = native_dataframe self._version = version - self._cached_schema: dict[str, DType] | None = None - self._cached_columns: list[str] | None = None @staticmethod def _is_native(obj: daft.DataFrame | Any) -> TypeIs[daft.DataFrame]: return isinstance(obj, daft.DataFrame) - @classmethod - def from_native(cls, data: daft.DataFrame, /, *, context: _LimitedContext) -> Self: - return cls(data, version=context._version) - def to_narwhals(self) -> LazyFrame[daft.DataFrame]: return self._version.lazyframe(self, level="lazy") @@ -73,6 +65,7 @@ def columns(self) -> list[str]: drop_nulls = not_implemented() explode = not_implemented() filter = not_implemented() + from_native = not_implemented() group_by = not_implemented() head = not_implemented() join = not_implemented() From 9dfcd0910a29138ace8f37d0868d717436801e1c Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:03:42 +0100 Subject: [PATCH 26/88] fixup --- narwhals/_utils.py | 9 ++ narwhals/translate.py | 121 +++++++++++++-------- tests/test-plugin/test_plugin/dataframe.py | 3 +- 3 files changed, 86 insertions(+), 47 deletions(-) diff --git a/narwhals/_utils.py b/narwhals/_utils.py index acd325ba6f..d77c04b0c1 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -1646,6 +1646,15 @@ def is_compliant_expr( return hasattr(obj, "__narwhals_expr__") +def is_compliant_object(obj: Any) -> bool: + return ( + is_compliant_dataframe(obj) + or is_compliant_lazyframe(obj) + or is_compliant_series(obj) + or is_compliant_expr(obj) + ) + + def is_eager_allowed(impl: Implementation, /) -> TypeIs[_EagerAllowedImpl]: """Return True if `impl` allows eager operations.""" return impl in { diff --git a/narwhals/translate.py b/narwhals/translate.py index 3310cec8c6..242bfdc7f8 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -18,6 +18,9 @@ Version, discover_plugins, has_native_namespace, + is_compliant_dataframe, + is_compliant_lazyframe, + is_compliant_series, ) from narwhals.dependencies import ( get_dask_expr, @@ -320,8 +323,8 @@ def from_native( # noqa: D417 ) -def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 - native_object: Any, +def _translate_if_compliant( # noqa: C901,PLR0911 + compliant_object: Any, *, pass_through: bool = False, eager_only: bool = False, @@ -331,72 +334,82 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 allow_series: bool | None = None, version: Version, ) -> Any: - from narwhals._interchange.dataframe import supports_dataframe_interchange - from narwhals._utils import ( - is_compliant_dataframe, - is_compliant_lazyframe, - is_compliant_series, - ) - from narwhals.dataframe import DataFrame, LazyFrame - from narwhals.series import Series - - # Early returns - if isinstance(native_object, (DataFrame, LazyFrame)) and not series_only: - return native_object - if isinstance(native_object, Series) and (series_only or allow_series): - return native_object - - if series_only: - if allow_series is False: - msg = "Invalid parameter combination: `series_only=True` and `allow_series=False`" - raise ValueError(msg) - allow_series = True - if eager_only and eager_or_interchange_only: - msg = "Invalid parameter combination: `eager_only=True` and `eager_or_interchange_only=True`" - raise ValueError(msg) - - if sys.version_info >= (3, 10): # pragma: no cover - discovered_plugins = discover_plugins() - - for plugin in discovered_plugins: # pragma: no cover - obj = plugin.load() - if obj.is_native_object(native_object): - native_object = obj.from_native(native_object) - break - - # Extensions - if is_compliant_dataframe(native_object): + if is_compliant_dataframe(compliant_object): if series_only: if not pass_through: msg = "Cannot only use `series_only` with dataframe" raise TypeError(msg) - return native_object + return compliant_object return version.dataframe( - native_object.__narwhals_dataframe__()._with_version(version), level="full" + compliant_object.__narwhals_dataframe__()._with_version(version), level="full" ) - if is_compliant_lazyframe(native_object): + if is_compliant_lazyframe(compliant_object): if series_only: if not pass_through: msg = "Cannot only use `series_only` with lazyframe" raise TypeError(msg) - return native_object + return compliant_object if eager_only or eager_or_interchange_only: if not pass_through: msg = "Cannot only use `eager_only` or `eager_or_interchange_only` with lazyframe" raise TypeError(msg) - return native_object + return compliant_object return version.lazyframe( - native_object.__narwhals_lazyframe__()._with_version(version), level="full" + compliant_object.__narwhals_lazyframe__()._with_version(version), level="full" ) - if is_compliant_series(native_object): + if is_compliant_series(compliant_object): if not allow_series: if not pass_through: msg = "Please set `allow_series=True` or `series_only=True`" raise TypeError(msg) - return native_object + return compliant_object return version.series( - native_object.__narwhals_series__()._with_version(version), level="full" + compliant_object.__narwhals_series__()._with_version(version), level="full" ) + # Object wasn't compliant, can't translate here. + return None + + +def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 + native_object: Any, + *, + pass_through: bool = False, + eager_only: bool = False, + # Interchange-level was removed after v1 + eager_or_interchange_only: bool = False, + series_only: bool = False, + allow_series: bool | None = None, + version: Version, +) -> Any: + from narwhals._interchange.dataframe import supports_dataframe_interchange + from narwhals.dataframe import DataFrame, LazyFrame + from narwhals.series import Series + + # Early returns + if isinstance(native_object, (DataFrame, LazyFrame)) and not series_only: + return native_object + if isinstance(native_object, Series) and (series_only or allow_series): + return native_object + + if series_only: + if allow_series is False: + msg = "Invalid parameter combination: `series_only=True` and `allow_series=False`" + raise ValueError(msg) + allow_series = True + if eager_only and eager_or_interchange_only: + msg = "Invalid parameter combination: `eager_only=True` and `eager_or_interchange_only=True`" + raise ValueError(msg) + + # Extensions + if translated := _translate_if_compliant( + native_object, + pass_through=pass_through, + eager_only=eager_only, + eager_or_interchange_only=eager_or_interchange_only, + series_only=series_only, + version=version, + ): + return translated # Polars if is_native_polars(native_object): @@ -549,6 +562,22 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 raise TypeError(msg) return Version.V1.dataframe(InterchangeFrame(native_object), level="interchange") + if sys.version_info >= (3, 10): + discovered_plugins = discover_plugins() + + for plugin in discovered_plugins: + obj = plugin.load() + if obj.is_native_object(native_object): + compliant_object = obj.from_native(native_object) + return _translate_if_compliant( + compliant_object, + pass_through=pass_through, + eager_only=eager_only, + eager_or_interchange_only=eager_or_interchange_only, + series_only=series_only, + version=version, + ) + if not pass_through: msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(native_object)}" raise TypeError(msg) diff --git a/tests/test-plugin/test_plugin/dataframe.py b/tests/test-plugin/test_plugin/dataframe.py index f182c70e42..449aea167b 100644 --- a/tests/test-plugin/test_plugin/dataframe.py +++ b/tests/test-plugin/test_plugin/dataframe.py @@ -23,7 +23,8 @@ class DictLazyFrame( - CompliantLazyFrame[Any, "DictFrame", "LazyFrame[DictFrame]"], ValidateBackendVersion + CompliantLazyFrame[Any, "DictFrame", "LazyFrame[DictFrame]"], + ValidateBackendVersion, # pyright: ignore[reportInvalidTypeArguments] ): _implementation = Implementation.UNKNOWN From b999e9aa3eaa67eb2ac4b88a8fb4d453c3c8fb03 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:05:44 +0100 Subject: [PATCH 27/88] remove unused function, fixup type ignore --- narwhals/_utils.py | 9 --------- tests/test-plugin/test_plugin/dataframe.py | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/narwhals/_utils.py b/narwhals/_utils.py index d77c04b0c1..acd325ba6f 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -1646,15 +1646,6 @@ def is_compliant_expr( return hasattr(obj, "__narwhals_expr__") -def is_compliant_object(obj: Any) -> bool: - return ( - is_compliant_dataframe(obj) - or is_compliant_lazyframe(obj) - or is_compliant_series(obj) - or is_compliant_expr(obj) - ) - - def is_eager_allowed(impl: Implementation, /) -> TypeIs[_EagerAllowedImpl]: """Return True if `impl` allows eager operations.""" return impl in { diff --git a/tests/test-plugin/test_plugin/dataframe.py b/tests/test-plugin/test_plugin/dataframe.py index 449aea167b..8b32523bef 100644 --- a/tests/test-plugin/test_plugin/dataframe.py +++ b/tests/test-plugin/test_plugin/dataframe.py @@ -23,8 +23,8 @@ class DictLazyFrame( - CompliantLazyFrame[Any, "DictFrame", "LazyFrame[DictFrame]"], - ValidateBackendVersion, # pyright: ignore[reportInvalidTypeArguments] + CompliantLazyFrame[Any, "DictFrame", "LazyFrame[DictFrame]"], # pyright: ignore[reportInvalidTypeArguments] + ValidateBackendVersion, ): _implementation = Implementation.UNKNOWN From 3460f4e7725cfb51c3b5b977ffb24a8bb0c8462d Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:08:41 +0100 Subject: [PATCH 28/88] install test-plugin in CI --- .github/workflows/pytest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 76ffb0c68a..198aab3169 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -84,6 +84,8 @@ jobs: cache-dependency-glob: "pyproject.toml" - name: install-reqs run: uv pip install -e ".[dask, modin, ibis]" --group core-tests --group extra --system + - name: install-test-plugin + run: uv pip install -e tests/test-plugin --system - name: show-deps run: uv pip freeze - name: Run pytest From 1c246dc382a083a7fd9b105d97b8fc1288ca58ab Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:13:31 +0100 Subject: [PATCH 29/88] pass Version down --- narwhals/translate.py | 2 +- tests/test-plugin/test_plugin/__init__.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index 242bfdc7f8..55e6e073ff 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -568,7 +568,7 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 for plugin in discovered_plugins: obj = plugin.load() if obj.is_native_object(native_object): - compliant_object = obj.from_native(native_object) + compliant_object = obj.from_native(native_object, version) return _translate_if_compliant( compliant_object, pass_through=pass_through, diff --git a/tests/test-plugin/test_plugin/__init__.py b/tests/test-plugin/test_plugin/__init__.py index 5d47feab4b..b9eb847590 100644 --- a/tests/test-plugin/test_plugin/__init__.py +++ b/tests/test-plugin/test_plugin/__init__.py @@ -4,16 +4,15 @@ from typing_extensions import TypeIs -from narwhals.utils import Version - if TYPE_CHECKING: + from narwhals.utils import Version from test_plugin.dataframe import DictFrame, DictLazyFrame -def from_native(native_object: DictFrame) -> DictLazyFrame: +def from_native(native_object: DictFrame, version: Version) -> DictLazyFrame: from test_plugin.dataframe import DictLazyFrame - return DictLazyFrame(native_object, version=Version.MAIN) + return DictLazyFrame(native_object, version=version) def is_native_object(obj: Any) -> TypeIs[DictFrame]: From ab8303d659edb2521036ba67a7c866e2b4cce0f6 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:22:03 +0100 Subject: [PATCH 30/88] fixup, remove more defaults --- narwhals/stable/v2/__init__.py | 1 + narwhals/translate.py | 32 ++++++++++++++++++-------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/narwhals/stable/v2/__init__.py b/narwhals/stable/v2/__init__.py index fe8f7a71b0..b2d5ef4c02 100644 --- a/narwhals/stable/v2/__init__.py +++ b/narwhals/stable/v2/__init__.py @@ -553,6 +553,7 @@ def from_native( # noqa: D417 eager_only=eager_only, series_only=series_only, allow_series=allow_series, + eager_or_interchange_only=False, version=Version.V2, ) diff --git a/narwhals/translate.py b/narwhals/translate.py index 55e6e073ff..5993ce50bb 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -329,9 +329,9 @@ def _translate_if_compliant( # noqa: C901,PLR0911 pass_through: bool = False, eager_only: bool = False, # Interchange-level was removed after v1 - eager_or_interchange_only: bool = False, - series_only: bool = False, - allow_series: bool | None = None, + eager_or_interchange_only: bool, + series_only: bool, + allow_series: bool | None, version: Version, ) -> Any: if is_compliant_dataframe(compliant_object): @@ -376,9 +376,9 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 pass_through: bool = False, eager_only: bool = False, # Interchange-level was removed after v1 - eager_or_interchange_only: bool = False, - series_only: bool = False, - allow_series: bool | None = None, + eager_or_interchange_only: bool, + series_only: bool, + allow_series: bool | None, version: Version, ) -> Any: from narwhals._interchange.dataframe import supports_dataframe_interchange @@ -401,14 +401,17 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 raise ValueError(msg) # Extensions - if translated := _translate_if_compliant( - native_object, - pass_through=pass_through, - eager_only=eager_only, - eager_or_interchange_only=eager_or_interchange_only, - series_only=series_only, - version=version, - ): + if ( + translated := _translate_if_compliant( + native_object, + pass_through=pass_through, + eager_only=eager_only, + eager_or_interchange_only=eager_or_interchange_only, + series_only=series_only, + allow_series=allow_series, + version=version, + ) + ) is not None: return translated # Polars @@ -575,6 +578,7 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 eager_only=eager_only, eager_or_interchange_only=eager_or_interchange_only, series_only=series_only, + allow_series=allow_series, version=version, ) From a8501f5b42f1111ef053e5fa730689efe8dd2e32 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:38:20 +0100 Subject: [PATCH 31/88] coverage --- narwhals/translate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index 5993ce50bb..8a55ceb373 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -570,7 +570,7 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 for plugin in discovered_plugins: obj = plugin.load() - if obj.is_native_object(native_object): + if obj.is_native_object(native_object): # pragma: no cover (coverage bug?) compliant_object = obj.from_native(native_object, version) return _translate_if_compliant( compliant_object, @@ -581,6 +581,8 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 allow_series=allow_series, version=version, ) + else: # pragma: no cover + pass if not pass_through: msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(native_object)}" From 19dc900148e2b216eb8277144ac6f7a8bace53c9 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:42:03 +0100 Subject: [PATCH 32/88] actually stage test file --- narwhals/translate.py | 2 +- tests/plugins_test.py | 15 +++++++++++++++ tests/test-plugin/__init__.py | 0 tests/test-plugin/test_plugin/__init__.py | 3 ++- 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 tests/plugins_test.py create mode 100644 tests/test-plugin/__init__.py diff --git a/narwhals/translate.py b/narwhals/translate.py index 8a55ceb373..b2142de39b 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -570,7 +570,7 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 for plugin in discovered_plugins: obj = plugin.load() - if obj.is_native_object(native_object): # pragma: no cover (coverage bug?) + if obj.is_native_object(native_object): compliant_object = obj.from_native(native_object, version) return _translate_if_compliant( compliant_object, diff --git a/tests/plugins_test.py b/tests/plugins_test.py new file mode 100644 index 0000000000..49395f557d --- /dev/null +++ b/tests/plugins_test.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import sys + +import pytest + +import narwhals as nw + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason="3.10+ required for entrypoints") +def test_plugin() -> None: + pytest.importorskip("test_plugin") + df_native = {"a": [1, 1, 2], "b": [4, 5, 6]} + lf = nw.from_native(df_native) # pyright: ignore[reportCallIssue, reportArgumentType] + assert lf.columns == ["a", "b"] diff --git a/tests/test-plugin/__init__.py b/tests/test-plugin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test-plugin/test_plugin/__init__.py b/tests/test-plugin/test_plugin/__init__.py index b9eb847590..84fbf365df 100644 --- a/tests/test-plugin/test_plugin/__init__.py +++ b/tests/test-plugin/test_plugin/__init__.py @@ -5,9 +5,10 @@ from typing_extensions import TypeIs if TYPE_CHECKING: - from narwhals.utils import Version from test_plugin.dataframe import DictFrame, DictLazyFrame + from narwhals.utils import Version + def from_native(native_object: DictFrame, version: Version) -> DictLazyFrame: from test_plugin.dataframe import DictLazyFrame From 42f2df827244e3b14e1706b99c93b1c43f279306 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:43:26 +0100 Subject: [PATCH 33/88] remove daft traces --- tests/test-plugin/test_plugin/dataframe.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test-plugin/test_plugin/dataframe.py b/tests/test-plugin/test_plugin/dataframe.py index 8b32523bef..9d21b85cfa 100644 --- a/tests/test-plugin/test_plugin/dataframe.py +++ b/tests/test-plugin/test_plugin/dataframe.py @@ -2,10 +2,6 @@ from typing import TYPE_CHECKING, Any, TypeAlias -import daft -import daft.exceptions -import daft.functions - from narwhals._utils import ( Implementation, ValidateBackendVersion, @@ -33,10 +29,10 @@ def __init__(self, native_dataframe: DictFrame, *, version: Version) -> None: self._version = version @staticmethod - def _is_native(obj: daft.DataFrame | Any) -> TypeIs[daft.DataFrame]: - return isinstance(obj, daft.DataFrame) + def _is_native(obj: DictFrame | Any) -> TypeIs[DictFrame]: + return isinstance(obj, dict) - def to_narwhals(self) -> LazyFrame[daft.DataFrame]: + def to_narwhals(self) -> LazyFrame[DictFrame]: # pyright: ignore[reportInvalidTypeArguments] return self._version.lazyframe(self, level="lazy") def __narwhals_lazyframe__(self) -> Self: From d6a384a5e0497a72d3061dcefc00ea62cbc1c8be Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:46:55 +0100 Subject: [PATCH 34/88] rename --- .github/workflows/pytest.yml | 2 +- .pre-commit-config.yaml | 2 +- tests/{test-plugin => test_plugin}/__init__.py | 0 tests/{test-plugin => test_plugin}/pyproject.toml | 0 tests/{test-plugin => test_plugin}/test_plugin/__init__.py | 0 tests/{test-plugin => test_plugin}/test_plugin/dataframe.py | 0 6 files changed, 2 insertions(+), 2 deletions(-) rename tests/{test-plugin => test_plugin}/__init__.py (100%) rename tests/{test-plugin => test_plugin}/pyproject.toml (100%) rename tests/{test-plugin => test_plugin}/test_plugin/__init__.py (100%) rename tests/{test-plugin => test_plugin}/test_plugin/dataframe.py (100%) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 198aab3169..35fd96b0c0 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -85,7 +85,7 @@ jobs: - name: install-reqs run: uv pip install -e ".[dask, modin, ibis]" --group core-tests --group extra --system - name: install-test-plugin - run: uv pip install -e tests/test-plugin --system + run: uv pip install -e tests/test_plugin --system - name: show-deps run: uv pip freeze - name: Run pytest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10c26b09df..cb63242c0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -97,7 +97,7 @@ repos: exclude: | (?x) ^(tests/utils\.py) - |^(tests/test-plugin/) + |^(tests/test_plugin/) - id: no-commit-to-branch - id: end-of-file-fixer exclude: .svg$ diff --git a/tests/test-plugin/__init__.py b/tests/test_plugin/__init__.py similarity index 100% rename from tests/test-plugin/__init__.py rename to tests/test_plugin/__init__.py diff --git a/tests/test-plugin/pyproject.toml b/tests/test_plugin/pyproject.toml similarity index 100% rename from tests/test-plugin/pyproject.toml rename to tests/test_plugin/pyproject.toml diff --git a/tests/test-plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py similarity index 100% rename from tests/test-plugin/test_plugin/__init__.py rename to tests/test_plugin/test_plugin/__init__.py diff --git a/tests/test-plugin/test_plugin/dataframe.py b/tests/test_plugin/test_plugin/dataframe.py similarity index 100% rename from tests/test-plugin/test_plugin/dataframe.py rename to tests/test_plugin/test_plugin/dataframe.py From 9cf290df70479e38d3851636c8237de04dc02b15 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:54:17 +0100 Subject: [PATCH 35/88] install test_plugin in makefile --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 44c744614f..c96790bb1f 100644 --- a/Makefile +++ b/Makefile @@ -21,5 +21,6 @@ help: ## Display this help screen .PHONY: typing typing: ## Run typing checks $(VENV_BIN)/uv pip install -e . --group typing + $(VENV_BIN)/uv pip install -e tests/test_plugin $(VENV_BIN)/pyright $(VENV_BIN)/mypy From bdd71e79374ec470987ef788f626bde534075789 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:02:27 +0100 Subject: [PATCH 36/88] coverage --- Makefile | 1 - tests/plugins_test.py | 2 +- tests/test_plugin/test_plugin/__init__.py | 5 ++++- tests/test_plugin/test_plugin/dataframe.py | 15 ++++----------- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index c96790bb1f..44c744614f 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,5 @@ help: ## Display this help screen .PHONY: typing typing: ## Run typing checks $(VENV_BIN)/uv pip install -e . --group typing - $(VENV_BIN)/uv pip install -e tests/test_plugin $(VENV_BIN)/pyright $(VENV_BIN)/mypy diff --git a/tests/plugins_test.py b/tests/plugins_test.py index 49395f557d..78919ac28a 100644 --- a/tests/plugins_test.py +++ b/tests/plugins_test.py @@ -11,5 +11,5 @@ def test_plugin() -> None: pytest.importorskip("test_plugin") df_native = {"a": [1, 1, 2], "b": [4, 5, 6]} - lf = nw.from_native(df_native) # pyright: ignore[reportCallIssue, reportArgumentType] + lf = nw.from_native(df_native) # type: ignore[call-overload] assert lf.columns == ["a", "b"] diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index 84fbf365df..5c2cbda436 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -5,7 +5,10 @@ from typing_extensions import TypeIs if TYPE_CHECKING: - from test_plugin.dataframe import DictFrame, DictLazyFrame + from test_plugin.dataframe import ( # type: ignore[import-not-found] + DictFrame, + DictLazyFrame, + ) from narwhals.utils import Version diff --git a/tests/test_plugin/test_plugin/dataframe.py b/tests/test_plugin/test_plugin/dataframe.py index 9d21b85cfa..d7f3321222 100644 --- a/tests/test_plugin/test_plugin/dataframe.py +++ b/tests/test_plugin/test_plugin/dataframe.py @@ -13,13 +13,11 @@ DictFrame: TypeAlias = dict[str, list[Any]] if TYPE_CHECKING: - from typing_extensions import Self, TypeIs - - from narwhals.dataframe import LazyFrame + from typing_extensions import Self class DictLazyFrame( - CompliantLazyFrame[Any, "DictFrame", "LazyFrame[DictFrame]"], # pyright: ignore[reportInvalidTypeArguments] + CompliantLazyFrame[Any, "DictFrame", "LazyFrame[DictFrame]"], # type: ignore[type-var] ValidateBackendVersion, ): _implementation = Implementation.UNKNOWN @@ -28,13 +26,6 @@ def __init__(self, native_dataframe: DictFrame, *, version: Version) -> None: self._native_frame: DictFrame = native_dataframe self._version = version - @staticmethod - def _is_native(obj: DictFrame | Any) -> TypeIs[DictFrame]: - return isinstance(obj, dict) - - def to_narwhals(self) -> LazyFrame[DictFrame]: # pyright: ignore[reportInvalidTypeArguments] - return self._version.lazyframe(self, level="lazy") - def __narwhals_lazyframe__(self) -> Self: return self @@ -52,6 +43,7 @@ def columns(self) -> list[str]: schema = not_implemented() # type: ignore[assignment] # Helpers + _is_native = not_implemented() _iter_columns = not_implemented() # Functions @@ -72,6 +64,7 @@ def columns(self) -> list[str]: simple_select = not_implemented() sink_parquet = not_implemented() sort = not_implemented() + to_narwhals = not_implemented() unique = not_implemented() unpivot = not_implemented() with_columns = not_implemented() From bde288f5ecde84a965bfbe66eb47b9eec246e778 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:41:17 +0100 Subject: [PATCH 37/88] fix typing (for real this time) --- tests/plugins_test.py | 1 + tests/test_plugin/test_plugin/__init__.py | 2 +- tests/test_plugin/test_plugin/dataframe.py | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/plugins_test.py b/tests/plugins_test.py index 78919ac28a..f5a5ab44dd 100644 --- a/tests/plugins_test.py +++ b/tests/plugins_test.py @@ -12,4 +12,5 @@ def test_plugin() -> None: pytest.importorskip("test_plugin") df_native = {"a": [1, 1, 2], "b": [4, 5, 6]} lf = nw.from_native(df_native) # type: ignore[call-overload] + assert isinstance(lf, nw.LazyFrame) assert lf.columns == ["a", "b"] diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index 5c2cbda436..120cc7ee3a 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -5,7 +5,7 @@ from typing_extensions import TypeIs if TYPE_CHECKING: - from test_plugin.dataframe import ( # type: ignore[import-not-found] + from test_plugin.dataframe import ( # type: ignore[import-untyped, import-not-found, unused-ignore] DictFrame, DictLazyFrame, ) diff --git a/tests/test_plugin/test_plugin/dataframe.py b/tests/test_plugin/test_plugin/dataframe.py index d7f3321222..8f7dc04376 100644 --- a/tests/test_plugin/test_plugin/dataframe.py +++ b/tests/test_plugin/test_plugin/dataframe.py @@ -42,8 +42,10 @@ def columns(self) -> list[str]: # Properties schema = not_implemented() # type: ignore[assignment] + # Static + _is_native = not_implemented() # type: ignore[assignment] + # Helpers - _is_native = not_implemented() _iter_columns = not_implemented() # Functions From 7035533cb171fda096c48cde700415d2f4dfdfde Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:45:29 +0100 Subject: [PATCH 38/88] aah ruff check was removing the not-really-unused import --- tests/test_plugin/test_plugin/dataframe.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_plugin/test_plugin/dataframe.py b/tests/test_plugin/test_plugin/dataframe.py index 8f7dc04376..b66cf0133c 100644 --- a/tests/test_plugin/test_plugin/dataframe.py +++ b/tests/test_plugin/test_plugin/dataframe.py @@ -15,6 +15,8 @@ if TYPE_CHECKING: from typing_extensions import Self + from narwhals import LazyFrame # noqa: F401 + class DictLazyFrame( CompliantLazyFrame[Any, "DictFrame", "LazyFrame[DictFrame]"], # type: ignore[type-var] From 4868aaba67717dc169836068eae938b275c6632c Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 22 Aug 2025 20:07:31 +0100 Subject: [PATCH 39/88] lru_cache -> cache --- narwhals/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/narwhals/_utils.py b/narwhals/_utils.py index acd325ba6f..17e4ee13d3 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -2035,7 +2035,7 @@ def deep_getattr(obj: Any, name_1: str, *nested: str) -> Any: return deep_attrgetter(name_1, *nested)(obj) -@lru_cache(maxsize=8) +@cache def discover_plugins() -> EntryPoints: from importlib.metadata import entry_points From 1a73ab8d1a44da43a1d6c89e5598209714ab4f54 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 4 Sep 2025 17:35:14 +0000 Subject: [PATCH 40/88] feat(suggestion): `<3.10` support? --- narwhals/_utils.py | 7 +++++-- narwhals/translate.py | 32 +++++++++++++------------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/narwhals/_utils.py b/narwhals/_utils.py index f1804b058b..bcfa52d8fc 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -2065,9 +2065,12 @@ def deep_getattr(obj: Any, name_1: str, *nested: str) -> Any: @cache def discover_plugins() -> EntryPoints: - from importlib.metadata import entry_points + import sys + from importlib.metadata import entry_points as eps - return entry_points(group="narwhals.plugins") + group = "narwhals.plugins" + plugins = eps(group=group) if sys.version_info >= (3, 10) else eps()[group] + return cast("EntryPoints", plugins) class Compliant( diff --git a/narwhals/translate.py b/narwhals/translate.py index b2142de39b..d6a570e9d8 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -1,7 +1,6 @@ from __future__ import annotations import datetime as dt -import sys from decimal import Decimal from functools import wraps from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, overload @@ -565,24 +564,19 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 raise TypeError(msg) return Version.V1.dataframe(InterchangeFrame(native_object), level="interchange") - if sys.version_info >= (3, 10): - discovered_plugins = discover_plugins() - - for plugin in discovered_plugins: - obj = plugin.load() - if obj.is_native_object(native_object): - compliant_object = obj.from_native(native_object, version) - return _translate_if_compliant( - compliant_object, - pass_through=pass_through, - eager_only=eager_only, - eager_or_interchange_only=eager_or_interchange_only, - series_only=series_only, - allow_series=allow_series, - version=version, - ) - else: # pragma: no cover - pass + for plugin in discover_plugins(): + obj = plugin.load() + if obj.is_native_object(native_object): + compliant_object = obj.from_native(native_object, version) + return _translate_if_compliant( + compliant_object, + pass_through=pass_through, + eager_only=eager_only, + eager_or_interchange_only=eager_or_interchange_only, + series_only=series_only, + allow_series=allow_series, + version=version, + ) if not pass_through: msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(native_object)}" From 1481d48fc16ff1671edfdb6a52c16f65c3547cbf Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 4 Sep 2025 17:56:41 +0000 Subject: [PATCH 41/88] fix: don't expect plugins? https://github.com/python/cpython/blob/06fc882eac0e59220a7b8b127a1e7babe0055d45/Lib/importlib/metadata.py#L572-L585 --- narwhals/_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/narwhals/_utils.py b/narwhals/_utils.py index bcfa52d8fc..488894282c 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -2069,8 +2069,9 @@ def discover_plugins() -> EntryPoints: from importlib.metadata import entry_points as eps group = "narwhals.plugins" - plugins = eps(group=group) if sys.version_info >= (3, 10) else eps()[group] - return cast("EntryPoints", plugins) + if sys.version_info < (3, 10): + return cast("EntryPoints", eps().get(group, ())) + return eps(group=group) class Compliant( From 59589c1d88415a632a162f2a14acaa6393b0817a Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 4 Sep 2025 18:02:28 +0000 Subject: [PATCH 42/88] chore(typing): Ignore unimplemented https://github.com/narwhals-dev/narwhals/actions/runs/17472513945/job/49624229108?pr=2978 --- tests/test_plugin/test_plugin/dataframe.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_plugin/test_plugin/dataframe.py b/tests/test_plugin/test_plugin/dataframe.py index b66cf0133c..37feed400c 100644 --- a/tests/test_plugin/test_plugin/dataframe.py +++ b/tests/test_plugin/test_plugin/dataframe.py @@ -40,6 +40,7 @@ def columns(self) -> list[str]: # Dunders __narwhals_namespace__ = not_implemented() + __native_namespace__ = not_implemented() # Properties schema = not_implemented() # type: ignore[assignment] @@ -68,6 +69,7 @@ def columns(self) -> list[str]: simple_select = not_implemented() sink_parquet = not_implemented() sort = not_implemented() + tail = not_implemented() to_narwhals = not_implemented() unique = not_implemented() unpivot = not_implemented() From 8f22fdbfae0c5df3ad71bc926818e91bfcb0195d Mon Sep 17 00:00:00 2001 From: ym-pett Date: Mon, 8 Sep 2025 16:00:05 +0100 Subject: [PATCH 43/88] wip: hybrid approach to importing --- narwhals/translate.py | 3 ++- tests/test_plugin/test_plugin/__init__.py | 14 ++++++++++---- tests/test_plugin/test_plugin/namespace.py | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 tests/test_plugin/test_plugin/namespace.py diff --git a/narwhals/translate.py b/narwhals/translate.py index d6a570e9d8..8e330c0f4e 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -567,7 +567,8 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 for plugin in discover_plugins(): obj = plugin.load() if obj.is_native_object(native_object): - compliant_object = obj.from_native(native_object, version) + compliant_namespace = obj.__narwhals_namespace__(version=version) + compliant_object = compliant_namespace.from_native(native_object) return _translate_if_compliant( compliant_object, pass_through=pass_through, diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index 120cc7ee3a..3f32ab8b9e 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -9,14 +9,20 @@ DictFrame, DictLazyFrame, ) + from test_plugin.namespace import DictNamespace - from narwhals.utils import Version + from narwhals.utils import Version -def from_native(native_object: DictFrame, version: Version) -> DictLazyFrame: - from test_plugin.dataframe import DictLazyFrame +def __narwhals_namespace__(version:Version): + from test_plugin.namespace import DictNamespace + return DictNamespace(version=version) + +# here need to implement __narwhals_namespace__ instead! +# def from_native(native_object: DictFrame, version: Version) -> DictLazyFrame: +# from test_plugin.dataframe import DictLazyFrame - return DictLazyFrame(native_object, version=version) +# return DictLazyFrame(native_object, version=version) def is_native_object(obj: Any) -> TypeIs[DictFrame]: diff --git a/tests/test_plugin/test_plugin/namespace.py b/tests/test_plugin/test_plugin/namespace.py new file mode 100644 index 0000000000..9ce5411769 --- /dev/null +++ b/tests/test_plugin/test_plugin/namespace.py @@ -0,0 +1,15 @@ +from typing import TYPE_CHECKING, Any +from narwhals._compliant import CompliantNamespace +from test_plugin.dataframe import DictLazyFrame +from test_plugin.dataframe import DictFrame + +if TYPE_CHECKING: + from narwhals.utils import Version + +class DictNamespace(CompliantNamespace[DictLazyFrame, Any]): + + def __init__(self, *, version: Version) -> None: + self._version = version + + def from_native(self, native_object: DictFrame) -> DictLazyFrame: + return DictLazyFrame(native_object, version=self._version) \ No newline at end of file From b3f2b6060b1630c7c7e040cac75db662b4e21856 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Mon, 8 Sep 2025 16:07:28 +0100 Subject: [PATCH 44/88] wip: checkpoint --- tests/test_plugin/test_plugin/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index 3f32ab8b9e..e8a218997d 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -18,12 +18,5 @@ def __narwhals_namespace__(version:Version): from test_plugin.namespace import DictNamespace return DictNamespace(version=version) -# here need to implement __narwhals_namespace__ instead! -# def from_native(native_object: DictFrame, version: Version) -> DictLazyFrame: -# from test_plugin.dataframe import DictLazyFrame - -# return DictLazyFrame(native_object, version=version) - - def is_native_object(obj: Any) -> TypeIs[DictFrame]: return isinstance(obj, dict) From 422ec7085befee6209e8801d910d1faaa05b4244 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 15:11:31 +0000 Subject: [PATCH 45/88] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_plugin/test_plugin/__init__.py | 8 +++++--- tests/test_plugin/test_plugin/namespace.py | 11 +++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index e8a218997d..ff5a96617f 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -11,12 +11,14 @@ ) from test_plugin.namespace import DictNamespace - from narwhals.utils import Version -def __narwhals_namespace__(version:Version): + +def __narwhals_namespace__(version: Version): from test_plugin.namespace import DictNamespace + return DictNamespace(version=version) - + + def is_native_object(obj: Any) -> TypeIs[DictFrame]: return isinstance(obj, dict) diff --git a/tests/test_plugin/test_plugin/namespace.py b/tests/test_plugin/test_plugin/namespace.py index 9ce5411769..bdc6d9cd4f 100644 --- a/tests/test_plugin/test_plugin/namespace.py +++ b/tests/test_plugin/test_plugin/namespace.py @@ -1,15 +1,18 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, Any + +from test_plugin.dataframe import DictFrame, DictLazyFrame + from narwhals._compliant import CompliantNamespace -from test_plugin.dataframe import DictLazyFrame -from test_plugin.dataframe import DictFrame if TYPE_CHECKING: from narwhals.utils import Version -class DictNamespace(CompliantNamespace[DictLazyFrame, Any]): +class DictNamespace(CompliantNamespace[DictLazyFrame, Any]): def __init__(self, *, version: Version) -> None: self._version = version def from_native(self, native_object: DictFrame) -> DictLazyFrame: - return DictLazyFrame(native_object, version=self._version) \ No newline at end of file + return DictLazyFrame(native_object, version=self._version) From b0b524b2526423b1c47f0d2034c564f9590e1857 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Mon, 8 Sep 2025 18:03:30 +0100 Subject: [PATCH 46/88] damn I broke the plugin tests --- tests/test_plugin/test_plugin/__init__.py | 4 ++-- tests/test_plugin/test_plugin/namespace.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index e8a218997d..f6448c6274 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -1,8 +1,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any - from typing_extensions import TypeIs +#from narwhals.utils import Version if TYPE_CHECKING: from test_plugin.dataframe import ( # type: ignore[import-untyped, import-not-found, unused-ignore] @@ -16,7 +16,7 @@ def __narwhals_namespace__(version:Version): from test_plugin.namespace import DictNamespace - return DictNamespace(version=version) + return DictNamespace(Version) def is_native_object(obj: Any) -> TypeIs[DictFrame]: return isinstance(obj, dict) diff --git a/tests/test_plugin/test_plugin/namespace.py b/tests/test_plugin/test_plugin/namespace.py index 9ce5411769..e6e97e7d81 100644 --- a/tests/test_plugin/test_plugin/namespace.py +++ b/tests/test_plugin/test_plugin/namespace.py @@ -2,6 +2,7 @@ from narwhals._compliant import CompliantNamespace from test_plugin.dataframe import DictLazyFrame from test_plugin.dataframe import DictFrame +from narwhals._utils import not_implemented if TYPE_CHECKING: from narwhals.utils import Version @@ -12,4 +13,7 @@ def __init__(self, *, version: Version) -> None: self._version = version def from_native(self, native_object: DictFrame) -> DictLazyFrame: - return DictLazyFrame(native_object, version=self._version) \ No newline at end of file + return DictLazyFrame(native_object, version=self._version) + + # implementation = not_implemented() + # expr = not_implemented() \ No newline at end of file From 0960d6923f30139d418c122cfc9417b5edb3b965 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Mon, 8 Sep 2025 18:11:23 +0100 Subject: [PATCH 47/88] wip: fixed local mess in test_plugin --- tests/test_plugin/test_plugin/__init__.py | 5 +---- tests/test_plugin/test_plugin/namespace.py | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index ff5a96617f..87f7eb241e 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any - from typing_extensions import TypeIs if TYPE_CHECKING: @@ -16,9 +15,7 @@ def __narwhals_namespace__(version: Version): from test_plugin.namespace import DictNamespace - return DictNamespace(version=version) - - + def is_native_object(obj: Any) -> TypeIs[DictFrame]: return isinstance(obj, dict) diff --git a/tests/test_plugin/test_plugin/namespace.py b/tests/test_plugin/test_plugin/namespace.py index bdc6d9cd4f..e16639348d 100644 --- a/tests/test_plugin/test_plugin/namespace.py +++ b/tests/test_plugin/test_plugin/namespace.py @@ -5,11 +5,13 @@ from test_plugin.dataframe import DictFrame, DictLazyFrame from narwhals._compliant import CompliantNamespace +from test_plugin.dataframe import DictLazyFrame +from test_plugin.dataframe import DictFrame +from narwhals._utils import not_implemented if TYPE_CHECKING: from narwhals.utils import Version - class DictNamespace(CompliantNamespace[DictLazyFrame, Any]): def __init__(self, *, version: Version) -> None: self._version = version From d9512205f32990e49c9b31e6749b7fcaa56b4ef3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:16:22 +0000 Subject: [PATCH 48/88] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_plugin/test_plugin/__init__.py | 8 ++++++-- tests/test_plugin/test_plugin/namespace.py | 4 +--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index 3ee75364bb..1ce71d40dd 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -1,8 +1,10 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any + from typing_extensions import TypeIs -#from narwhals.utils import Version + +# from narwhals.utils import Version if TYPE_CHECKING: from test_plugin.dataframe import ( # type: ignore[import-untyped, import-not-found, unused-ignore] @@ -16,7 +18,9 @@ def __narwhals_namespace__(version: Version): from test_plugin.namespace import DictNamespace + return DictNamespace(version=version) - + + def is_native_object(obj: Any) -> TypeIs[DictFrame]: return isinstance(obj, dict) diff --git a/tests/test_plugin/test_plugin/namespace.py b/tests/test_plugin/test_plugin/namespace.py index e16639348d..bdc6d9cd4f 100644 --- a/tests/test_plugin/test_plugin/namespace.py +++ b/tests/test_plugin/test_plugin/namespace.py @@ -5,13 +5,11 @@ from test_plugin.dataframe import DictFrame, DictLazyFrame from narwhals._compliant import CompliantNamespace -from test_plugin.dataframe import DictLazyFrame -from test_plugin.dataframe import DictFrame -from narwhals._utils import not_implemented if TYPE_CHECKING: from narwhals.utils import Version + class DictNamespace(CompliantNamespace[DictLazyFrame, Any]): def __init__(self, *, version: Version) -> None: self._version = version From b563ace832ccfe168e61303371505035ae925866 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Tue, 9 Sep 2025 13:08:09 +0100 Subject: [PATCH 49/88] added not_implemented functions to namespace --- tests/test_plugin/test_plugin/__init__.py | 8 +++----- tests/test_plugin/test_plugin/namespace.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index 1ce71d40dd..ccbe48c9b1 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -4,19 +4,17 @@ from typing_extensions import TypeIs -# from narwhals.utils import Version - if TYPE_CHECKING: from test_plugin.dataframe import ( # type: ignore[import-untyped, import-not-found, unused-ignore] DictFrame, - DictLazyFrame, ) - from test_plugin.namespace import DictNamespace from narwhals.utils import Version + from tests.test_plugin.test_plugin.namespace import DictNamespace -def __narwhals_namespace__(version: Version): +# @mp: is the return type here correct? +def __narwhals_namespace__(version: Version) -> DictNamespace: # noqa: N807 from test_plugin.namespace import DictNamespace return DictNamespace(version=version) diff --git a/tests/test_plugin/test_plugin/namespace.py b/tests/test_plugin/test_plugin/namespace.py index bdc6d9cd4f..4c236c47e9 100644 --- a/tests/test_plugin/test_plugin/namespace.py +++ b/tests/test_plugin/test_plugin/namespace.py @@ -5,6 +5,7 @@ from test_plugin.dataframe import DictFrame, DictLazyFrame from narwhals._compliant import CompliantNamespace +from narwhals._utils import not_implemented if TYPE_CHECKING: from narwhals.utils import Version @@ -16,3 +17,19 @@ def __init__(self, *, version: Version) -> None: def from_native(self, native_object: DictFrame) -> DictLazyFrame: return DictLazyFrame(native_object, version=self._version) + + _expr: Any = not_implemented() + _implementation: Any = not_implemented() + len: Any = not_implemented() + lit: Any = not_implemented() + all_horizontal: Any = not_implemented() + any_horizontal: Any = not_implemented() + sum_horizontal: Any = not_implemented() + mean_horizontal: Any = not_implemented() + min_horizontal: Any = not_implemented() + max_horizontal: Any = not_implemented() + concat: Any = not_implemented() + when: Any = not_implemented() + concat_str: Any = not_implemented() + selectors: Any = not_implemented() + coalesce: Any = not_implemented() From 9847793cadc48225e8140b7f32f225674beaf161 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Tue, 9 Sep 2025 13:16:20 +0100 Subject: [PATCH 50/88] removed comment --- tests/test_plugin/test_plugin/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index ccbe48c9b1..b0040115dc 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -13,7 +13,6 @@ from tests.test_plugin.test_plugin.namespace import DictNamespace -# @mp: is the return type here correct? def __narwhals_namespace__(version: Version) -> DictNamespace: # noqa: N807 from test_plugin.namespace import DictNamespace From 1f38e31437ebb32cd8ff846ba6bae7fab0746b17 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Tue, 9 Sep 2025 15:15:51 +0100 Subject: [PATCH 51/88] changed naming for plugin entrypoins --- narwhals/_utils.py | 2 +- narwhals/translate.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/narwhals/_utils.py b/narwhals/_utils.py index 44fcab12f9..c761cbb3d1 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -2064,7 +2064,7 @@ def deep_getattr(obj: Any, name_1: str, *nested: str) -> Any: @cache -def discover_plugins() -> EntryPoints: +def discover_entrypoints() -> EntryPoints: import sys from importlib.metadata import entry_points as eps diff --git a/narwhals/translate.py b/narwhals/translate.py index 8e330c0f4e..3e0f955f59 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -15,7 +15,7 @@ from narwhals._utils import ( Implementation, Version, - discover_plugins, + discover_entrypoints, has_native_namespace, is_compliant_dataframe, is_compliant_lazyframe, @@ -564,10 +564,10 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 raise TypeError(msg) return Version.V1.dataframe(InterchangeFrame(native_object), level="interchange") - for plugin in discover_plugins(): - obj = plugin.load() - if obj.is_native_object(native_object): - compliant_namespace = obj.__narwhals_namespace__(version=version) + for entry_point in discover_entrypoints(): + plugin = entry_point.load() + if plugin.is_native_object(native_object): + compliant_namespace = plugin.__narwhals_namespace__(version=version) compliant_object = compliant_namespace.from_native(native_object) return _translate_if_compliant( compliant_object, From 5dee0bbb0375f4010a2a0ace360621f86714cd22 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Tue, 9 Sep 2025 12:14:10 +0100 Subject: [PATCH 52/88] added not_implemented functions to dictnamespace --- tests/test_plugin/test_plugin/__init__.py | 4 ++-- tests/test_plugin/test_plugin/namespace.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index b0040115dc..9d262ce2b2 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -14,9 +14,9 @@ def __narwhals_namespace__(version: Version) -> DictNamespace: # noqa: N807 - from test_plugin.namespace import DictNamespace + from test_plugin.namespace import DictNamespace # type: ignore - return DictNamespace(version=version) + return DictNamespace(version=version) # type: ignore def is_native_object(obj: Any) -> TypeIs[DictFrame]: diff --git a/tests/test_plugin/test_plugin/namespace.py b/tests/test_plugin/test_plugin/namespace.py index 4c236c47e9..10dac00ab2 100644 --- a/tests/test_plugin/test_plugin/namespace.py +++ b/tests/test_plugin/test_plugin/namespace.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any -from test_plugin.dataframe import DictFrame, DictLazyFrame +from test_plugin.dataframe import DictFrame, DictLazyFrame # type: ignore from narwhals._compliant import CompliantNamespace from narwhals._utils import not_implemented From c508ab0c3a26bf2c7277e65214410fb5b4c900cf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:34:28 +0000 Subject: [PATCH 53/88] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_plugin/test_plugin/__init__.py | 4 ++-- tests/test_plugin/test_plugin/namespace.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index 9d262ce2b2..1f6250e43a 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -14,9 +14,9 @@ def __narwhals_namespace__(version: Version) -> DictNamespace: # noqa: N807 - from test_plugin.namespace import DictNamespace # type: ignore + from test_plugin.namespace import DictNamespace # type: ignore - return DictNamespace(version=version) # type: ignore + return DictNamespace(version=version) # type: ignore def is_native_object(obj: Any) -> TypeIs[DictFrame]: diff --git a/tests/test_plugin/test_plugin/namespace.py b/tests/test_plugin/test_plugin/namespace.py index 10dac00ab2..2f72dcbc54 100644 --- a/tests/test_plugin/test_plugin/namespace.py +++ b/tests/test_plugin/test_plugin/namespace.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any -from test_plugin.dataframe import DictFrame, DictLazyFrame # type: ignore +from test_plugin.dataframe import DictFrame, DictLazyFrame # type: ignore from narwhals._compliant import CompliantNamespace from narwhals._utils import not_implemented From 6c65fd4035c05e8ad3467e3460da37fb3478d92e Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:54:50 +0000 Subject: [PATCH 54/88] fix(typing): Add `py.typed` marker https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-library-stubs-or-py-typed-marker --- tests/test_plugin/test_plugin/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/test_plugin/test_plugin/py.typed diff --git a/tests/test_plugin/test_plugin/py.typed b/tests/test_plugin/test_plugin/py.typed new file mode 100644 index 0000000000..e69de29bb2 From b26d7d46325ddb9628482c9fd9d181c532ed90c1 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:55:28 +0000 Subject: [PATCH 55/88] fix: Fully qualify imports (from `tests`) --- tests/test_plugin/test_plugin/__init__.py | 9 +++------ tests/test_plugin/test_plugin/namespace.py | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index 1f6250e43a..09f59a2315 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -5,18 +5,15 @@ from typing_extensions import TypeIs if TYPE_CHECKING: - from test_plugin.dataframe import ( # type: ignore[import-untyped, import-not-found, unused-ignore] - DictFrame, - ) - from narwhals.utils import Version + from tests.test_plugin.test_plugin.dataframe import DictFrame from tests.test_plugin.test_plugin.namespace import DictNamespace def __narwhals_namespace__(version: Version) -> DictNamespace: # noqa: N807 - from test_plugin.namespace import DictNamespace # type: ignore + from tests.test_plugin.test_plugin.namespace import DictNamespace - return DictNamespace(version=version) # type: ignore + return DictNamespace(version=version) def is_native_object(obj: Any) -> TypeIs[DictFrame]: diff --git a/tests/test_plugin/test_plugin/namespace.py b/tests/test_plugin/test_plugin/namespace.py index 2f72dcbc54..0cba9e5964 100644 --- a/tests/test_plugin/test_plugin/namespace.py +++ b/tests/test_plugin/test_plugin/namespace.py @@ -2,10 +2,9 @@ from typing import TYPE_CHECKING, Any -from test_plugin.dataframe import DictFrame, DictLazyFrame # type: ignore - from narwhals._compliant import CompliantNamespace from narwhals._utils import not_implemented +from tests.test_plugin.test_plugin.dataframe import DictFrame, DictLazyFrame if TYPE_CHECKING: from narwhals.utils import Version From 22362ad80a03f76b75d7895fa775eee4fab2dffb Mon Sep 17 00:00:00 2001 From: ym-pett Date: Thu, 11 Sep 2025 11:26:07 +0100 Subject: [PATCH 56/88] added protocol & plugin detection function --- narwhals/_utils.py | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/narwhals/_utils.py b/narwhals/_utils.py index c761cbb3d1..9f446d6493 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -2,6 +2,7 @@ import os import re +import sys from collections.abc import Collection, Container, Iterable, Iterator, Mapping, Sequence from datetime import timezone from enum import Enum, auto @@ -50,7 +51,6 @@ if TYPE_CHECKING: from collections.abc import Set # noqa: PYI025 - from importlib.metadata import EntryPoints from types import ModuleType import pandas as pd @@ -73,7 +73,12 @@ CompliantSeriesT, NativeSeriesT_co, ) - from narwhals._compliant.typing import EvalNames, NativeDataFrameT, NativeLazyFrameT + from narwhals._compliant.typing import ( + CompliantNamespaceAny, + EvalNames, + NativeDataFrameT, + NativeLazyFrameT, + ) from narwhals._namespace import ( Namespace, _NativeArrow, @@ -2063,16 +2068,35 @@ def deep_getattr(obj: Any, name_1: str, *nested: str) -> Any: return deep_attrgetter(name_1, *nested)(obj) +# @cache +# def discover_entrypoints() -> EntryPoints: +# import sys +# from importlib.metadata import entry_points as eps + +# group = "narwhals.plugins" +# if sys.version_info < (3, 10): +# return cast("EntryPoints", eps().get(group, ())) +# return eps(group=group) + + +# @mp: should the protocol be defined in namespace? +class Plugin2(Protocol): + NATIVE_PACKAGE: LiteralString + + def __narwhals_namespace__(self, version: Version) -> CompliantNamespaceAny: ... + + @cache -def discover_entrypoints() -> EntryPoints: - import sys - from importlib.metadata import entry_points as eps +def _might_be(cls: type, type_: str) -> bool: + try: + return any(f"{type_}." in str(o) for o in cls.mro()) + except TypeError: + return False - group = "narwhals.plugins" - if sys.version_info < (3, 10): - return cast("EntryPoints", eps().get(group, ())) - return eps(group=group) +def _is_native_plugin(native_object: Any, plugin: Plugin2) -> bool: + pkg = plugin.NATIVE_PACKAGE + return sys.modules.get(pkg) is not None and _might_be(type(native_object), pkg) class Compliant( _StoresNative[NativeT_co], _StoresImplementation, Protocol[NativeT_co] From d22f046fbcbf4d2d0abc5506339cb264c87dbec9 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Thu, 11 Sep 2025 14:29:11 +0100 Subject: [PATCH 57/88] changed plugin contract --- narwhals/_utils.py | 20 +++++++++++--------- narwhals/translate.py | 3 ++- tests/test_plugin/test_plugin/__init__.py | 9 +++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/narwhals/_utils.py b/narwhals/_utils.py index 9f446d6493..681ba7296e 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -51,6 +51,7 @@ if TYPE_CHECKING: from collections.abc import Set # noqa: PYI025 + from importlib.metadata import EntryPoints from types import ModuleType import pandas as pd @@ -2068,15 +2069,15 @@ def deep_getattr(obj: Any, name_1: str, *nested: str) -> Any: return deep_attrgetter(name_1, *nested)(obj) -# @cache -# def discover_entrypoints() -> EntryPoints: -# import sys -# from importlib.metadata import entry_points as eps +@cache +def discover_entrypoints() -> EntryPoints: + import sys + from importlib.metadata import entry_points as eps -# group = "narwhals.plugins" -# if sys.version_info < (3, 10): -# return cast("EntryPoints", eps().get(group, ())) -# return eps(group=group) + group = "narwhals.plugins" + if sys.version_info < (3, 10): + return cast("EntryPoints", eps().get(group, ())) + return eps(group=group) # @mp: should the protocol be defined in namespace? @@ -2089,7 +2090,7 @@ def __narwhals_namespace__(self, version: Version) -> CompliantNamespaceAny: ... @cache def _might_be(cls: type, type_: str) -> bool: try: - return any(f"{type_}." in str(o) for o in cls.mro()) + return any(type_ in o.__module__.split(".") for o in cls.mro()) except TypeError: return False @@ -2098,6 +2099,7 @@ def _is_native_plugin(native_object: Any, plugin: Plugin2) -> bool: pkg = plugin.NATIVE_PACKAGE return sys.modules.get(pkg) is not None and _might_be(type(native_object), pkg) + class Compliant( _StoresNative[NativeT_co], _StoresImplementation, Protocol[NativeT_co] ): ... diff --git a/narwhals/translate.py b/narwhals/translate.py index 3e0f955f59..3c2324f58b 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -15,6 +15,7 @@ from narwhals._utils import ( Implementation, Version, + _is_native_plugin, discover_entrypoints, has_native_namespace, is_compliant_dataframe, @@ -566,7 +567,7 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 for entry_point in discover_entrypoints(): plugin = entry_point.load() - if plugin.is_native_object(native_object): + if _is_native_plugin(native_object, plugin): compliant_namespace = plugin.__narwhals_namespace__(version=version) compliant_object = compliant_namespace.from_native(native_object) return _translate_if_compliant( diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index 09f59a2315..5b5169b057 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -1,12 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any - -from typing_extensions import TypeIs +from typing import TYPE_CHECKING if TYPE_CHECKING: from narwhals.utils import Version - from tests.test_plugin.test_plugin.dataframe import DictFrame + from tests.test_plugin.test_plugin.dataframe import DictFrame as DictFrame from tests.test_plugin.test_plugin.namespace import DictNamespace @@ -16,5 +14,4 @@ def __narwhals_namespace__(version: Version) -> DictNamespace: # noqa: N807 return DictNamespace(version=version) -def is_native_object(obj: Any) -> TypeIs[DictFrame]: - return isinstance(obj, dict) +NATIVE_PACKAGE = "builtins" From 5c16c7f96bc2459b5e1d680ccf9b1a50a143c55f Mon Sep 17 00:00:00 2001 From: ym-pett Date: Fri, 12 Sep 2025 13:57:52 +0100 Subject: [PATCH 58/88] added is_native back in, adapted plugin tests --- narwhals/_utils.py | 7 ++++++- tests/test_plugin/test_plugin/__init__.py | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/narwhals/_utils.py b/narwhals/_utils.py index f7f799ca5f..18ea16c591 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -2086,6 +2086,7 @@ class Plugin2(Protocol): NATIVE_PACKAGE: LiteralString def __narwhals_namespace__(self, version: Version) -> CompliantNamespaceAny: ... + def is_native(self, native_object: object, /) -> bool: ... @cache @@ -2098,7 +2099,11 @@ def _might_be(cls: type, type_: str) -> bool: def _is_native_plugin(native_object: Any, plugin: Plugin2) -> bool: pkg = plugin.NATIVE_PACKAGE - return sys.modules.get(pkg) is not None and _might_be(type(native_object), pkg) + return ( + sys.modules.get(pkg) is not None + and _might_be(type(native_object), pkg) + and plugin.is_native(native_object) + ) class Compliant( diff --git a/tests/test_plugin/test_plugin/__init__.py b/tests/test_plugin/test_plugin/__init__.py index 5b5169b057..3845cb23f9 100644 --- a/tests/test_plugin/test_plugin/__init__.py +++ b/tests/test_plugin/test_plugin/__init__.py @@ -3,8 +3,10 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: + from typing_extensions import TypeIs + from narwhals.utils import Version - from tests.test_plugin.test_plugin.dataframe import DictFrame as DictFrame + from tests.test_plugin.test_plugin.dataframe import DictFrame from tests.test_plugin.test_plugin.namespace import DictNamespace @@ -14,4 +16,8 @@ def __narwhals_namespace__(version: Version) -> DictNamespace: # noqa: N807 return DictNamespace(version=version) +def is_native(native_object: object) -> TypeIs[DictFrame]: + return isinstance(native_object, dict) + + NATIVE_PACKAGE = "builtins" From 0c69762d8cd89675a9ccfdfef393bc5aa3a0e86c Mon Sep 17 00:00:00 2001 From: ym-pett Date: Mon, 15 Sep 2025 16:31:54 +0100 Subject: [PATCH 59/88] wip: moving plugins --- narwhals/_utils.py | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/narwhals/_utils.py b/narwhals/_utils.py index 18ea16c591..509239da55 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -51,7 +51,6 @@ if TYPE_CHECKING: from collections.abc import Set # noqa: PYI025 - from importlib.metadata import EntryPoints from types import ModuleType import pandas as pd @@ -75,7 +74,6 @@ NativeSeriesT_co, ) from narwhals._compliant.typing import ( - CompliantNamespaceAny, EvalNames, NativeDataFrameT, NativeLazyFrameT, @@ -2069,43 +2067,6 @@ def deep_getattr(obj: Any, name_1: str, *nested: str) -> Any: """Perform a nested attribute lookup on `obj`.""" return deep_attrgetter(name_1, *nested)(obj) - -@cache -def discover_entrypoints() -> EntryPoints: - import sys - from importlib.metadata import entry_points as eps - - group = "narwhals.plugins" - if sys.version_info < (3, 10): - return cast("EntryPoints", eps().get(group, ())) - return eps(group=group) - - -# @mp: should the protocol be defined in namespace? -class Plugin2(Protocol): - NATIVE_PACKAGE: LiteralString - - def __narwhals_namespace__(self, version: Version) -> CompliantNamespaceAny: ... - def is_native(self, native_object: object, /) -> bool: ... - - -@cache -def _might_be(cls: type, type_: str) -> bool: - try: - return any(type_ in o.__module__.split(".") for o in cls.mro()) - except TypeError: - return False - - -def _is_native_plugin(native_object: Any, plugin: Plugin2) -> bool: - pkg = plugin.NATIVE_PACKAGE - return ( - sys.modules.get(pkg) is not None - and _might_be(type(native_object), pkg) - and plugin.is_native(native_object) - ) - - class Compliant( _StoresNative[NativeT_co], _StoresImplementation, Protocol[NativeT_co] ): ... From f7765e8d147378d863cc31d95d3ca37c3e94e9d7 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Mon, 15 Sep 2025 16:41:18 +0100 Subject: [PATCH 60/88] wip: fixed imports in plugins utils --- narwhals/plugins/_utils.py | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 narwhals/plugins/_utils.py diff --git a/narwhals/plugins/_utils.py b/narwhals/plugins/_utils.py new file mode 100644 index 0000000000..f04b3c2c37 --- /dev/null +++ b/narwhals/plugins/_utils.py @@ -0,0 +1,40 @@ +import sys +from functools import cache +from typing import TYPE_CHECKING, Protocol, Any +from narwhals.utils import Version + +if TYPE_CHECKING: + from importlib.metadata import EntryPoints + from typing_extensions import LiteralString + from narwhals._compliant.typing import CompliantNamespaceAny + +@cache +def discover_entrypoints() -> EntryPoints: + from importlib.metadata import entry_points as eps + + group = "narwhals.plugins" + if sys.version_info < (3, 10): + return cast("EntryPoints", eps().get(group, ())) + return eps(group=group) + +class Plugin(Protocol): + NATIVE_PACKAGE: LiteralString + + def __narwhals_namespace__(self, version: Version) -> CompliantNamespaceAny: ... + def is_native(self, native_object: object, /) -> bool: ... + + +@cache +def _might_be(cls: type, type_: str) -> bool: + try: + return any(type_ in o.__module__.split(".") for o in cls.mro()) + except TypeError: + return False + +def _is_native_plugin(native_object: Any, plugin: Plugin) -> bool: + pkg = plugin.NATIVE_PACKAGE + return ( + sys.modules.get(pkg) is not None + and _might_be(type(native_object), pkg) + and plugin.is_native(native_object) + ) \ No newline at end of file From e3e5ec7a7dcd79caa824b7c0a1b749e8ea6cc53c Mon Sep 17 00:00:00 2001 From: ym-pett Date: Mon, 15 Sep 2025 16:55:00 +0100 Subject: [PATCH 61/88] refactored plugin-related utils into their own file --- narwhals/plugins/__init__.py | 0 narwhals/plugins/_utils.py | 13 ++++++++++--- narwhals/translate.py | 3 +-- 3 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 narwhals/plugins/__init__.py diff --git a/narwhals/plugins/__init__.py b/narwhals/plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/narwhals/plugins/_utils.py b/narwhals/plugins/_utils.py index f04b3c2c37..bf467d69b3 100644 --- a/narwhals/plugins/_utils.py +++ b/narwhals/plugins/_utils.py @@ -1,12 +1,17 @@ +from __future__ import annotations + import sys from functools import cache -from typing import TYPE_CHECKING, Protocol, Any -from narwhals.utils import Version +from typing import TYPE_CHECKING, Any, Protocol, cast if TYPE_CHECKING: from importlib.metadata import EntryPoints + from typing_extensions import LiteralString + from narwhals._compliant.typing import CompliantNamespaceAny + from narwhals.utils import Version + @cache def discover_entrypoints() -> EntryPoints: @@ -17,6 +22,7 @@ def discover_entrypoints() -> EntryPoints: return cast("EntryPoints", eps().get(group, ())) return eps(group=group) + class Plugin(Protocol): NATIVE_PACKAGE: LiteralString @@ -31,10 +37,11 @@ def _might_be(cls: type, type_: str) -> bool: except TypeError: return False + def _is_native_plugin(native_object: Any, plugin: Plugin) -> bool: pkg = plugin.NATIVE_PACKAGE return ( sys.modules.get(pkg) is not None and _might_be(type(native_object), pkg) and plugin.is_native(native_object) - ) \ No newline at end of file + ) diff --git a/narwhals/translate.py b/narwhals/translate.py index 3c2324f58b..3e5a3f5f66 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -15,8 +15,6 @@ from narwhals._utils import ( Implementation, Version, - _is_native_plugin, - discover_entrypoints, has_native_namespace, is_compliant_dataframe, is_compliant_lazyframe, @@ -37,6 +35,7 @@ is_pyarrow_scalar, is_pyarrow_table, ) +from narwhals.plugins._utils import _is_native_plugin, discover_entrypoints if TYPE_CHECKING: from narwhals.dataframe import DataFrame, LazyFrame From 0cd5f2f389188dcd46718d8aaff4ef4230e2c205 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:19:38 +0000 Subject: [PATCH 62/88] refactor: Make `plugins` a module instead of a package There's only one module in the package, we can always switch back to a package if needed later --- narwhals/_utils.py | 7 ++----- narwhals/{plugins/_utils.py => plugins.py} | 0 narwhals/plugins/__init__.py | 0 narwhals/translate.py | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) rename narwhals/{plugins/_utils.py => plugins.py} (100%) delete mode 100644 narwhals/plugins/__init__.py diff --git a/narwhals/_utils.py b/narwhals/_utils.py index 509239da55..929c81a220 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -73,11 +73,7 @@ CompliantSeriesT, NativeSeriesT_co, ) - from narwhals._compliant.typing import ( - EvalNames, - NativeDataFrameT, - NativeLazyFrameT, - ) + from narwhals._compliant.typing import EvalNames, NativeDataFrameT, NativeLazyFrameT from narwhals._namespace import ( Namespace, _NativeArrow, @@ -2067,6 +2063,7 @@ def deep_getattr(obj: Any, name_1: str, *nested: str) -> Any: """Perform a nested attribute lookup on `obj`.""" return deep_attrgetter(name_1, *nested)(obj) + class Compliant( _StoresNative[NativeT_co], _StoresImplementation, Protocol[NativeT_co] ): ... diff --git a/narwhals/plugins/_utils.py b/narwhals/plugins.py similarity index 100% rename from narwhals/plugins/_utils.py rename to narwhals/plugins.py diff --git a/narwhals/plugins/__init__.py b/narwhals/plugins/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/narwhals/translate.py b/narwhals/translate.py index 3e5a3f5f66..994036d228 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -35,7 +35,7 @@ is_pyarrow_scalar, is_pyarrow_table, ) -from narwhals.plugins._utils import _is_native_plugin, discover_entrypoints +from narwhals.plugins import _is_native_plugin, discover_entrypoints if TYPE_CHECKING: from narwhals.dataframe import DataFrame, LazyFrame From d511fc02bcda48d8bab04ed29e5cc38624ee8ca9 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:21:22 +0000 Subject: [PATCH 63/88] chore(typing): ignore `@cache` warning --- narwhals/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/narwhals/plugins.py b/narwhals/plugins.py index bf467d69b3..4f8676b0f4 100644 --- a/narwhals/plugins.py +++ b/narwhals/plugins.py @@ -42,6 +42,6 @@ def _is_native_plugin(native_object: Any, plugin: Plugin) -> bool: pkg = plugin.NATIVE_PACKAGE return ( sys.modules.get(pkg) is not None - and _might_be(type(native_object), pkg) + and _might_be(type(native_object), pkg) # type: ignore[arg-type] and plugin.is_native(native_object) ) From 6dd4d1c7909d7c8069a16de49b60c3b006ec0e17 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:45:36 +0000 Subject: [PATCH 64/88] refactor: Expose as `plugins.from_native` Got a few more typing/docs tweaks to follow --- narwhals/plugins.py | 17 ++++++++++++++++- narwhals/translate.py | 27 ++++++++++++--------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/narwhals/plugins.py b/narwhals/plugins.py index 4f8676b0f4..d2f3a15bb5 100644 --- a/narwhals/plugins.py +++ b/narwhals/plugins.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Protocol, cast if TYPE_CHECKING: + from collections.abc import Iterator from importlib.metadata import EntryPoints from typing_extensions import LiteralString @@ -12,9 +13,11 @@ from narwhals._compliant.typing import CompliantNamespaceAny from narwhals.utils import Version +__all__ = ["Plugin", "from_native"] + @cache -def discover_entrypoints() -> EntryPoints: +def _discover_entrypoints() -> EntryPoints: from importlib.metadata import entry_points as eps group = "narwhals.plugins" @@ -45,3 +48,15 @@ def _is_native_plugin(native_object: Any, plugin: Plugin) -> bool: and _might_be(type(native_object), pkg) # type: ignore[arg-type] and plugin.is_native(native_object) ) + + +def _iter_from_native(native_object: Any, version: Version) -> Iterator[object]: + for entry_point in _discover_entrypoints(): + plugin = entry_point.load() + if _is_native_plugin(native_object, plugin): + compliant_namespace = plugin.__narwhals_namespace__(version=version) + yield compliant_namespace.from_native(native_object) + + +def from_native(native_object: Any, version: Version) -> object | None: + return next(_iter_from_native(native_object, version), None) diff --git a/narwhals/translate.py b/narwhals/translate.py index 994036d228..33e84cd365 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -5,6 +5,7 @@ from functools import wraps from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, overload +from narwhals import plugins from narwhals._constants import EPOCH, MS_PER_SECOND from narwhals._namespace import ( is_native_arrow, @@ -35,7 +36,6 @@ is_pyarrow_scalar, is_pyarrow_table, ) -from narwhals.plugins import _is_native_plugin, discover_entrypoints if TYPE_CHECKING: from narwhals.dataframe import DataFrame, LazyFrame @@ -564,20 +564,17 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 raise TypeError(msg) return Version.V1.dataframe(InterchangeFrame(native_object), level="interchange") - for entry_point in discover_entrypoints(): - plugin = entry_point.load() - if _is_native_plugin(native_object, plugin): - compliant_namespace = plugin.__narwhals_namespace__(version=version) - compliant_object = compliant_namespace.from_native(native_object) - return _translate_if_compliant( - compliant_object, - pass_through=pass_through, - eager_only=eager_only, - eager_or_interchange_only=eager_or_interchange_only, - series_only=series_only, - allow_series=allow_series, - version=version, - ) + compliant_object = plugins.from_native(native_object, version) + if compliant_object is not None: + return _translate_if_compliant( + compliant_object, + pass_through=pass_through, + eager_only=eager_only, + eager_or_interchange_only=eager_or_interchange_only, + series_only=series_only, + allow_series=allow_series, + version=version, + ) if not pass_through: msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(native_object)}" From 8a96d84a8128ab0685e3cff6c71bcc7e65ede44b Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:51:28 +0000 Subject: [PATCH 65/88] fix(typing): Add the `Plugin` annotation I forgot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I used it here (https://github.com/narwhals-dev/narwhals/pull/2978#discussion_r2330937270) ... but forgot by the next day 🤦‍♂️ (https://github.com/narwhals-dev/narwhals/pull/2978#issuecomment-3270424661) --- narwhals/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/narwhals/plugins.py b/narwhals/plugins.py index d2f3a15bb5..12f39095b0 100644 --- a/narwhals/plugins.py +++ b/narwhals/plugins.py @@ -52,7 +52,7 @@ def _is_native_plugin(native_object: Any, plugin: Plugin) -> bool: def _iter_from_native(native_object: Any, version: Version) -> Iterator[object]: for entry_point in _discover_entrypoints(): - plugin = entry_point.load() + plugin: Plugin = entry_point.load() if _is_native_plugin(native_object, plugin): compliant_namespace = plugin.__narwhals_namespace__(version=version) yield compliant_namespace.from_native(native_object) From 1944e472c1cfabfa8ce1ea00e7b6cc753538d344 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:20:51 +0000 Subject: [PATCH 66/88] docs(DRAFT): Start working on `plugins.from_native` --- narwhals/plugins.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/narwhals/plugins.py b/narwhals/plugins.py index 12f39095b0..bd4a260c15 100644 --- a/narwhals/plugins.py +++ b/narwhals/plugins.py @@ -59,4 +59,16 @@ def _iter_from_native(native_object: Any, version: Version) -> Iterator[object]: def from_native(native_object: Any, version: Version) -> object | None: + """Attempt to convert `native_object` to a Compliant object, using any available plugin(s). + + Arguments: + native_object: Raw object from user. + version: Narwhals API version. + + Returns: + (TODO: Probably better to do this through typing) + ... + + In all other cases, `None`. + """ return next(_iter_from_native(native_object, version), None) From ce712e0e574852918913dd9023c2cff40a018ad8 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:21:17 +0000 Subject: [PATCH 67/88] feat(typing): Add some slightly narrower typing --- narwhals/plugins.py | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/narwhals/plugins.py b/narwhals/plugins.py index bd4a260c15..8e92a9720b 100644 --- a/narwhals/plugins.py +++ b/narwhals/plugins.py @@ -4,17 +4,38 @@ from functools import cache from typing import TYPE_CHECKING, Any, Protocol, cast +from narwhals._compliant import CompliantNamespace +from narwhals._typing_compat import TypeVar + if TYPE_CHECKING: from collections.abc import Iterator from importlib.metadata import EntryPoints - from typing_extensions import LiteralString + from typing_extensions import LiteralString, TypeAlias - from narwhals._compliant.typing import CompliantNamespaceAny + from narwhals._compliant.typing import ( + CompliantDataFrameAny, + CompliantFrameAny, + CompliantLazyFrameAny, + CompliantSeriesAny, + ) from narwhals.utils import Version + __all__ = ["Plugin", "from_native"] +CompliantAny: TypeAlias = ( + "CompliantDataFrameAny | CompliantLazyFrameAny | CompliantSeriesAny" +) +FrameT = TypeVar( + "FrameT", + bound="CompliantFrameAny", + default="CompliantDataFrameAny | CompliantLazyFrameAny", +) +FromNativeR_co = TypeVar( + "FromNativeR_co", bound=CompliantAny, covariant=True, default=CompliantAny +) + @cache def _discover_entrypoints() -> EntryPoints: @@ -26,10 +47,16 @@ def _discover_entrypoints() -> EntryPoints: return eps(group=group) -class Plugin(Protocol): +class PluginNamespace(CompliantNamespace[FrameT, Any], Protocol[FrameT, FromNativeR_co]): + def from_native(self, data: Any, /) -> FromNativeR_co: ... + + +class Plugin(Protocol[FrameT, FromNativeR_co]): NATIVE_PACKAGE: LiteralString - def __narwhals_namespace__(self, version: Version) -> CompliantNamespaceAny: ... + def __narwhals_namespace__( + self, version: Version + ) -> PluginNamespace[FrameT, FromNativeR_co]: ... def is_native(self, native_object: object, /) -> bool: ... @@ -50,7 +77,7 @@ def _is_native_plugin(native_object: Any, plugin: Plugin) -> bool: ) -def _iter_from_native(native_object: Any, version: Version) -> Iterator[object]: +def _iter_from_native(native_object: Any, version: Version) -> Iterator[CompliantAny]: for entry_point in _discover_entrypoints(): plugin: Plugin = entry_point.load() if _is_native_plugin(native_object, plugin): @@ -58,7 +85,7 @@ def _iter_from_native(native_object: Any, version: Version) -> Iterator[object]: yield compliant_namespace.from_native(native_object) -def from_native(native_object: Any, version: Version) -> object | None: +def from_native(native_object: Any, version: Version) -> CompliantAny | None: """Attempt to convert `native_object` to a Compliant object, using any available plugin(s). Arguments: From 7bcec6199fe83ef2ddec9c76786c9c3c49612a1f Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:23:43 +0000 Subject: [PATCH 68/88] docs: Update `plugins.from_native` --- narwhals/plugins.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/narwhals/plugins.py b/narwhals/plugins.py index 8e92a9720b..c994509a02 100644 --- a/narwhals/plugins.py +++ b/narwhals/plugins.py @@ -27,6 +27,8 @@ CompliantAny: TypeAlias = ( "CompliantDataFrameAny | CompliantLazyFrameAny | CompliantSeriesAny" ) +"""A statically-unknown, Compliant object originating from a plugin.""" + FrameT = TypeVar( "FrameT", bound="CompliantFrameAny", @@ -93,9 +95,16 @@ def from_native(native_object: Any, version: Version) -> CompliantAny | None: version: Narwhals API version. Returns: - (TODO: Probably better to do this through typing) - ... + If the following conditions are met + - at least 1 plugin is installed + - at least 1 installed plugin supports `type(native_object)` + + Then for the **first matching plugin**, the result of the call below. + This *should* be an object accepted by a Narwhals Dataframe, Lazyframe, or Series: + + plugin: Plugin + plugin.__narwhals_namespace__(version).from_native(native_object) - In all other cases, `None`. + In all other cases, `None` is returned instead. """ return next(_iter_from_native(native_object, version), None) From 013880338c8e45d6079c0e289dadc5f4d06c34a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mich=C3=A8le=20Pettinato?= Date: Tue, 30 Sep 2025 12:02:13 +0200 Subject: [PATCH 69/88] fix mess in pytest.yml adapted l.88 to what's in main already --- .github/workflows/pytest.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c2fe07ec7c..070d111b74 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -84,11 +84,10 @@ jobs: cache-suffix: pytest-full-coverage-${{ matrix.python-version }} cache-dependency-glob: "pyproject.toml" - name: install-reqs - run: uv pip install -e ".[dask, modin, ibis]" --group core-tests --group extra --system + run: uv pip install -e ".[dask, modin, ibis]" --group core-tests --group extra "duckdb<1.4" --system + # TODO(FBruzzesi): Unpin duckdb version once ibis makes a new release - name: install-test-plugin run: uv pip install -e tests/test_plugin --system - # TODO(FBruzzesi): Unpin duckdb version once ibis makes a new release - run: uv pip install -e ".[dask, modin, ibis]" --group core-tests --group extra "duckdb<1.4" --system - name: show-deps run: uv pip freeze - name: Run pytest From fa09ba6570ee74abab9732eced00c6acb30be7ab Mon Sep 17 00:00:00 2001 From: ym-pett Date: Tue, 30 Sep 2025 14:31:30 +0200 Subject: [PATCH 70/88] modified pytest.yml --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 070d111b74..3c39cea546 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -84,7 +84,7 @@ jobs: cache-suffix: pytest-full-coverage-${{ matrix.python-version }} cache-dependency-glob: "pyproject.toml" - name: install-reqs - run: uv pip install -e ".[dask, modin, ibis]" --group core-tests --group extra "duckdb<1.4" --system + run: uv pip install -e ".[dask, modin, ibis]" --group core-tests --group extra --system # TODO(FBruzzesi): Unpin duckdb version once ibis makes a new release - name: install-test-plugin run: uv pip install -e tests/test_plugin --system From 213cc315a255becc12ee036ecfaeded8a1a148f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mich=C3=A8le=20Pettinato?= Date: Tue, 30 Sep 2025 16:51:24 +0200 Subject: [PATCH 71/88] Update pytest.yml add "duckdb<1.4" back in --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 3c39cea546..070d111b74 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -84,7 +84,7 @@ jobs: cache-suffix: pytest-full-coverage-${{ matrix.python-version }} cache-dependency-glob: "pyproject.toml" - name: install-reqs - run: uv pip install -e ".[dask, modin, ibis]" --group core-tests --group extra --system + run: uv pip install -e ".[dask, modin, ibis]" --group core-tests --group extra "duckdb<1.4" --system # TODO(FBruzzesi): Unpin duckdb version once ibis makes a new release - name: install-test-plugin run: uv pip install -e tests/test_plugin --system From da2d115a7f177acba0c89fbed05849ddbff01599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mich=C3=A8le=20Pettinato?= Date: Tue, 30 Sep 2025 16:54:42 +0200 Subject: [PATCH 72/88] Update pytest.yml moved comment back --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 070d111b74..2cc5bec675 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -84,8 +84,8 @@ jobs: cache-suffix: pytest-full-coverage-${{ matrix.python-version }} cache-dependency-glob: "pyproject.toml" - name: install-reqs - run: uv pip install -e ".[dask, modin, ibis]" --group core-tests --group extra "duckdb<1.4" --system # TODO(FBruzzesi): Unpin duckdb version once ibis makes a new release + run: uv pip install -e ".[dask, modin, ibis]" --group core-tests --group extra "duckdb<1.4" --system - name: install-test-plugin run: uv pip install -e tests/test_plugin --system - name: show-deps From 3a10b683de073a2c56e8144587b8c33e8266ffae Mon Sep 17 00:00:00 2001 From: ym-pett Date: Tue, 30 Sep 2025 21:34:47 +0200 Subject: [PATCH 73/88] added _with_native to plugintest --- tests/test_plugin/test_plugin/dataframe.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_plugin/test_plugin/dataframe.py b/tests/test_plugin/test_plugin/dataframe.py index 37feed400c..5095cf1dca 100644 --- a/tests/test_plugin/test_plugin/dataframe.py +++ b/tests/test_plugin/test_plugin/dataframe.py @@ -31,6 +31,9 @@ def __init__(self, native_dataframe: DictFrame, *, version: Version) -> None: def __narwhals_lazyframe__(self) -> Self: return self + def _with_native(self, df: DictFrame) -> Self: + return self.__class__(df, version=self._version) + def _with_version(self, version: Version) -> Self: return self.__class__(self._native_frame, version=version) From afd4d835bb018742c08465d5f139e52f1cb50679 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 8 Oct 2025 14:37:57 +0200 Subject: [PATCH 74/88] added is_native not implemented to plugin tests --- tests/test_plugin/test_plugin/namespace.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_plugin/test_plugin/namespace.py b/tests/test_plugin/test_plugin/namespace.py index 0cba9e5964..3ab1a04443 100644 --- a/tests/test_plugin/test_plugin/namespace.py +++ b/tests/test_plugin/test_plugin/namespace.py @@ -17,6 +17,7 @@ def __init__(self, *, version: Version) -> None: def from_native(self, native_object: DictFrame) -> DictLazyFrame: return DictLazyFrame(native_object, version=self._version) + is_native: Any = not_implemented() _expr: Any = not_implemented() _implementation: Any = not_implemented() len: Any = not_implemented() From 910a31ee29b528c887e0e8da45927ab7a3197372 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 8 Oct 2025 15:38:45 +0200 Subject: [PATCH 75/88] defined is_native properly --- tests/test_plugin/test_plugin/namespace.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_plugin/test_plugin/namespace.py b/tests/test_plugin/test_plugin/namespace.py index 3ab1a04443..e058279978 100644 --- a/tests/test_plugin/test_plugin/namespace.py +++ b/tests/test_plugin/test_plugin/namespace.py @@ -7,6 +7,8 @@ from tests.test_plugin.test_plugin.dataframe import DictFrame, DictLazyFrame if TYPE_CHECKING: + from typing_extensions import TypeIs + from narwhals.utils import Version @@ -17,7 +19,9 @@ def __init__(self, *, version: Version) -> None: def from_native(self, native_object: DictFrame) -> DictLazyFrame: return DictLazyFrame(native_object, version=self._version) - is_native: Any = not_implemented() + def is_native(self, obj: DictFrame) -> TypeIs[DictFrame]: + return isinstance(obj, DictLazyFrame) + _expr: Any = not_implemented() _implementation: Any = not_implemented() len: Any = not_implemented() From f227c13f6b13e9a25b39dc1578a233ccbaeb3d1c Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 8 Oct 2025 17:15:49 +0200 Subject: [PATCH 76/88] using not_implemented and nocover to silence plugin test failures --- narwhals/plugins.py | 2 +- tests/test_plugin/test_plugin/dataframe.py | 6 +++--- tests/test_plugin/test_plugin/namespace.py | 6 +----- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/narwhals/plugins.py b/narwhals/plugins.py index c994509a02..c68e606ed4 100644 --- a/narwhals/plugins.py +++ b/narwhals/plugins.py @@ -63,7 +63,7 @@ def is_native(self, native_object: object, /) -> bool: ... @cache -def _might_be(cls: type, type_: str) -> bool: +def _might_be(cls: type, type_: str) -> bool: # pragma: no cover try: return any(type_ in o.__module__.split(".") for o in cls.mro()) except TypeError: diff --git a/tests/test_plugin/test_plugin/dataframe.py b/tests/test_plugin/test_plugin/dataframe.py index 5095cf1dca..3b4b5fb3a8 100644 --- a/tests/test_plugin/test_plugin/dataframe.py +++ b/tests/test_plugin/test_plugin/dataframe.py @@ -34,13 +34,13 @@ def __narwhals_lazyframe__(self) -> Self: def _with_native(self, df: DictFrame) -> Self: return self.__class__(df, version=self._version) - def _with_version(self, version: Version) -> Self: - return self.__class__(self._native_frame, version=version) - @property def columns(self) -> list[str]: return list(self._native_frame.keys()) + _with_native = not_implemented() + _with_version = not_implemented() + # Dunders __narwhals_namespace__ = not_implemented() __native_namespace__ = not_implemented() diff --git a/tests/test_plugin/test_plugin/namespace.py b/tests/test_plugin/test_plugin/namespace.py index e058279978..3ab1a04443 100644 --- a/tests/test_plugin/test_plugin/namespace.py +++ b/tests/test_plugin/test_plugin/namespace.py @@ -7,8 +7,6 @@ from tests.test_plugin.test_plugin.dataframe import DictFrame, DictLazyFrame if TYPE_CHECKING: - from typing_extensions import TypeIs - from narwhals.utils import Version @@ -19,9 +17,7 @@ def __init__(self, *, version: Version) -> None: def from_native(self, native_object: DictFrame) -> DictLazyFrame: return DictLazyFrame(native_object, version=self._version) - def is_native(self, obj: DictFrame) -> TypeIs[DictFrame]: - return isinstance(obj, DictLazyFrame) - + is_native: Any = not_implemented() _expr: Any = not_implemented() _implementation: Any = not_implemented() len: Any = not_implemented() From 3ef415d822d3aa1356fa8cdd4041590e53b2faf5 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 8 Oct 2025 17:30:43 +0200 Subject: [PATCH 77/88] removed duplicate definition --- tests/test_plugin/test_plugin/dataframe.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_plugin/test_plugin/dataframe.py b/tests/test_plugin/test_plugin/dataframe.py index 3b4b5fb3a8..9b7b28d207 100644 --- a/tests/test_plugin/test_plugin/dataframe.py +++ b/tests/test_plugin/test_plugin/dataframe.py @@ -31,9 +31,6 @@ def __init__(self, native_dataframe: DictFrame, *, version: Version) -> None: def __narwhals_lazyframe__(self) -> Self: return self - def _with_native(self, df: DictFrame) -> Self: - return self.__class__(df, version=self._version) - @property def columns(self) -> list[str]: return list(self._native_frame.keys()) From 793a1cd1f600ae50bc9c06015520e35a2626330e Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 8 Oct 2025 17:47:09 +0200 Subject: [PATCH 78/88] trying to silence colums coverage error --- tests/test_plugin/test_plugin/dataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_plugin/test_plugin/dataframe.py b/tests/test_plugin/test_plugin/dataframe.py index 9b7b28d207..dbe4d65268 100644 --- a/tests/test_plugin/test_plugin/dataframe.py +++ b/tests/test_plugin/test_plugin/dataframe.py @@ -32,7 +32,7 @@ def __narwhals_lazyframe__(self) -> Self: return self @property - def columns(self) -> list[str]: + def columns(self) -> list[str]: # pragma: no cover return list(self._native_frame.keys()) _with_native = not_implemented() From c0d9d006d57627b4115d7aab7d32456d1b66e237 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Wed, 8 Oct 2025 18:04:33 +0200 Subject: [PATCH 79/88] seeing if no cover works in this file --- tests/plugins_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/plugins_test.py b/tests/plugins_test.py index f5a5ab44dd..64f6239bec 100644 --- a/tests/plugins_test.py +++ b/tests/plugins_test.py @@ -12,5 +12,5 @@ def test_plugin() -> None: pytest.importorskip("test_plugin") df_native = {"a": [1, 1, 2], "b": [4, 5, 6]} lf = nw.from_native(df_native) # type: ignore[call-overload] - assert isinstance(lf, nw.LazyFrame) - assert lf.columns == ["a", "b"] + assert isinstance(lf, nw.LazyFrame) # pragma: no cover + assert lf.columns == ["a", "b"] # pragma: no cover From 725ce3c8d9eacab640d107fe9759f8b459528e88 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Fri, 10 Oct 2025 18:49:44 +0200 Subject: [PATCH 80/88] implementing _with_version to stop test failing, cleaned up unnecessary no covers --- tests/plugins_test.py | 4 ++-- tests/test_plugin/test_plugin/dataframe.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/plugins_test.py b/tests/plugins_test.py index 64f6239bec..f5a5ab44dd 100644 --- a/tests/plugins_test.py +++ b/tests/plugins_test.py @@ -12,5 +12,5 @@ def test_plugin() -> None: pytest.importorskip("test_plugin") df_native = {"a": [1, 1, 2], "b": [4, 5, 6]} lf = nw.from_native(df_native) # type: ignore[call-overload] - assert isinstance(lf, nw.LazyFrame) # pragma: no cover - assert lf.columns == ["a", "b"] # pragma: no cover + assert isinstance(lf, nw.LazyFrame) + assert lf.columns == ["a", "b"] diff --git a/tests/test_plugin/test_plugin/dataframe.py b/tests/test_plugin/test_plugin/dataframe.py index dbe4d65268..688561af8e 100644 --- a/tests/test_plugin/test_plugin/dataframe.py +++ b/tests/test_plugin/test_plugin/dataframe.py @@ -36,7 +36,9 @@ def columns(self) -> list[str]: # pragma: no cover return list(self._native_frame.keys()) _with_native = not_implemented() - _with_version = not_implemented() + + def _with_version(self, version: Version) -> Self: + return self.__class__(self._native_frame, version=version) # Dunders __narwhals_namespace__ = not_implemented() From 242d8dd91d1c7c0cc46a93b0a7025c85b7c73dee Mon Sep 17 00:00:00 2001 From: ym-pett Date: Mon, 13 Oct 2025 13:55:47 +0200 Subject: [PATCH 81/88] removing skip for lower versions as it now runs for all --- tests/plugins_test.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/plugins_test.py b/tests/plugins_test.py index f5a5ab44dd..82fb616aa9 100644 --- a/tests/plugins_test.py +++ b/tests/plugins_test.py @@ -1,13 +1,10 @@ from __future__ import annotations -import sys - import pytest import narwhals as nw -@pytest.mark.skipif(sys.version_info < (3, 10), reason="3.10+ required for entrypoints") def test_plugin() -> None: pytest.importorskip("test_plugin") df_native = {"a": [1, 1, 2], "b": [4, 5, 6]} From 62ea8c97094bc3078892336c039b72675bdf2564 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Mon, 13 Oct 2025 14:02:52 +0200 Subject: [PATCH 82/88] added pytest to 39 and windows --- .github/workflows/pytest.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 375a3abe98..555f2aecfc 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -33,6 +33,8 @@ jobs: run: uv pip freeze - name: Run pytest run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=75 --constructors=pandas,pyarrow,polars[eager],polars[lazy] + - name: install-test-plugin + run: uv pip install -e tests/test_plugin --system pytest-windows: strategy: @@ -55,6 +57,8 @@ jobs: # we are not testing pyspark on Windows here because it is very slow # TODO(FBruzzesi): Unpin duckdb version once ibis makes a new release run: uv pip install -e ".[dask, modin, ibis]" --group core-tests --group extra "duckdb<1.4" --system + - name: install-test-plugin + run: uv pip install -e tests/test_plugin --system - name: show-deps run: uv pip freeze - name: Run pytest From 29d7b8ee96a990f5b20aad0185e40cc76c050a87 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Tue, 14 Oct 2025 17:37:10 +0200 Subject: [PATCH 83/88] fixing TYP001 guard import --- tests/test_plugin/test_plugin/dataframe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_plugin/test_plugin/dataframe.py b/tests/test_plugin/test_plugin/dataframe.py index 688561af8e..80d1ee840d 100644 --- a/tests/test_plugin/test_plugin/dataframe.py +++ b/tests/test_plugin/test_plugin/dataframe.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, TypeAlias +from typing import TYPE_CHECKING, Any from narwhals._utils import ( Implementation, @@ -13,7 +13,7 @@ DictFrame: TypeAlias = dict[str, list[Any]] if TYPE_CHECKING: - from typing_extensions import Self + from typing_extensions import Self, TypeAlias from narwhals import LazyFrame # noqa: F401 From ee3784886969f126126d7ec948e86b0659ade5e2 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Thu, 16 Oct 2025 10:49:32 +0200 Subject: [PATCH 84/88] fixing import order --- tests/test_plugin/test_plugin/dataframe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_plugin/test_plugin/dataframe.py b/tests/test_plugin/test_plugin/dataframe.py index 80d1ee840d..74d539b2ec 100644 --- a/tests/test_plugin/test_plugin/dataframe.py +++ b/tests/test_plugin/test_plugin/dataframe.py @@ -10,13 +10,13 @@ ) from narwhals.typing import CompliantLazyFrame -DictFrame: TypeAlias = dict[str, list[Any]] - if TYPE_CHECKING: from typing_extensions import Self, TypeAlias from narwhals import LazyFrame # noqa: F401 +DictFrame: TypeAlias = dict[str, list[Any]] + class DictLazyFrame( CompliantLazyFrame[Any, "DictFrame", "LazyFrame[DictFrame]"], # type: ignore[type-var] From 28ca7fd961cebfc5c3b2d21c63fdf000864fab51 Mon Sep 17 00:00:00 2001 From: ym-pett Date: Thu, 16 Oct 2025 21:20:25 +0200 Subject: [PATCH 85/88] fixing typos --- docs/extending.md | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/extending.md b/docs/extending.md index 37ff88720f..7035ddb64e 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -6,10 +6,12 @@ If you want your own library to be recognised too, you're welcome open a PR (with tests)!. Alternatively, if you can't do that (for example, if you library is closed-source), see -the next section for what else you can do. +the next sections for what else you can do. + +## Creating an Extension We love open source, but we're not "open source absolutists". If you're unable to open -source you library, then this is how you can make your library compatible with Narwhals. +source your library, then this is how you can make your library compatible with Narwhals. Make sure that you also define: @@ -33,3 +35,34 @@ Make sure that you also define: Note that this "extension" mechanism is still experimental. If anything is not clear, or doesn't work, please do raise an issue or contact us on Discord (see the link on the README). + +## Creating a Plugin + +Another option is to write a plugin. Narwhals itself has the necessary utilities to detect and handle +plugins. For this integration to work, any plugin architecture must contain the following: + + 1. an entrypoint defined in a `pyproject.toml` file: + + ``` + [project.entry-points.'narwhals.plugins'] + narwhals- = 'narwhals_' + ``` + The first line needs to be the same for all plugins, whereas the second is to be adapted to the + library name. + + 2. a top-level `__init__.py` file containing the following: + + - a `is_native` and a `__narwhals_namespace__` function + - a string constant `NATIVE_PACKAGE` which holds the name of the library for which the plugin is made + + `is_native` must receive a native object and return a boolean indicating whether the native object is + a dataframe of the plugin library. + + `__narwhals_namespace__` takes the Narwhals version and returns a compliant namespace for the library, + i.e. one that complies with the CompliantNamespace protocol. This protocol specifies a `from_native` + function, whose input parameter is the Narwhals version and which returns a compliant Narwhals LazyFrame + which wraps the native dataframe. + +If you want to see an example of a plugin, we have implemented a bare-bones version for the `daft` library +that allows users to pass daft dataframes to Narwhals: +[narwhals-daft](https://github.com/MarcoGorelli/narwhals-daft). From 840c65f763b1fb34fc08c620870490826ed24b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mich=C3=A8le=20Pettinato?= Date: Sat, 18 Oct 2025 15:09:03 +0200 Subject: [PATCH 86/88] Update docs/extending.md Co-authored-by: Marco Edward Gorelli <33491632+MarcoGorelli@users.noreply.github.com> --- docs/extending.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extending.md b/docs/extending.md index 7035ddb64e..e80cd80e21 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -52,7 +52,7 @@ plugins. For this integration to work, any plugin architecture must contain the 2. a top-level `__init__.py` file containing the following: - - a `is_native` and a `__narwhals_namespace__` function + - `is_native` and `__narwhals_namespace__` functions - a string constant `NATIVE_PACKAGE` which holds the name of the library for which the plugin is made `is_native` must receive a native object and return a boolean indicating whether the native object is From 4550ed35b5ecd868d5629b00ddb7bf5957b706bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mich=C3=A8le=20Pettinato?= Date: Sat, 18 Oct 2025 15:10:04 +0200 Subject: [PATCH 87/88] Update docs/extending.md `is_native` explanation Co-authored-by: Marco Edward Gorelli <33491632+MarcoGorelli@users.noreply.github.com> --- docs/extending.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/extending.md b/docs/extending.md index e80cd80e21..bf750520d8 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -55,8 +55,8 @@ plugins. For this integration to work, any plugin architecture must contain the - `is_native` and `__narwhals_namespace__` functions - a string constant `NATIVE_PACKAGE` which holds the name of the library for which the plugin is made - `is_native` must receive a native object and return a boolean indicating whether the native object is - a dataframe of the plugin library. + `is_native` accepts a native object and returns a boolean indicating whether the native object is + a dataframe of the library the plugin was written for. `__narwhals_namespace__` takes the Narwhals version and returns a compliant namespace for the library, i.e. one that complies with the CompliantNamespace protocol. This protocol specifies a `from_native` From 79ce894826bb6aa38c2ccd75ef9b8ef9f7c28b1f Mon Sep 17 00:00:00 2001 From: ym-pett Date: Sat, 18 Oct 2025 15:29:54 +0200 Subject: [PATCH 88/88] made suggested changes to docs --- docs/extending.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/extending.md b/docs/extending.md index bf750520d8..0a59eb949f 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -13,7 +13,7 @@ the next sections for what else you can do. We love open source, but we're not "open source absolutists". If you're unable to open source your library, then this is how you can make your library compatible with Narwhals. -Make sure that you also define: +Make sure that you define: - `DataFrame.__narwhals_dataframe__`: return an object which implements methods from the `CompliantDataFrame` protocol in `narwhals/typing.py`. @@ -38,8 +38,9 @@ doesn't work, please do raise an issue or contact us on Discord (see the link on ## Creating a Plugin -Another option is to write a plugin. Narwhals itself has the necessary utilities to detect and handle -plugins. For this integration to work, any plugin architecture must contain the following: +If it's not possible to add extra functions like `__narwhals_namespace__` and others to a dataframe object +itself, then another option is to write a plugin. Narwhals itself has the necessary utilities to detect and +handle plugins. For this integration to work, any plugin architecture must contain the following: 1. an entrypoint defined in a `pyproject.toml` file: @@ -47,8 +48,9 @@ plugins. For this integration to work, any plugin architecture must contain the [project.entry-points.'narwhals.plugins'] narwhals- = 'narwhals_' ``` - The first line needs to be the same for all plugins, whereas the second is to be adapted to the - library name. + The section name needs to be the same for all plugins; inside it, plugin creators can replace their + own library name, for example `narwhals-grizzlies = 'narwhals_grizzlies'` + 2. a top-level `__init__.py` file containing the following: