Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8a0b9e0
test: Add failing tests
dangotbanned Sep 12, 2025
f80a83c
feat: Everything but the method name working
dangotbanned Sep 12, 2025
fbbe1b1
refactor: Remove no-longer-required `_version`
dangotbanned Sep 12, 2025
3009d8e
feat: Add `_accessor` class var
dangotbanned Sep 12, 2025
c926ca8
refactor: Do the same thing as pandas_like
dangotbanned Sep 12, 2025
ccdd735
feat: prefix method name
dangotbanned Sep 12, 2025
4b79c04
Merge branch 'main' into @requires-on-accessors
dangotbanned Sep 13, 2025
4a87646
refactor: `NamespaceAccessor` typing/protocol changes
dangotbanned Sep 14, 2025
6cc8ed0
refactor: `@requires` runtime changes
dangotbanned Sep 14, 2025
6854043
revert "refactor: `@requires` runtime changes"
dangotbanned Sep 15, 2025
cfea56b
revert "refactor: `NamespaceAccessor` typing/protocol changes"
dangotbanned Sep 15, 2025
7b271eb
refactor: try a different way
dangotbanned Sep 15, 2025
456545f
Merge branch 'main' into @requires-on-accessors
dangotbanned Sep 16, 2025
00af1a4
use it for PolarsExpr.str.zfill
FBruzzesi Sep 17, 2025
88e9e9f
change check in is_namespace_accessor
FBruzzesi Sep 17, 2025
a52beb2
chore(typing): Narrow what can be passed to `is_namespace_accessor`
dangotbanned Sep 17, 2025
72a6e0f
refactor: Make super private
dangotbanned Sep 17, 2025
32a211b
chore: make the runtime check safer
dangotbanned Sep 17, 2025
02c4984
perf: Move optimization into `_hasattr_static`
dangotbanned Sep 17, 2025
7906b15
docs: Leave a trail for "why so strict dude?"
dangotbanned Sep 17, 2025
75da19e
Merge branch 'main' into @requires-on-accessors
FBruzzesi Sep 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion narwhals/_arrow/series_cat.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
import pyarrow as pa

from narwhals._arrow.utils import ArrowSeriesNamespace
from narwhals._compliant.any_namespace import CatNamespace

if TYPE_CHECKING:
from narwhals._arrow.series import ArrowSeries
from narwhals._arrow.typing import Incomplete


class ArrowSeriesCatNamespace(ArrowSeriesNamespace):
class ArrowSeriesCatNamespace(ArrowSeriesNamespace, CatNamespace["ArrowSeries"]):
def get_categories(self) -> ArrowSeries:
# NOTE: Should be `list[pa.DictionaryArray]`, but `DictionaryArray` has no attributes
chunks: Incomplete = self.native.chunks
Expand Down
5 changes: 4 additions & 1 deletion narwhals/_arrow/series_dt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pyarrow.compute as pc

