chore: Support @requires.backend_version in namespaces#3127
chore: Support @requires.backend_version in namespaces#3127dangotbanned merged 21 commits intomainfrom
@requires.backend_version in namespaces#3127Conversation
- Towards #3116 (comment) - Typing is easy to fix
want `some.nope`, but got `nope` for now
Yoinks from polars util
shorter in most cases
FBruzzesi
left a comment
There was a problem hiding this comment.
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_contextresponsibilities
narwhals/_utils.py
Outdated
| 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 |
There was a problem hiding this comment.
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._implementationi.e. by simply forwarding attributes as properties
There was a problem hiding this comment.
ooohh nice idea! 😍
It's pretty funny how I managed to do almost the exact same thing a while back
narwhals/narwhals/_compliant/series.py
Lines 269 to 286 in 1471cbb
but seemed to fumble on this one 😂
There was a problem hiding this comment.
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:
Line 165 in 1471cbb
So I traded that out to reuse Implementation, which is still technically unsafe (#3132):
Line 166 in 6cc8ed0
... but the alternative is adding _backend_version as a requirement to CompliantColumn - which I don't want to do
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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._implementationI have a commit almost ready, I will push that and see how it looks like
There was a problem hiding this comment.
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
There was a problem hiding this comment.
@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
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
_StoreImplementationsince 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:
Lines 2097 to 2098 in 7b271eb
Lines 241 to 250 in 7b271eb
There was a problem hiding this comment.
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
Everything **but** the runtime change to `requires._unwrap_context` #3127 (comment)
| "DateTimeNamespace", | ||
| "ListNamespace", | ||
| "NameNamespace", | ||
| "NamespaceAccessor", |
There was a problem hiding this comment.
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`Any` was the wrong choice (explanation to follow!)
Inlined `_hasattr_static` to avoid creating lots of sentinels
|
@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 🥳 |
FBruzzesi
left a comment
There was a problem hiding this comment.
Thanks for bearing with me on this one @dangotbanned - I am also looking forward to getting rid of some of the tech debt here


What type of PR is this? (check all applicable)
Related issues
{Expr,Series}.str.to_titlecase#3116{Expr,Series}.str.to_titlecase#3116 (comment){Expr,Series}.str.to_titlecase#3116 (comment)Checklist
If you have comments or can explain your changes, please do so below