-
Notifications
You must be signed in to change notification settings - Fork 170
feat: introduce (experimental) plugin system #2978
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 87 commits
6d36b4e
1062d99
8d4cda6
cfd156f
e3a2f3b
f65d7bf
a133796
eee9068
c3a8c4d
ca01346
c4611fe
90ad973
76232df
ebc1a8f
15d9c8c
16832f9
9e11a8f
d8663d8
34e7fb6
8a2b019
f92cdbf
54509d2
941edc4
72a5df4
e783af7
4cf2f96
a52a6bb
a4e539a
aca8cc4
02d544a
0952cd9
de0e5ce
9dfcd09
b999e9a
3460f4e
1c246dc
ab8303d
a8501f5
19dc900
42f2df8
d6a384a
9cf290d
bdd71e7
bde288f
7035533
4868aab
afd8620
b09d3ad
1a73ab8
1481d48
59589c1
6888f78
8f22fdb
b3f2b60
422ec70
b0b524b
0960d69
3207de1
d951220
ea28de2
b563ace
9847793
1f38e31
5dee0bb
c508ab0
f2d6e57
6c65fd4
b26d7d4
22362ad
d22f046
028e473
87e67bf
5c16c7f
e00c7c6
0c69762
f7765e8
e3e5ec7
0cd5f2f
d511fc0
6dd4d1c
8a96d84
1944e47
ce712e0
7bcec61
9bbc0a6
0f65066
8ad25ea
21aab4a
0138803
fa09ba6
213cc31
da2d115
3a10b68
2d3f034
547db41
afd4d83
910a31e
f227c13
3ef415d
793a1cd
c0d9d00
725ce3c
bd95e6d
150dc52
dc82860
41a64c0
242d8dd
62ea8c9
29d7b8e
1b877d4
f7e5ae9
ee37848
28ca7fd
19d08e1
840c65f
4550ed3
79ce894
a49e214
b980628
0cdb198
4a3f43b
3f06f3a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import sys | ||
| 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, TypeAlias | ||
|
|
||
| from narwhals._compliant.typing import ( | ||
| CompliantDataFrameAny, | ||
| CompliantFrameAny, | ||
| CompliantLazyFrameAny, | ||
| CompliantSeriesAny, | ||
| ) | ||
| from narwhals.utils import Version | ||
|
|
||
|
|
||
| __all__ = ["Plugin", "from_native"] | ||
|
|
||
| CompliantAny: TypeAlias = ( | ||
| "CompliantDataFrameAny | CompliantLazyFrameAny | CompliantSeriesAny" | ||
| ) | ||
| """A statically-unknown, Compliant object originating from a plugin.""" | ||
|
|
||
| FrameT = TypeVar( | ||
| "FrameT", | ||
| bound="CompliantFrameAny", | ||
| default="CompliantDataFrameAny | CompliantLazyFrameAny", | ||
| ) | ||
| FromNativeR_co = TypeVar( | ||
| "FromNativeR_co", bound=CompliantAny, covariant=True, default=CompliantAny | ||
| ) | ||
|
|
||
|
|
||
| @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 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 | ||
| ) -> PluginNamespace[FrameT, FromNativeR_co]: ... | ||
| 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) # type: ignore[arg-type] | ||
| and plugin.is_native(native_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): | ||
| compliant_namespace = plugin.__narwhals_namespace__(version=version) | ||
| yield compliant_namespace.from_native(native_object) | ||
|
|
||
|
|
||
| def from_native(native_object: Any, version: Version) -> CompliantAny | None: | ||
| """Attempt to convert `native_object` to a Compliant object, using any available plugin(s). | ||
|
Comment on lines
+90
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just linking some stuff from the other PR, @ym-pett feel free to add to this π
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
thanks @dangotbanned I couldn't find that merged branch anymore, glad you've located it! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added #3130 in the linked issues as I think it'll fix the type checking failure in the tests. |
||
| Arguments: | ||
| native_object: Raw object from user. | ||
| version: Narwhals API version. | ||
| Returns: | ||
| 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` is returned instead. | ||
| """ | ||
| return next(_iter_from_native(native_object, version), None) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| 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: | ||
ym-pett marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| from __future__ import annotations | ||
|
|
||
| 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 | ||
| from tests.test_plugin.test_plugin.namespace import DictNamespace | ||
|
|
||
|
|
||
ym-pett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| def __narwhals_namespace__(version: Version) -> DictNamespace: # noqa: N807 | ||
| from tests.test_plugin.test_plugin.namespace import DictNamespace | ||
|
|
||
| return DictNamespace(version=version) | ||
|
|
||
|
|
||
| def is_native(native_object: object) -> TypeIs[DictFrame]: | ||
| return isinstance(native_object, dict) | ||
|
|
||
|
|
||
| NATIVE_PACKAGE = "builtins" | ||
Uh oh!
There was an error while loading. Please reload this page.