Skip to content

feat(typing): Introduce PluginName NewType so plugin backends can type check - #3794

Merged
FBruzzesi merged 4 commits into
mainfrom
fix/plugin-typing
Jul 17, 2026
Merged

feat(typing): Introduce PluginName NewType so plugin backends can type check#3794
FBruzzesi merged 4 commits into
mainfrom
fix/plugin-typing

Conversation

@FBruzzesi

@FBruzzesi FBruzzesi commented Jul 15, 2026

Copy link
Copy Markdown
Member

Description

Other prerequisite for #3753

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

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

Related

@FBruzzesi FBruzzesi added enhancement New feature or request typing labels Jul 15, 2026
@FBruzzesi
FBruzzesi marked this pull request as ready for review July 15, 2026 14:02
@dangotbanned
dangotbanned self-requested a review July 16, 2026 10:22
Comment thread src/narwhals/_typing.py
parameter, e.g. `IntoBackend[EagerAllowed | PluginName]`.
"""

BackendT = TypeVar("BackendT", bound=Backend | PluginName)

@dangotbanned dangotbanned Jul 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this!

Much better idea than I had, while getting to the same goal 🥳

Edit: woops that was meant to start the review

@dangotbanned dangotbanned left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @FBruzzesi, just a few suggestions/questions from me

this narwhal loves you

Comment thread tests/plugins_test.py Outdated
import narwhals.stable.v1.dependencies as nw_v1_dependencies
import narwhals.stable.v2.dependencies as nw_v2_dependencies
from narwhals import dependencies as nw_dependencies
from narwhals._typing import PluginName

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suggestion is only a part of it 😄

Suggested change
from narwhals._typing import PluginName
from narwhals.plugins import PluginName

I guess the main part is, do we need to make a decision here on this bit of (#3753 (comment))?

All that is left is to decide where the wrapping in PluginName happens
I see 3 options for that, but it could be a mix of them too:

Show me the money

  1. The plugin author does it explicitly, somewhere here (and/or alongside where they write Implementation.UNKNOWN)
    class Plugin(Protocol[FrameT, FromNativeR_co]):
    @property
    def NATIVE_PACKAGE(self) -> LiteralString: ... # noqa: N802
    def __narwhals_namespace__(
    self, version: Version
    ) -> PluginNamespace[FrameT, FromNativeR_co]: ...
    def is_native(self, native_object: object, /) -> bool: ...
  2. We wrap EntryPoint.name implicitly here
    def _plugin_names() -> tuple[str, ...]:
    """Entry point names of all installed plugins."""
    return tuple(entry_point.name for entry_point in _discover_entrypoints())
  3. We expose PluginName and users/downstream do everything imperatively

I like first choice.
The second would block how we might solve How do we introduce the concepts of {Eager,Lazy}{Allowed,Only} for plugins, when we can't define them in a Literal?.
The third I'd be okay with as an escape hatch - but as the only option it would be cumbersome.

Maybe the simplest thing we could do in this PR is make PluginName accessible from narwhals.plugins?
It could be defined in _typing.py or in plugins.py

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. I would also prefer option 1 if feasible. As you also mentioned, for now I will just make PluginName exposed in plugins.py

@dangotbanned dangotbanned Jul 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you also mentioned, for now I will just make PluginName exposed in plugins.py

#3794 (comment)

Also made me realize that one should import from two modules:

One solution to that is make the symbol(s) available in multiple places

Examples

1

This module defines and exports 2 protocols.

While I was implementing them, having to import Implementation from _utils was giving me a headache.
So I made it available here too

__all__ = ("Builtin", "Implementation", "Plugin")

2

This package imports and re-exports those 2 protocols, plus some other stuff

__all__ = ("Plugin", "Builtin", "load_plugin", "manager") # noqa: RUF022

3

I defined PluginName here. I don't have a reason, but that is what I did 😂

PluginName = NewType("PluginName", str)

I guess the only difference vs what you had was that this is typing.py vs _typing.py.
I probably wouldn't have blinked if the import came from anything public 😄

It isn't a perfect solution, as you might question which is the canonical one?
But idk, sometimes the world is a hierarchy and sometimes you have things that fit multiple boxes

Comment thread src/narwhals/_typing.py Outdated
Comment thread src/narwhals/_typing.py Outdated
Comment thread src/narwhals/_typing.py Outdated
Comment thread tests/plugins_test.py
Comment thread src/narwhals/_utils.py
Comment thread src/narwhals/series.py
dtype: IntoDType | None = None,
*,
backend: IntoBackend[EagerAllowed],
backend: IntoBackend[EagerAllowed | PluginName],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Not a review, just trying to add context from our conversation on discord

If you are downstream from Narwhals and make use of our typing - we have been suggesting (#3149 (comment)) you write things like:

backend: IntoBackend[EagerAllowed]

Following this PR, that type describes exactly the same thing.
It does not indicate plugin support, meaning that there should be no expectation that downstream tests against plugins.

If downstream wants to opt-in to plugins, the update to their typing small:

- backend: IntoBackend[EagerAllowed]
+ backend: IntoBackend[EagerAllowed | PluginName]

But it gives a very clear signal (to their users) that plugins should work.
I hope that downstream (authors) will be more likely to update their tests, to account for plugins, when these concepts are visible in each signature with a backend 🙂

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this should go somewhere! but where exactly? 🧐

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also made me realize that one should import from two modules:

from narwhals.typing import IntoBackend, EagerAllowed
from narwhals.plugins import PluginName

...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this should go somewhere! but where exactly? 🧐

We could do either the PR description or the release notes until we find somewhere else?
For release notes I mean if we had a short highlight message in this style (https://github.com/vega/altair/releases#release-v5.5.0)

Both can work too 😅

Comment thread src/narwhals/_typing.py
Comment thread src/narwhals/_typing.py
- Wrap an entry point name to pass it wherever a `backend` is expected, e.g. `PluginName("my-plugin")`.
- Add it to a signature's `IntoBackend` parameter to advertise plugin support, e.g. `IntoBackend[EagerAllowed | PluginName]`.

See the `narwhals.plugins` module for how plugin authors register entry points

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So... when I went to add the reference via [...][], I realized that we have no api-reference for plugins 😭 that's a needed follow-up

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a needed follow-up

Agreed!

For now, something I discovered with (microsoft/pylance-release#6611) - is that it supports lots of stuff (by default) that we either:

  • haven't even got configured for mkdocstrings
  • aren't actually possible
Cross ref to a function in the same module, but isn't API reference

mkdocstrings has a support for 2 kinds of cross-refs that we haven't got enabled.
IIRC, the syntax was different and it didn't work (yet?) in zensical.
But it still wouldn't be able to do this wizardry 😄

image

Cross ref to 3rd-party symbols

We can do something similar by adding inventories (if they have one ibis-project/ibis#11723)

But this let's you link links direct to the source code 🤯

image

Try these out

diff --git a/src/narwhals/_typing.py b/src/narwhals/_typing.py
index 189a1543b..c47190a26 100644
--- a/src/narwhals/_typing.py
+++ b/src/narwhals/_typing.py
@@ -110,6 +110,16 @@ PluginName = NewType("PluginName", str)
 
 See the `narwhals.plugins` module for how plugin authors register entry points
 and the contract a wrapped name must satisfy.
+
+
+## Some examples of working cross-refs
+Try these out in your IDE
+
+- [`importlib.metadata.EntryPoint.name`][]
+- [`IntoBackend`][]
+- [`EagerAllowed`][]
+- [`narwhals.typing.LazyAllowed`][]
+- [`narwhals.plugins.Plugin`][]
 """
 
 BackendT = TypeVar("BackendT", bound=Backend | PluginName)
image

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem to work with modules though. Will merge without it for now and follow up with proper docs

@dangotbanned dangotbanned left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again @FBruzzesi

choo choo narwhal

@FBruzzesi
FBruzzesi merged commit bc64476 into main Jul 17, 2026
39 of 41 checks passed
@FBruzzesi
FBruzzesi deleted the fix/plugin-typing branch July 17, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants