Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0d18f96
Update
osoucy Mar 3, 2025
496cbd0
Update
osoucy Mar 4, 2025
9592277
Update
osoucy Mar 4, 2025
aaebbbe
Update
osoucy Mar 4, 2025
273f331
Update
osoucy Mar 4, 2025
2d95d3d
Update
osoucy Mar 4, 2025
decba02
Update
osoucy Mar 4, 2025
f89f7d3
Merge branch 'main' into feat/struct_namespace
osoucy Mar 4, 2025
e75da43
Addressed PR comment
osoucy Mar 5, 2025
ef8b863
Update docs/api-completeness/expr_struct.md
osoucy Mar 5, 2025
fa802a4
Merge remote-tracking branch 'origin/feat/struct_namespace' into feat…
osoucy Mar 5, 2025
5e04a46
Addressed PR comment
osoucy Mar 5, 2025
47345f5
Addressed PR comment
osoucy Mar 5, 2025
9517b91
Merge branch 'main' into feat/struct_namespace
osoucy Mar 5, 2025
e8623e2
Addressed PR comment
osoucy Mar 5, 2025
2d4ce6b
Addressed PR comment
osoucy Mar 5, 2025
825d29e
Addressed PR comment
osoucy Mar 5, 2025
6f867ef
Addressed PR comment
osoucy Mar 5, 2025
e54663c
Addressed PR comment
osoucy Mar 5, 2025
8fe86ae
Addressed PR comment
osoucy Mar 5, 2025
37dc7fc
Addressed PR comment
osoucy Mar 5, 2025
104d40d
Addressed PR comment
osoucy Mar 5, 2025
9ead00a
Merge branch 'main' into feat/struct_namespace
osoucy Mar 5, 2025
9e2a24d
Merge branch 'main' into feat/struct_namespace
osoucy Mar 5, 2025
20ec4eb
Merge branch 'main' into feat/struct_namespace
osoucy Mar 5, 2025
ecfd180
Merge branch 'main' into feat/struct_namespace
osoucy Mar 5, 2025
6c5597b
PR Comments
osoucy Mar 6, 2025
99a78e3
Merge branch 'main' into feat/struct_namespace
osoucy Mar 6, 2025
105d3f9
Update docs/api-completeness/series_struct.md
osoucy Mar 6, 2025
d21bdc2
PR Comments
osoucy Mar 6, 2025
9db179a
Merge branch 'main' into feat/struct_namespace
osoucy Mar 6, 2025
865bedd
Merge branch 'main' into feat/struct_namespace
osoucy Mar 7, 2025
3ad7ae9
PR Comments
osoucy Mar 8, 2025
7e5d813
PR Comments
osoucy Mar 8, 2025
c5cf993
PR Comments
osoucy Mar 8, 2025
4d36295
PR Comments
osoucy Mar 8, 2025
8701e43
Merge branch 'main' into feat/struct_namespace
osoucy Mar 8, 2025
4ed080c
PR Comments
osoucy Mar 9, 2025
195088c
PR Comments
osoucy Mar 9, 2025
71df89d
Merge branch 'main' into feat/struct_namespace
osoucy Mar 9, 2025
ebe4d6b
remove unnecessary aliases
MarcoGorelli Mar 9, 2025
59fb567
remove unnecessary aliases
MarcoGorelli Mar 9, 2025
736599e
extra test
MarcoGorelli Mar 9, 2025
f71e677
remove outdated
MarcoGorelli Mar 9, 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
5 changes: 5 additions & 0 deletions docs/api-completeness/expr_struct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Expr.str
Comment thread
osoucy marked this conversation as resolved.
Outdated

| Method | arrow | duckdb | pandas-like | polars | spark-like |
|--------------|--------------------|--------------------|--------------------|--------------------|--------------------|
| field | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Comment thread
osoucy marked this conversation as resolved.
Outdated
5 changes: 5 additions & 0 deletions docs/api-completeness/series_struct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Series.str
Comment thread
osoucy marked this conversation as resolved.
Outdated

| Method | arrow | pandas-like | polars |
|--------------|---------------------|--------------------|--------------------|
| field | :white_check_mark: | :white_check_mark: | :white_check_mark: |
9 changes: 9 additions & 0 deletions docs/api-reference/expr_struct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# `narwhals.Expr.struct`

