Skip to content

Conversation

dangotbanned
Copy link
Member

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

  • πŸ’Ύ Refactor
  • ✨ Feature
  • πŸ› Bug Fix
  • πŸ”§ Optimization
  • πŸ“ Documentation
  • βœ… Test
  • 🐳 Other

Related issues

Checklist

  • Code follows style guide (ruff)
  • Tests added
  • Documented the changes

If you have comments or can explain your changes, please do so below

@dangotbanned dangotbanned marked this pull request as ready for review September 12, 2025 22:19
Copy link
Member

@FBruzzesi FBruzzesi left a comment

Choose a reason for hiding this comment

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

Thanks @dangotbanned - let's prioritize this.

There are a couple of relevant comments in _utils.py:

  • One regarding what to check to make sure something is an accessor
  • The other is a suggestion for how to split the _unwrap_context responsibilities

Comment on lines 1939 to 1945
def _unwrap_context(self, instance: _IntoContext) -> _FullContext:
if is_namespace_accessor(instance):
# NOTE: Should only need to do this once per class (the first time the method is called)
if "." not in self._wrapped_name:
self._wrapped_name = f"{instance._accessor}.{self._wrapped_name}"
return instance.compliant
return instance
Copy link
Member

Choose a reason for hiding this comment

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

I am not the biggest fan of this method. It tries to achieve two things:

  • qualify the method name
  • return either the instance or the instance.compliant, depending if we are in the accessor namespace or not.

I would keep the name clean up, but for how to access an element that has _backend_version and _implementation, I would prefer to move that in:

class NamespaceAccessor(_StoresCompliant[CompliantT_co], Protocol[CompliantT_co]):
    _accessor: ClassVar[Accessor]

+     @property
+     def _backend_version(self):
+         return self.compliant._backend_version
+
+     @property
+     def _implementation(self):
+         return self.compliant._implementation

i.e. by simply forwarding attributes as properties

Copy link
Member Author

Choose a reason for hiding this comment

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

ooohh nice idea! 😍

It's pretty funny how I managed to do almost the exact same thing a while back

class _SeriesNamespace( # type: ignore[misc]
_StoresCompliant[CompliantSeriesT_co],
_StoresNative[NativeSeriesT_co],
Protocol[CompliantSeriesT_co, NativeSeriesT_co],
):
_compliant_series: CompliantSeriesT_co
@property
def compliant(self) -> CompliantSeriesT_co:
return self._compliant_series
@property
def implementation(self) -> Implementation:
return self.compliant._implementation
@property
def backend_version(self) -> tuple[int, ...]:
return self.implementation._backend_version()

but seemed to fumble on this one πŸ˜‚

Copy link
Member Author

Choose a reason for hiding this comment

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

i.e. by simply forwarding attributes as properties

@FBruzzesi oh how I wish that were true πŸ˜‚

I split that into 2 commits:


