-
Notifications
You must be signed in to change notification settings - Fork 167
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
base: main
Are you sure you want to change the base?
Conversation
@ym-pett did you know about Marco's PR? Apologies if you've discussed this privately 😅 |
thanks for starting this! 😄 @dangotbanned yeah we'd discussed this My hope is that we can use Daft as the way to develop the plugin system, and it can serve as a reference implementation Here's what we're aiming for: If a user has narwhals-daft installed, then they should be able to run import narwhals as nw
import daft
df_native = daft.from_pydict({"a": [1, 2, 3], "b": [4, 5, 6]})
df = nw.from_native(df_compliant)
result = df.select("a", nw.col("b") * nw.col("a"))
print(result.collect()) This needs to be done in a way that won't be specific to Daft, so that anyone can register their own plugin without Narwhals having any knowledge about it. In this PR it's currently all Daft-specific The packaging docs around entry-points might be useful here:
I'll also cc @camriddell into the conversation, as IIRC he'd also thought about pluggable backends For prior art on plugins and entry-points, I think https://github.com/PyCQA/flake8 might also be good to look at |
thanks both, I'll revert the current changes - I feel like I had to go down the wrong route first to see what this actually consists of! :) I can now go through the materials armed with more background! 🦾 |
3c8b34b
to
11fe33f
Compare
oops, didn't mean to close this! will reopen when I push new content! |
based on flake8 example - will try to get something more sensible in next |
1ac87eb
to
cfd156f
Compare
b004d4e
to
e3a2f3b
Compare
narwhals/translate.py
Outdated
for plugin in discovered_plugins: | ||
obj = plugin.load() | ||
frame = obj.dataframe.DaftLazyFrame | ||
|
||
# from obj.dataframe import DaftLazyFrame | ||
try: | ||
df_compliant = frame(native_object, version=Version.MAIN) | ||
return df_compliant.to_narwhals() | ||
except: | ||
# try the next plugin | ||
continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ooh, nice! does this work?
to make it not daft-specific, perhaps we could aim to have something like
try:
df_compliant = obj.from_native(native_object, version=Version.MAIN)
?
This would mean making a top-level function in narwhals-daft too, and then we document that plugin authors are expected to implement this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a feeling I'm doing something weird with how things are imported. Maybe it's that the top-level __init__
file needs altering in daft-narwhals.
with your suggestion, t.py no longer works and I get the error
TypeError: Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: <class 'daft.dataframe.dataframe.DataFrame'>
that dataframe.dataframe looks weird to me... would you expect that structure?
the type of plugin
is <class 'importlib.metadata.EntryPoint'>
, so I figured I had to load the module via that, for obj I then get the type <class 'module'>
the only way I could get access to the DaftLazyFrame (haven't tried simple LazyFrame yet) was by assigning it to a variable name, I couldn't do something like
from obj.dataframe import DaftLazyFrame
(the error then is ModuleNotFoundError: No module named 'obj'
)
I think at the moment this all leaves us too bound to daft, and I bet I'm breaking a million coding conventions, eek!
I suspect I need to do a better job at exposing the modules within daft-nw but I haven't found how yet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps in narwhals_daft/__init__.py
you could make a from_native
function, and then here use obj.from_native
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, will try that!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should plugin detection come before we check our own written backends? That way if someone really wanted to override our pandas
backend they would be empowered to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 yeah maybe
d72da25
to
ca01346
Compare
319b3d6
to
90ad973
Compare
5e3be1f
to
76232df
Compare
495d727
to
ebc1a8f
Compare
refactor: `plugins` uno reverse card: big thanks to @dangotbanned!
refactor: moved plugins-related into plugins file
def from_native(native_object: Any, version: Version) -> CompliantAny | None: | ||
"""Attempt to convert `native_object` to a Compliant object, using any available plugin(s). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 🙂
- refactor:
plugins
uno reverse card ym-pett/narwhals#2 (comment) - refactor:
plugins
uno reverse card ym-pett/narwhals#2 (comment) - refactor:
plugins
uno reverse card ym-pett/narwhals#2 (comment) - refactor:
plugins
uno reverse card ym-pett/narwhals#2 (comment) - refactor:
plugins
uno reverse card ym-pett/narwhals#2 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- refactor:
plugins
uno reverse card ym-pett/narwhals#2 (comment) - refactor:
plugins
uno reverse card ym-pett/narwhals#2 (comment)
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 comment
The 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.
adapted l.88 to what's in main already
enable-cache: "true" | ||
cache-suffix: pytest-full-coverage-${{ matrix.python-version }} | ||
cache-dependency-glob: "pyproject.toml" | ||
- name: install-reqs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm I tried to set l.87 to be the same as in the 'main' branch, but maybe this has contributed to the additional test failures? I will try out locally if going back to the line which doesn't specify the duckdb version prevents these new test failures. Can't quite make out if they're all duckdb related though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hey - doesn't look like it's the same, could you make sure you've fetched upstream first please?
the only diff we should be seeing here is
+ - name: install-test-plugin
+ run: uv pip install -e tests/test_plugin --system
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MarcoGorelli, I'm a bit confused as to what's happening with these nightly tests now failing (Min, old, and nightly versions / nightlies (3.12, ubuntu-latest) (pull_request) - locally my pytest runs without any failures.
Similarly these PyTest / pytest-full-coverage (3.11, ubuntu-latest) (pull_request)
Wondering if I have I introduced an error into the pytest.yml? Does the order in which the processes are listed matter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took out "duckdb<1.4" as I thought that might be the issue, but it doesn't seem to be the case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hey - doesn't look like it's the same, could you make sure you've fetched upstream first please?
the only diff we should be seeing here is
- name: install-test-plugin
run: uv pip install -e tests/test_plugin --system
just seen your comment now - will do!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok I think the diff is looking how it should now
add "duckdb<1.4" back in
moved comment back
ah still those nightly versions and full-coverage tests failing, is there anything I can investigate for those? |
the nightly one is unrelated, but the coverage one does show some missing coverage:
the typing issue is also related: /home/runner/work/narwhals/narwhals/tests/test_plugin/test_plugin/__init__.py
/home/runner/work/narwhals/narwhals/tests/test_plugin/test_plugin/__init__.py:16:12 - error: Cannot instantiate abstract class "DictNamespace"
"CompliantNamespace.is_native" is not implemented (reportAbstractUsage) |
#2978 (comment) - thanks Marco, will try to fix, may have some more concrete questions :) |
class DictNamespace(CompliantNamespace[DictLazyFrame, Any]): | ||
def __init__(self, *, version: Version) -> None: | ||
self._version = version | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wasn't sure of my definition of is_native
, but it was passing locally.
in my local version I'm not getting any test failures using pytest
, but on the remote the coverage fails; my problem is I can't see the details like you got @MarcoGorelli.
I click on the failing test

, get a long printout with the most informative being:

I've tried running just the plugins tests with pytest tests/test_plugin/test_plugin
and pytest tests/test_plugin/
, and I realise no tests have run, so maybe that's why I'm not getting any failures locally? Or am I running this incorrectly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah the tests are passing, it's just telling you that those lines of code aren't being run
- the
narwhals/plugins.py
one is a defensive check (i presume) so i'd say it's ok topragma: no cover
it - for the other uncovered methods (
_with_native
/is_native
), unless you write a test which hits them, i'd suggest to turn them intonot_implemented
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah ok, will do that for the moment, would like to figure the tests for those out when I have more time though. Putting this on my personal backlog :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ps: I'm so silly, only just realised I can look at the 'Missing' column to see which line is causing the problem! 🤦
return self | ||
|
||
@property | ||
def columns(self) -> list[str]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if it's ok to use the pragma: no cover
trick here, it won't accept not_implemented as it conflicts with another instantiation.
I'm not sure if I can use the same trick in the tests/plugins_test.py
file, will try & just roll back commit if not
def columns(self) -> list[str]: # pragma: no cover | ||
return list(self._native_frame.keys()) | ||
|
||
_with_native = not_implemented() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
having _with_native
as not implemented and the paragma: no cover
silencing in the test/plugins_test.py (ll.15-16) trick it into saying we have full test coverage, but this now complains that
E NotImplementedError: '_with_version' is not implemented for: <Implementation.UNKNOWN: 'unknown'>.
I'm not sure what I've done so far is kosher, seems weird to silence tests that were explicitly written in the test/plugins_test.py
file.
What type of PR is this? (check all applicable)
Related
plugins
uno reverse card ym-pett/narwhals#2CompliantNamespace.is_native
#3130Checklist
If you have comments or can explain your changes, please do so below
started trying to adapt
nw
so that it can read daft dataframes, but I might be barking up the wrong tree: I installed daft in the repo so that it would be available to nw. I have a feeling we want to avoid that, and I'm working as if I were adding an actual_daft
module rather than prep for a plugin.note to self: if installing daft was correct, need to add this to an install file, think it's pyproject.toml