Skip to content

feat: Extend plugin support for IO and from_* methods/functions#3753

Open
FBruzzesi wants to merge 5 commits into
mainfrom
feat/extend-from-backend
Open

feat: Extend plugin support for IO and from_* methods/functions#3753
FBruzzesi wants to merge 5 commits into
mainfrom
feat/extend-from-backend

Conversation

@FBruzzesi

Copy link
Copy Markdown
Member

Description

The core changes are in the eager_namespace for the backend resolution logic, which allows for backend= arguments to resolve for Narwhals plugins: users can pass a plugin's entry point name, module name, or the module itself.

  • IO functions {read,scan}_{csv,parquet} dispatch to same-named top-level plugin hooks.
  • Eager constructors (from_*, new_series, DataFrame.from_*, Series.from_*) work with zero extra plugin code if the plugin's compliant namespace implements the EagerNamespace protocol.

Most of the lines changes are in packages/test-plugin and tests/plugins_test.py so that we have coverage for the functionalities.

What type of PR is this? (check all applicable)

  • 💾 Refactor
  • ✨ Feature
  • 🐛 Bug Fix
  • 🔧 Optimization
  • 📝 Documentation
  • ✅ Test
  • 🐳 Other

Related issues

@FBruzzesi FBruzzesi added enhancement New feature or request extensibility labels Jul 5, 2026
@FBruzzesi FBruzzesi marked this pull request as ready for review July 5, 2026 11:30
@dangotbanned

dangotbanned commented Jul 5, 2026

Copy link
Copy Markdown
Member

Hey @FBruzzesi, just thought I'd share this here as I hinted at something I did in narwhals._plan a while back.
But I didn't really explain that it solved this issue 😅

Important

I'm not saying you need to do this, just inspiration if you want it

Examples

So here's a breakdown ...

read_csv

Most things look like some variation of this:

def read_csv(
source: FileSource, *, backend: IntoBackend[EagerAllowed], **kwds: Any
) -> DataFrame[Any, Any]:
source = normalize_path(source)
manager = plugins.manager()
return manager.dataframe(backend, _MAIN).read_csv(source, **kwds).to_narwhals()

Step 1

Get or initialize the plugin manager:

manager = plugins.manager()

Step 2

Import the compliant-level type that we want to construct with 1:

tp_frame: type[CompliantDataFrame] = manager.dataframe(backend, Version.MAIN)

Step 3

Call the @classmethod constructor :

compliant_frame: CompliantDataFrame = tp_frame.read_csv(normalize_path(source), **kwds)

Yes, all the constructors are now @classmethod(s).
This makes a bunch of things simpler (including typing 😄):

@classmethod
def from_dict(
cls, data: Mapping[str, Any], /, *, schema: IntoSchema | None = None
) -> Self: ...
@classmethod
def read_csv(cls, source: str, /, **kwds: Any) -> Self: ...
@classmethod
def read_parquet(cls, source: str, /, **kwds: Any) -> Self: ...

def concat(cls, dfs: Iterable[Self], /, how: ConcatMethod) -> Self: ...

Step 4

Wrap as the the narwhals-level object we'd like to return 2:

result: DataFrame = compliant_frame.to_narwhals()

Others

All follow the roughly same pattern, which is basically a successor to #2324 (minus two levels of Namespace):

Related

Footnotes

  1. You can ignore the version part for now

  2. The Version.MAIN has already propagated from the first call

  3. Skips the plugin manager part, but benefits from the @classmethod being exactly where we need it 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request extensibility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Find way to support scan_csv for plugins

2 participants