(#3131) is playing a part in the complexity. I chose to keep the properties shown in (#3127 (comment)) - as updating those would've created an even bigger diff 😳


But also CompliantT_co not having a bound=... means that self.compliant.<anything> is unsafe:

CompliantT_co = TypeVar("CompliantT_co", covariant=True)

So I traded that out to reuse Implementation, which is still technically unsafe (#3132):

CompliantT_co = TypeVar("CompliantT_co", bound="_StoresImplementation", covariant=True)

... but the alternative is adding _backend_version as a requirement to CompliantColumn - which I don't want to do

Copy link
Member Author

Choose a reason for hiding this comment

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

Sooooooo, while I agree with what you've said in (#3127 (comment)) - I'm finding it tricky to justify what it took to end up with (6cc8ed0) πŸ€”

WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

Oh boy! I see the issue is #3131 not helping at all.

But if that's fixed, isn't _FullContext what we are looking for to bound CompliantT_co i.e. to implement both _implementation and _backend_version?

Also I would aim to decouple _unwrap_context into something like:

@@ -1936,23 +1936,19 @@ class requires:  # noqa: N801
     def _unparse_version(backend_version: tuple[int, ...], /) -> str:
         return ".".join(f"{d}" for d in backend_version)
 
-    def _unwrap_context(
-        self, instance: _IntoContext
-    ) -> tuple[Implementation, tuple[int, ...]]:
-        if is_namespace_accessor(instance):
+    def _qualify_wrapped_name(self, instance: _IntoContext) -> None:
+        if is_namespace_accessor(instance) and "." not in self._wrapped_name:
             # NOTE: Should only need to do this once per class (the first time the method is called)
-            if "." not in self._wrapped_name:
-                self._wrapped_name = f"{instance._accessor}.{self._wrapped_name}"
-            return instance.implementation, instance.backend_version
-        return instance._implementation, instance._backend_version
+            self._wrapped_name = f"{instance._accessor}.{self._wrapped_name}"
 
     def _ensure_version(self, instance: _IntoContext, /) -> None:
-        backend, version = self._unwrap_context(instance)
-        if version >= self._min_version:
+        self._qualify_wrapped_name(instance)
+        if (version := instance._backend_version) >= self._min_version:
             return
         method = self._wrapped_name
         minimum = self._unparse_version(self._min_version)
         found = self._unparse_version(version)
+        backend = instance._implementation

I have a commit almost ready, I will push that and see how it looks like

Copy link
Member Author

Choose a reason for hiding this comment

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

Nevermind, I guess this is another main blocker for you?!

.. but the alternative is adding _backend_version as a requirement to CompliantColumn - which I don't want to do

ah yeah, this issue is relevant since it traces-back to where I removed _backend_version πŸ˜…

But I guess the short version is _backend_version is not something we need an extender of narwhals to provide.
We won't be branching on it - so requiring it didn't make sense to me

Copy link
Member Author

@dangotbanned dangotbanned Sep 15, 2025

Choose a reason for hiding this comment

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

@FBruzzesi I think this is as close as I can get to (#3127 (comment))

(refactor: try a different way)

... while avoiding the bigger changes for now πŸ˜‰

I just wanted to help out with #3116

Copy link
Member

Choose a reason for hiding this comment

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

Hey @dangotbanned thanks for bearing with me, I wanted to get back to you earlier but I couldn't.

Another thought I had was something you also mentioned: namely all we need is _StoreImplementation since we can do:

impl = self._implementation
version = impl.backend_version()

I have mixed feelings at the moment for both solutions, but my understanding was that you eventually wanted to move towards this latter suggestion (I hope I didn't get that wrong).

Copy link
Member Author

Choose a reason for hiding this comment

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

thanks for bearing with me, I wanted to get back to you earlier but I couldn't.

No worries @FBruzzesi, I took longer to respond πŸ˜‰

Another thought I had was something you also mentioned: namely all we need is _StoreImplementation since we can do:

Yeah that was the direction I went in with 4a8764#diff
and is what I've been trying to move towards recently e.g:

narwhals/narwhals/_utils.py

Lines 2097 to 2098 in 7b271eb

class Compliant(
_StoresNative[NativeT_co], _StoresImplementation, Protocol[NativeT_co]

narwhals/narwhals/_utils.py

Lines 241 to 250 in 7b271eb

class ValidateBackendVersion(_StoresImplementation, Protocol):
"""Ensure the target `Implementation` is on a supported version."""
def _validate_backend_version(self) -> None:
"""Raise if installed version below `nw._utils.MIN_VERSIONS`.
**Only use this when moving between backends.**
Otherwise, the validation will have taken place already.
"""
_ = self._implementation._backend_version()

Both of the *Context protocols also depend on it

class _LimitedContext(_StoresImplementation, _StoresVersion, Protocol):

class _FullContext(_StoresImplementation, _StoresBackendVersion, Protocol):

Copy link
Member Author

@dangotbanned dangotbanned Sep 16, 2025

Choose a reason for hiding this comment

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

but my understanding was that you eventually wanted to move towards this latter suggestion (I hope I didn't get that wrong).

Absolutely right! πŸ˜„

To clarify (#3127 (comment)) some more ...

I want to tackle it holistically as part of this meta issue:

Currently, there are a few related issues and I don't wanna make a decision here for the benefit of only #3116

Show screenshot

image

"DateTimeNamespace",
"ListNamespace",
"NameNamespace",
"NamespaceAccessor",
Copy link
Member Author

Choose a reason for hiding this comment

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

Forgot to mention!

I chose to invert the naming because we've kinda overloaded the term Namespace

from narwhals._arrow.namespace import ArrowNamespace
from narwhals._compliant.any_namespace import (
    NameNamespace,
    #  AccessorNamespace,
    NamespaceAccessor,
    StringNamespace,
    StructNamespace,
)
from narwhals._compliant.expr import EagerExprCatNamespace
from narwhals._compliant.namespace import CompliantNamespace
from narwhals._namespace import Namespace

@FBruzzesi
Copy link
Member

@dangotbanned I think this is ready to merge - I opened a PR (#3136) to pin duckdb when we also run ibis, and get confidence from tests back.

@dangotbanned
Copy link
Member Author

@dangotbanned I think this is ready to merge - I opened a PR (#3136) to pin duckdb when we also run ibis, and get confidence from tests back.

Thanks @FBruzzesi!

Feel free to merge that and then this one whenever you're ready πŸ₯³

Copy link
Member

@FBruzzesi FBruzzesi left a comment

Choose a reason for hiding this comment

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

Thanks for bearing with me on this one @dangotbanned - I am also looking forward to getting rid of some of the tech debt here

@dangotbanned dangotbanned merged commit e3133de into main Sep 18, 2025
29 of 31 checks passed
@dangotbanned dangotbanned deleted the @requires-on-accessors branch September 18, 2025 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants