-
Notifications
You must be signed in to change notification settings - Fork 165
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
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_context
responsibilities
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._implementation
i.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._implementation
I 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
_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:
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 π₯³ |
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