-
Notifications
You must be signed in to change notification settings - Fork 171
chore: Support @requires.backend_version in namespaces
#3127
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
Conversation
- 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.
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_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.
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._implementationi.e. by simply forwarding attributes as properties
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.
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
| 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 π
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.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
| CompliantT_co = TypeVar("CompliantT_co", covariant=True) |
So I traded that out to reuse Implementation, which is still technically unsafe (#3132):
Line 166 in 6cc8ed0
| 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
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.
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.
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._implementationI have a commit almost ready, I will push that and see how it looks like
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.
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.
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
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 @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.
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
_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
| class Compliant( | |
| _StoresNative[NativeT_co], _StoresImplementation, Protocol[NativeT_co] |
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() |
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.
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.
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`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.
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


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