::: narwhals.expr.ExprStructNamespace
handler: python
options:
members:
- field
show_source: false
show_bases: false
2 changes: 2 additions & 0 deletions docs/api-reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [narwhals.Expr.list](expr_list.md)
- [narwhals.Expr.name](expr_name.md)
- [narwhals.Expr.str](expr_str.md)
- [narwhals.Expr.struct](expr_struct.md)
- [narwhals.GroupBy](group_by.md)
- [narwhals.LazyGroupBy](lazy_group_by.md)
- [narwhals.LazyFrame](lazyframe.md)
Expand All @@ -17,6 +18,7 @@
- [narwhals.Series.dt](series_dt.md)
- [narwhals.Series.list](series_list.md)
- [narwhals.Series.str](series_str.md)
- [narwhals.Series.struct](series_struct.md)
- [narwhals.dependencies](dependencies.md)
- [narwhals.Implementation](implementation.md)
- [narwhals.dtypes](dtypes.md)
Expand Down
9 changes: 9 additions & 0 deletions docs/api-reference/series_struct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# `narwhals.Series.struct`

::: narwhals.series.SeriesStructNamespace
handler: python
options:
members:
- field
show_source: false
show_bases: false
4 changes: 4 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ nav:
- Supported Expr.list methods: api-completeness/expr_list.md
- Supported Expr.name methods: api-completeness/expr_name.md
- Supported Expr.str methods: api-completeness/expr_str.md
- Supported Expr.struct methods: api-completeness/expr_struct.md
- Supported Series methods: api-completeness/series.md
- Supported Series.cat methods: api-completeness/series_cat.md
- Supported Series.dt methods: api-completeness/series_dt.md
- Supported Series.list methods: api-completeness/series_list.md
- Supported Series.str methods: api-completeness/series_str.md
- Supported Series.struct methods: api-completeness/series_struct.md
- API Reference:
- api-reference/index.md
- api-reference/narwhals.md
Expand All @@ -49,6 +51,7 @@ nav:
- api-reference/expr_list.md
- api-reference/expr_name.md
- api-reference/expr_str.md
- api-reference/expr_struct.md
- api-reference/group_by.md
- api-reference/lazy_group_by.md
- api-reference/lazyframe.md
Expand All @@ -58,6 +61,7 @@ nav:
- api-reference/series_dt.md
- api-reference/series_list.md
- api-reference/series_str.md
- api-reference/series_struct.md
- api-reference/dependencies.md
- api-reference/implementation.md
- api-reference/dtypes.md
Expand Down
5 changes: 5 additions & 0 deletions narwhals/_arrow/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from narwhals._arrow.expr_list import ArrowExprListNamespace
from narwhals._arrow.expr_name import ArrowExprNameNamespace
from narwhals._arrow.expr_str import ArrowExprStringNamespace
from narwhals._arrow.expr_struct import ArrowExprStructNamespace
from narwhals._arrow.series import ArrowSeries
from narwhals._expression_parsing import ExprKind
from narwhals._expression_parsing import evaluate_output_names_and_aliases
Expand Down Expand Up @@ -598,3 +599,7 @@ def name(self: Self) -> ArrowExprNameNamespace:
@property
def list(self: Self) -> ArrowExprListNamespace:
return ArrowExprListNamespace(self)

@property
def struct(self: Self) -> ArrowExprStructNamespace:
return ArrowExprStructNamespace(self)
21 changes: 21 additions & 0 deletions narwhals/_arrow/expr_struct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from narwhals._expression_parsing import reuse_series_namespace_implementation

if TYPE_CHECKING:
from typing_extensions import Self

from narwhals._arrow.expr import ArrowExpr


class ArrowExprStructNamespace:
def __init__(self: Self, expr: ArrowExpr) -> None:
self._compliant_expr = expr

def field(self: Self, name: str) -> ArrowExpr:
self._compliant_expr._evaluate_output_names = lambda _col: [name]
return reuse_series_namespace_implementation(
self._compliant_expr, "struct", "field", name=name
)

@dangotbanned dangotbanned Mar 5, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like mutating the current expression might cause issues elsewhere.

@osoucy was there a particular reason you chose this?
I can't seem to find any other cases like this (outside of the new expr_struct code)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I had to use it becase "facilitator" methods such as reuse_series_namespace_implementation or _compliant_expr._from_call does not allow to control the output expression / series name. I could not find other examples where we explicitly had to rename the output. Not having this resulted in raising errors because of a mismatch between the expected expresssion name and the received expression name (or something similar).