from narwhals._arrow.utils import UNITS_DICT, ArrowSeriesNamespace, floordiv_compat, lit
from narwhals._compliant.any_namespace import DateTimeNamespace
from narwhals._constants import (
MS_PER_MINUTE,
MS_PER_SECOND,
Expand Down Expand Up @@ -36,7 +37,9 @@
IntoRhs: TypeAlias = int


class ArrowSeriesDateTimeNamespace(ArrowSeriesNamespace):
class ArrowSeriesDateTimeNamespace(
ArrowSeriesNamespace, DateTimeNamespace["ArrowSeries"]
):
_TIMESTAMP_DATE_FACTOR: ClassVar[Mapping[TimeUnit, int]] = {
"ns": NS_PER_SECOND,
"us": US_PER_SECOND,
Expand Down
10 changes: 5 additions & 5 deletions narwhals/_arrow/series_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@
import pyarrow.compute as pc

from narwhals._arrow.utils import ArrowSeriesNamespace
from narwhals._compliant.any_namespace import ListNamespace
from narwhals._utils import not_implemented

if TYPE_CHECKING:
from narwhals._arrow.series import ArrowSeries


class ArrowSeriesListNamespace(ArrowSeriesNamespace):
class ArrowSeriesListNamespace(ArrowSeriesNamespace, ListNamespace["ArrowSeries"]):
def len(self) -> ArrowSeries:
return self.with_native(pc.list_value_length(self.native).cast(pa.uint32()))

unique = not_implemented()

contains = not_implemented()

def get(self, index: int) -> ArrowSeries:
return self.with_native(pc.list_element(self.native, index))

unique = not_implemented()
contains = not_implemented()
3 changes: 2 additions & 1 deletion narwhals/_arrow/series_str.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
import pyarrow.compute as pc

from narwhals._arrow.utils import ArrowSeriesNamespace, lit, parse_datetime_format
from narwhals._compliant.any_namespace import StringNamespace

if TYPE_CHECKING:
from narwhals._arrow.series import ArrowSeries
from narwhals._arrow.typing import Incomplete


class ArrowSeriesStringNamespace(ArrowSeriesNamespace):
class ArrowSeriesStringNamespace(ArrowSeriesNamespace, StringNamespace["ArrowSeries"]):
def len_chars(self) -> ArrowSeries:
return self.with_native(pc.utf8_length(self.native))

Expand Down
3 changes: 2 additions & 1 deletion narwhals/_arrow/series_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
import pyarrow.compute as pc

from narwhals._arrow.utils import ArrowSeriesNamespace
from narwhals._compliant.any_namespace import StructNamespace

if TYPE_CHECKING:
from narwhals._arrow.series import ArrowSeries


class ArrowSeriesStructNamespace(ArrowSeriesNamespace):
class ArrowSeriesStructNamespace(ArrowSeriesNamespace, StructNamespace["ArrowSeries"]):
def field(self, name: str) -> ArrowSeries:
return self.with_native(pc.struct_field(self.native, name)).alias(name)
24 changes: 20 additions & 4 deletions narwhals/_compliant/any_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,40 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol
from typing import TYPE_CHECKING, ClassVar, Protocol

from narwhals._utils import CompliantT_co, _StoresCompliant

if TYPE_CHECKING:
from typing import Callable

from narwhals._compliant.typing import Accessor
from narwhals.typing import NonNestedLiteral, TimeUnit

__all__ = [
"CatNamespace",
"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

"StringNamespace",
"StructNamespace",
]


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


class CatNamespace(NamespaceAccessor[CompliantT_co], Protocol[CompliantT_co]):
_accessor: ClassVar[Accessor] = "cat"

def get_categories(self) -> CompliantT_co: ...


class DateTimeNamespace(_StoresCompliant[CompliantT_co], Protocol[CompliantT_co]):
_accessor: ClassVar[Accessor] = "dt"

def to_string(self, format: str) -> CompliantT_co: ...
def replace_time_zone(self, time_zone: str | None) -> CompliantT_co: ...
def convert_time_zone(self, time_zone: str) -> CompliantT_co: ...
Expand All @@ -52,15 +62,17 @@ def offset_by(self, by: str) -> CompliantT_co: ...


class ListNamespace(_StoresCompliant[CompliantT_co], Protocol[CompliantT_co]):
def get(self, index: int) -> CompliantT_co: ...
_accessor: ClassVar[Accessor] = "list"

def get(self, index: int) -> CompliantT_co: ...
def len(self) -> CompliantT_co: ...

def unique(self) -> CompliantT_co: ...
def contains(self, item: NonNestedLiteral) -> CompliantT_co: ...


class NameNamespace(_StoresCompliant[CompliantT_co], Protocol[CompliantT_co]):
_accessor: ClassVar[Accessor] = "name"

def keep(self) -> CompliantT_co: ...
def map(self, function: Callable[[str], str]) -> CompliantT_co: ...
def prefix(self, prefix: str) -> CompliantT_co: ...
Expand All @@ -70,6 +82,8 @@ def to_uppercase(self) -> CompliantT_co: ...


class StringNamespace(_StoresCompliant[CompliantT_co], Protocol[CompliantT_co]):
_accessor: ClassVar[Accessor] = "str"

def len_chars(self) -> CompliantT_co: ...
def replace(
self, pattern: str, value: str, *, literal: bool, n: int
Expand All @@ -91,4 +105,6 @@ def zfill(self, width: int) -> CompliantT_co: ...


class StructNamespace(_StoresCompliant[CompliantT_co], Protocol[CompliantT_co]):
_accessor: ClassVar[Accessor] = "struct"

def field(self, name: str) -> CompliantT_co: ...
5 changes: 5 additions & 0 deletions narwhals/_compliant/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,8 @@ class ScalarKwargs(TypedDict, total=False):
- https://github.com/narwhals-dev/narwhals/issues/2526
- https://github.com/narwhals-dev/narwhals/issues/2660
"""

Accessor: TypeAlias = Literal[
"arr", "cat", "dt", "list", "meta", "name", "str", "bin", "struct"
]
"""`{Expr,Series}` method namespace accessor name."""
13 changes: 4 additions & 9 deletions narwhals/_polars/expr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Callable, Literal
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal

import polars as pl

Expand All @@ -22,6 +22,7 @@

from typing_extensions import Self

from narwhals._compliant.typing import Accessor
from narwhals._expression_parsing import ExprKind, ExprMetadata
from narwhals._polars.dataframe import Method
from narwhals._polars.namespace import PolarsNamespace
Expand Down Expand Up @@ -400,17 +401,11 @@ class PolarsExprDateTimeNamespace(
class PolarsExprStringNamespace(
PolarsExprNamespace, PolarsStringNamespace[PolarsExpr, pl.Expr]
):
@requires.backend_version((0, 20, 5))
def zfill(self, width: int) -> PolarsExpr:
backend_version = self.compliant._backend_version
native_result = self.native.str.zfill(width)

if backend_version < (0, 20, 5): # pragma: no cover
# Reason:
# `TypeError: argument 'length': 'Expr' object cannot be interpreted as an integer`
# in `native_expr.str.slice(1, length)`
msg = "`zfill` is only available in 'polars>=0.20.5', found version '0.20.4'."
raise NotImplementedError(msg)

if backend_version <= (1, 30, 0):
length = self.native.str.len_chars()
less_than_width = length < width
Expand All @@ -435,7 +430,7 @@ class PolarsExprCatNamespace(


class PolarsExprNameNamespace(PolarsExprNamespace):
_accessor = "name"
_accessor: ClassVar[Accessor] = "name"
keep: Method[PolarsExpr]
map: Method[PolarsExpr]
prefix: Method[PolarsExpr]
Expand Down
19 changes: 2 additions & 17 deletions narwhals/_polars/typing.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,10 @@
from __future__ import annotations # pragma: no cover

from typing import (
TYPE_CHECKING, # pragma: no cover
Union, # pragma: no cover
)
from typing import TYPE_CHECKING # pragma: no cover

if TYPE_CHECKING:
import sys
from typing import Literal, TypeVar

if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
from typing import TypeVar

from narwhals._polars.dataframe import PolarsDataFrame, PolarsLazyFrame
from narwhals._polars.expr import PolarsExpr
from narwhals._polars.series import PolarsSeries

IntoPolarsExpr: TypeAlias = Union[PolarsExpr, PolarsSeries]
FrameT = TypeVar("FrameT", PolarsDataFrame, PolarsLazyFrame)
NativeAccessor: TypeAlias = Literal[
"arr", "cat", "dt", "list", "meta", "name", "str", "bin", "struct"
]
14 changes: 7 additions & 7 deletions narwhals/_polars/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@

from typing_extensions import TypeIs

from narwhals._compliant.typing import Accessor
from narwhals._polars.dataframe import Method
from narwhals._polars.expr import PolarsExpr
from narwhals._polars.series import PolarsSeries
from narwhals._polars.typing import NativeAccessor
from narwhals.dtypes import DType
from narwhals.typing import IntoDType

Expand Down Expand Up @@ -261,7 +261,7 @@ class PolarsAnyNamespace(
_StoresNative[NativeT_co],
Protocol[CompliantT_co, NativeT_co],
):
_accessor: ClassVar[NativeAccessor]
_accessor: ClassVar[Accessor]

def __getattr__(self, attr: str) -> Callable[..., CompliantT_co]:
def func(*args: Any, **kwargs: Any) -> CompliantT_co:
Expand All @@ -273,7 +273,7 @@ def func(*args: Any, **kwargs: Any) -> CompliantT_co:


class PolarsDateTimeNamespace(PolarsAnyNamespace[CompliantT, NativeT_co]):
_accessor: ClassVar[NativeAccessor] = "dt"
_accessor: ClassVar[Accessor] = "dt"

def truncate(self, every: str) -> CompliantT:
# Ensure consistent error message is raised.
Expand Down Expand Up @@ -309,7 +309,7 @@ def offset_by(self, by: str) -> CompliantT:


class PolarsStringNamespace(PolarsAnyNamespace[CompliantT, NativeT_co]):
_accessor: ClassVar[NativeAccessor] = "str"
_accessor: ClassVar[Accessor] = "str"

# NOTE: Use `abstractmethod` if we have defs to implement, but also `Method` usage
@abc.abstractmethod
Expand All @@ -331,12 +331,12 @@ def zfill(self, width: int) -> CompliantT: ...


class PolarsCatNamespace(PolarsAnyNamespace[CompliantT, NativeT_co]):
_accessor: ClassVar[NativeAccessor] = "cat"
_accessor: ClassVar[Accessor] = "cat"
get_categories: Method[CompliantT]


class PolarsListNamespace(PolarsAnyNamespace[CompliantT, NativeT_co]):
_accessor: ClassVar[NativeAccessor] = "list"
_accessor: ClassVar[Accessor] = "list"

@abc.abstractmethod
def len(self) -> CompliantT: ...
Expand All @@ -347,5 +347,5 @@ def len(self) -> CompliantT: ...


class PolarsStructNamespace(PolarsAnyNamespace[CompliantT, NativeT_co]):
_accessor: ClassVar[NativeAccessor] = "struct"
_accessor: ClassVar[Accessor] = "struct"
field: Method[CompliantT]
Loading
Loading