The other option would be to not use facilitator methods and simply intantiate an expression / series class from scratch, but it would mean some code duplication.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the detail @osoucy that was helpful

I think that the pattern used here might be something to work backwards from
https://github.com/narwhals-dev/narwhals/blob/b986bfd5768759c2125c12ffdcd34911caf9e34c/narwhals/_pandas_like/expr_name.py

It seems quite a bit more complex than what you might need, but hopefully you can see how it avoids mutating the current Expr

def _from_colname_func_and_alias_output_names(
self: Self,
name_mapping_func: Callable[[str], str],
alias_output_names: Callable[[Sequence[str]], Sequence[str]] | None,
) -> PandasLikeExpr:
return self._compliant_expr.__class__(
call=lambda df: [
series.alias(name_mapping_func(name))
for series, name in zip(
self._compliant_expr._call(df),
self._compliant_expr._evaluate_output_names(df),
)
],
depth=self._compliant_expr._depth,
function_name=self._compliant_expr._function_name,
evaluate_output_names=self._compliant_expr._evaluate_output_names,
alias_output_names=alias_output_names,
backend_version=self._compliant_expr._backend_version,
implementation=self._compliant_expr._implementation,
version=self._compliant_expr._version,
call_kwargs=self._compliant_expr._call_kwargs,
)

@FBruzzesi per #1876

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Instead of creating another helper function (that might not get re-used), I modified the _from_call and reuse_series_namespace_implementation functions to allow to overwrite evaluate_output_names. I hope that works for you!

5 changes: 5 additions & 0 deletions narwhals/_arrow/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from narwhals._arrow.series_dt import ArrowSeriesDateTimeNamespace
from narwhals._arrow.series_list import ArrowSeriesListNamespace
from narwhals._arrow.series_str import ArrowSeriesStringNamespace
from narwhals._arrow.series_struct import ArrowSeriesStructNamespace
from narwhals._arrow.utils import cast_for_truediv
from narwhals._arrow.utils import chunked_array
from narwhals._arrow.utils import extract_native
Expand Down Expand Up @@ -1232,3 +1233,7 @@ def str(self: Self) -> ArrowSeriesStringNamespace:
@property
def list(self: Self) -> ArrowSeriesListNamespace:
return ArrowSeriesListNamespace(self)

@property
def struct(self: Self) -> ArrowSeriesStructNamespace:
return ArrowSeriesStructNamespace(self)
21 changes: 21 additions & 0 deletions narwhals/_arrow/series_struct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import pyarrow.compute as pc

if TYPE_CHECKING:
from typing_extensions import Self

from narwhals._arrow.series import ArrowSeries


class ArrowSeriesStructNamespace:
def __init__(self: Self, series: ArrowSeries) -> None:
self._compliant_series: ArrowSeries = series

def field(self: Self, name: str) -> ArrowSeries:
self._compliant_series._name = name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you can't mutate self._compliant_series, you'll need alias here too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For the Arrow* stuff I'd suggest using .compliant and .native

They weren't available when you started the PR @osoucy

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Some more context #2130 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is fine as a follow-up

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

At least the current implementation

    def field(self: Self, name: str) -> ArrowSeries:
        return self._compliant_series._from_native_series(
            pc.struct_field(self._compliant_series.alias(name)._native_series, name),
        )

avoids mutating the compliant series. Maybe I can look into @dangotbanned in a future PR?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All good @osoucy 😊

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So we are good to merge? :)

return self._compliant_series._from_native_series(
pc.struct_field(self._compliant_series._native_series, name)
)
2 changes: 1 addition & 1 deletion narwhals/_duckdb/dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ def join(
if self._backend_version < (1, 1, 4):
msg = f"DuckDB>=1.1.4 is required for cross-join, found version: {self._backend_version}"
raise NotImplementedError(msg)
rel = self._native_frame.set_alias("lhs").cross( # pragma: no cover
rel = self._native_frame.set_alias("lhs").cross( # type: ignore[operator] # pragma: no cover
other._native_frame.set_alias("rhs")
)
else:
Expand Down
5 changes: 5 additions & 0 deletions narwhals/_duckdb/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from narwhals._duckdb.expr_list import DuckDBExprListNamespace
from narwhals._duckdb.expr_name import DuckDBExprNameNamespace
from narwhals._duckdb.expr_str import DuckDBExprStringNamespace
from narwhals._duckdb.expr_struct import DuckDBExprStructNamespace
from narwhals._duckdb.utils import lit
from narwhals._duckdb.utils import maybe_evaluate_expr
from narwhals._duckdb.utils import narwhals_to_native_dtype
Expand Down Expand Up @@ -482,3 +483,7 @@ def name(self: Self) -> DuckDBExprNameNamespace:
@property
def list(self: Self) -> DuckDBExprListNamespace:
return DuckDBExprListNamespace(self)

@property
def struct(self: Self) -> DuckDBExprStructNamespace:
return DuckDBExprStructNamespace(self)
26 changes: 26 additions & 0 deletions narwhals/_duckdb/expr_struct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from duckdb import FunctionExpression

from narwhals._duckdb.utils import lit

if TYPE_CHECKING:
from typing_extensions import Self

from narwhals._duckdb.expr import DuckDBExpr


class DuckDBExprStructNamespace:
def __init__(self: Self, expr: DuckDBExpr) -> None:
self._compliant_expr = expr

def field(self: Self, name: str) -> DuckDBExpr:
self._compliant_expr._evaluate_output_names = lambda _col: [name]
return self._compliant_expr._from_call(
lambda _input: FunctionExpression("struct_extract", _input, lit(name)).alias(
name
),
"field",
)
5 changes: 5 additions & 0 deletions narwhals/_pandas_like/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from narwhals._pandas_like.expr_list import PandasLikeExprListNamespace
from narwhals._pandas_like.expr_name import PandasLikeExprNameNamespace
from narwhals._pandas_like.expr_str import PandasLikeExprStringNamespace
from narwhals._pandas_like.expr_struct import PandasLikeExprStructNamespace
from narwhals._pandas_like.group_by import AGGREGATIONS_TO_PANDAS_EQUIVALENT
from narwhals._pandas_like.series import PandasLikeSeries
from narwhals._pandas_like.utils import rename
Expand Down Expand Up @@ -710,3 +711,7 @@ def name(self: Self) -> PandasLikeExprNameNamespace:
@property
def list(self: Self) -> PandasLikeExprListNamespace:
return PandasLikeExprListNamespace(self)

@property
def struct(self: Self) -> PandasLikeExprStructNamespace:
return PandasLikeExprStructNamespace(self)
21 changes: 21 additions & 0 deletions narwhals/_pandas_like/expr_struct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from narwhals._expression_parsing import reuse_series_namespace_implementation

if TYPE_CHECKING:
from typing_extensions import Self

from narwhals._pandas_like.expr import PandasLikeExpr


class PandasLikeExprStructNamespace:
def __init__(self: Self, expr: PandasLikeExpr) -> None:
self._compliant_expr = expr

def field(self, name: str) -> PandasLikeExpr:
self._compliant_expr._evaluate_output_names = lambda _col: [name]
return reuse_series_namespace_implementation(
self._compliant_expr, "struct", "field", name=name
)
5 changes: 5 additions & 0 deletions narwhals/_pandas_like/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from narwhals._pandas_like.series_dt import PandasLikeSeriesDateTimeNamespace
from narwhals._pandas_like.series_list import PandasLikeSeriesListNamespace
from narwhals._pandas_like.series_str import PandasLikeSeriesStringNamespace
from narwhals._pandas_like.series_struct import PandasLikeSeriesStructNamespace
from narwhals._pandas_like.utils import align_and_extract_native
from narwhals._pandas_like.utils import get_dtype_backend
from narwhals._pandas_like.utils import narwhals_to_native_dtype
Expand Down Expand Up @@ -1106,3 +1107,7 @@ def cat(self: Self) -> PandasLikeSeriesCatNamespace:
@property
def list(self: Self) -> PandasLikeSeriesListNamespace:
return PandasLikeSeriesListNamespace(self)

@property
def struct(self: Self) -> PandasLikeSeriesStructNamespace:
return PandasLikeSeriesStructNamespace(self)
18 changes: 18 additions & 0 deletions narwhals/_pandas_like/series_struct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing_extensions import Self

from narwhals._pandas_like.series import PandasLikeSeries


class PandasLikeSeriesStructNamespace:
def __init__(self: Self, series: PandasLikeSeries) -> None:
self._compliant_series = series

def field(self: Self, name: str) -> PandasLikeSeries:
return self._compliant_series._from_native_series(
self._compliant_series._native_series.apply(lambda x: x[name]).rename(name),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of using apply can we use https://pandas.pydata.org/pandas-docs/dev/reference/api/pandas.Series.struct.field.html and only support this for pyarrow-backed dtypes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I feel the majority of Pandas users don't leverage pyarrow-backed dtypes. I added a switch to check and use the struct namespace if available and fallbacks on the apply function if it's not the case.

)
15 changes: 15 additions & 0 deletions narwhals/_polars/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ def name(self: Self) -> PolarsExprNameNamespace:
def list(self: Self) -> PolarsExprListNamespace:
return PolarsExprListNamespace(self)

@property
def struct(self: Self) -> PolarsExprStructNamespace:
return PolarsExprStructNamespace(self)


class PolarsExprDateTimeNamespace:
def __init__(self: Self, expr: PolarsExpr) -> None:
Expand Down Expand Up @@ -391,3 +395,14 @@ def func(*args: Any, **kwargs: Any) -> PolarsExpr:
)

return func


class PolarsExprStructNamespace:
def __init__(self: Self, expr: PolarsExpr) -> None:
self._expr = expr

def field(self: Self, name: str) -> PolarsExpr:
native_expr = self._expr._native_expr
native_result = native_expr.struct.field(name)

return self._expr._from_native_expr(native_result)
Comment thread
osoucy marked this conversation as resolved.
Outdated
11 changes: 11 additions & 0 deletions narwhals/_polars/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,3 +647,14 @@ def func(*args: Any, **kwargs: Any) -> Any:
)

return func


class PolarsSeriesStructNamespace:
def __init__(self: Self, series: PolarsSeries) -> None:
self._series = series

def field(self: Self, *names: str) -> PolarsSeries:
native_series = self._series._native_series
native_result = native_series.struct.field(*names)

return self._series._from_native_series(native_result)
5 changes: 5 additions & 0 deletions narwhals/_spark_like/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from narwhals._spark_like.expr_list import SparkLikeExprListNamespace
from narwhals._spark_like.expr_name import SparkLikeExprNameNamespace
from narwhals._spark_like.expr_str import SparkLikeExprStringNamespace
from narwhals._spark_like.expr_struct import SparkLikeExprStructNamespace
from narwhals._spark_like.utils import maybe_evaluate_expr
from narwhals._spark_like.utils import narwhals_to_native_dtype
from narwhals.dependencies import get_pyspark
Expand Down Expand Up @@ -526,3 +527,7 @@ def dt(self: Self) -> SparkLikeExprDateTimeNamespace:
@property
def list(self: Self) -> SparkLikeExprListNamespace:
return SparkLikeExprListNamespace(self)

@property
def struct(self: Self) -> SparkLikeExprStructNamespace:
return SparkLikeExprStructNamespace(self)
17 changes: 17 additions & 0 deletions narwhals/_spark_like/expr_struct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing_extensions import Self

from narwhals._spark_like.expr import SparkLikeExpr


class SparkLikeExprStructNamespace:
def __init__(self: Self, expr: SparkLikeExpr) -> None:
self._compliant_expr = expr

def field(self: Self, name: str) -> SparkLikeExpr:
self._compliant_expr._evaluate_output_names = lambda _col: [name]
return self._compliant_expr._from_call(lambda col: col.getField(name), "field")
5 changes: 5 additions & 0 deletions narwhals/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from narwhals.expr_list import ExprListNamespace
from narwhals.expr_name import ExprNameNamespace
from narwhals.expr_str import ExprStringNamespace
from narwhals.expr_struct import ExprStructNamespace
from narwhals.translate import to_native
from narwhals.utils import _validate_rolling_arguments
from narwhals.utils import flatten
Expand Down Expand Up @@ -2467,6 +2468,10 @@ def name(self: Self) -> ExprNameNamespace[Self]:
def list(self: Self) -> ExprListNamespace[Self]:
return ExprListNamespace(self)

@property
def struct(self: Self) -> ExprStructNamespace[Self]:
return ExprStructNamespace(self)


__all__ = [
"Expr",
Expand Down
Loading