Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
31 changes: 30 additions & 1 deletion narwhals/_compliant/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
NativeFrameT_co,
NativeSeriesT,
)
from narwhals._expression_parsing import is_expr, is_series
from narwhals._utils import (
exclude_column_names,
get_column_names,
passthrough_column_names,
)
from narwhals.dependencies import is_numpy_array_2d
from narwhals.dependencies import is_numpy_array, is_numpy_array_2d

if TYPE_CHECKING:
from collections.abc import Container, Iterable, Sequence
Expand All @@ -31,12 +32,15 @@
from narwhals._compliant.selectors import CompliantSelectorNamespace
from narwhals._compliant.when_then import CompliantWhen, EagerWhen
from narwhals._utils import Implementation, Version
from narwhals.expr import Expr
from narwhals.series import Series
from narwhals.typing import (
ConcatMethod,
Into1DArray,
IntoDType,
IntoSchema,
NonNestedLiteral,
_1DArray,
_2DArray,
)

Expand All @@ -57,6 +61,16 @@ class CompliantNamespace(Protocol[CompliantFrameT, CompliantExprT]):

@property
def _expr(self) -> type[CompliantExprT]: ...
def parse_into_expr(
self, data: Expr | NonNestedLiteral | Any, /, *, str_as_lit: bool
) -> CompliantExprT | NonNestedLiteral:
if is_expr(data):
expr = data._to_compliant_expr(self)
assert isinstance(expr, self._expr) # noqa: S101
return expr
if isinstance(data, str) and not str_as_lit:
return self.col(data)
return data

# NOTE: `polars`
def all(self) -> CompliantExprT:
Expand Down Expand Up @@ -171,6 +185,21 @@ def from_native(
msg = f"Unsupported type: {type(data).__name__!r}"
raise TypeError(msg)

def parse_into_expr(
self,
data: Expr | Series[NativeSeriesT] | _1DArray | NonNestedLiteral,
/,
*,
str_as_lit: bool,
) -> EagerExprT | NonNestedLiteral:
if not (is_series(data) or is_numpy_array(data)):
return super().parse_into_expr(data, str_as_lit=str_as_lit)
return self._expr._from_series(
data._compliant_series
if is_series(data)
else self._series.from_numpy(data, context=self)
)

@overload
def from_numpy(self, data: Into1DArray, /, schema: None = ...) -> EagerSeriesT: ...

Expand Down
5 changes: 0 additions & 5 deletions narwhals/_compliant/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
from typing_extensions import NotRequired, Self, TypedDict

from narwhals._compliant.dataframe import CompliantDataFrame
from narwhals._compliant.expr import CompliantExpr, EagerExpr
from narwhals._compliant.namespace import EagerNamespace
from narwhals._utils import Implementation, Version, _LimitedContext
from narwhals.dtypes import DType
Expand Down Expand Up @@ -94,7 +93,6 @@ def from_native(cls, data: NativeSeriesT, /, *, context: _LimitedContext) -> Sel
def to_narwhals(self) -> Series[NativeSeriesT]:
return self._version.series(self, level="full")

def _to_expr(self) -> CompliantExpr[Any, Self]: ...
def _with_native(self, series: Any) -> Self: ...
def _with_version(self, version: Version) -> Self: ...

Expand Down Expand Up @@ -245,9 +243,6 @@ def __narwhals_namespace__(
self,
) -> EagerNamespace[Any, Self, Any, Any, NativeSeriesT]: ...

def _to_expr(self) -> EagerExpr[Any, Any]:
return self.__narwhals_namespace__()._expr._from_series(self) # type: ignore[no-any-return]

def _gather(self, rows: SizedMultiIndexSelector[NativeSeriesT]) -> Self: ...
def _gather_slice(self, rows: _SliceIndex | range) -> Self: ...
def __getitem__(self, item: MultiIndexSelector[Self]) -> Self:
Expand Down
36 changes: 11 additions & 25 deletions narwhals/_expression_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

from enum import Enum, auto
from itertools import chain
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, cast
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar

from narwhals._utils import is_compliant_expr, zip_strict
from narwhals.dependencies import is_narwhals_series, is_numpy_array
from narwhals.dependencies import is_narwhals_series, is_numpy_array, is_numpy_array_1d
from narwhals.exceptions import InvalidOperationError, MultiOutputExpressionError

if TYPE_CHECKING:
Expand All @@ -23,7 +23,6 @@
CompliantExprAny,
CompliantFrameAny,
CompliantNamespaceAny,
EagerNamespaceAny,
EvalNames,
)
from narwhals.expr import Expr
Expand All @@ -47,6 +46,13 @@ def is_series(obj: Any) -> TypeIs[Series[Any]]:
return isinstance(obj, Series)


def is_into_expr_eager(obj: Any) -> TypeIs[Expr | Series[Any] | str | _1DArray]:
from narwhals.expr import Expr
from narwhals.series import Series

return isinstance(obj, (Series, Expr, str)) or is_numpy_array_1d(obj)


def combine_evaluate_output_names(
*exprs: CompliantExpr[CompliantFrameT, Any],
) -> EvalNames[CompliantFrameT]:
Expand Down Expand Up @@ -74,24 +80,6 @@ def alias_output_names(names: Sequence[str]) -> Sequence[str]:
return alias_output_names


def extract_compliant(
plx: CompliantNamespaceAny,
other: IntoExpr | NonNestedLiteral | _1DArray,
*,
str_as_lit: bool,
) -> CompliantExprAny | NonNestedLiteral:
if is_expr(other):
return other._to_compliant_expr(plx)
if isinstance(other, str) and not str_as_lit:
return plx.col(other)
if is_narwhals_series(other):
return other._compliant_series._to_expr()
if is_numpy_array(other):
ns = cast("EagerNamespaceAny", plx)
return ns._series.from_numpy(other, context=ns)._to_expr()
return other


def evaluate_output_names_and_aliases(
expr: CompliantExprAny, df: CompliantFrameAny, exclude: Sequence[str]
) -> tuple[Sequence[str], Sequence[str]]:
Expand Down Expand Up @@ -610,10 +598,8 @@ def apply_n_ary_operation(
*comparands: IntoExpr | NonNestedLiteral | _1DArray,
str_as_lit: bool,
) -> CompliantExprAny:
compliant_exprs = (
extract_compliant(plx, comparand, str_as_lit=str_as_lit)
for comparand in comparands
)
parse = plx.parse_into_expr
compliant_exprs = (parse(into, str_as_lit=str_as_lit) for into in comparands)
kinds = [
ExprKind.from_into_expr(comparand, str_as_lit=str_as_lit)
for comparand in comparands
Expand Down
4 changes: 0 additions & 4 deletions narwhals/_polars/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,6 @@ def __repr__(self) -> str: # pragma: no cover
def _with_native(self, expr: pl.Expr) -> Self:
return self.__class__(expr, self._version)

@classmethod
def _from_series(cls, series: Any) -> Self:
return cls(series.native, series._version)

def broadcast(self, kind: Literal[ExprKind.AGGREGATION, ExprKind.LITERAL]) -> Self:
# Let Polars do its thing.
return self
Expand Down
32 changes: 31 additions & 1 deletion narwhals/_polars/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import polars as pl

from narwhals._expression_parsing import is_expr, is_series
from narwhals._polars.expr import PolarsExpr
from narwhals._polars.series import PolarsSeries
from narwhals._polars.utils import extract_args_kwargs, narwhals_to_native_dtype
Expand All @@ -20,7 +21,17 @@
from narwhals._polars.dataframe import Method, PolarsDataFrame, PolarsLazyFrame
from narwhals._polars.typing import FrameT
from narwhals._utils import Version, _LimitedContext
from narwhals.typing import Into1DArray, IntoDType, IntoSchema, TimeUnit, _2DArray
from narwhals.expr import Expr
from narwhals.series import Series
from narwhals.typing import (
Into1DArray,
IntoDType,
IntoSchema,
NonNestedLiteral,
TimeUnit,
_1DArray,
_2DArray,
)


class PolarsNamespace:
Expand Down Expand Up @@ -70,6 +81,25 @@ def _expr(self) -> type[PolarsExpr]:
def _series(self) -> type[PolarsSeries]:
return PolarsSeries

def parse_into_expr(
self,
data: Expr | NonNestedLiteral | Series[pl.Series] | _1DArray,
/,
*,
str_as_lit: bool,
) -> PolarsExpr | None:
if data is None:
# NOTE: To avoid `pl.lit(None)` failing this `None` check
# https://github.com/pola-rs/polars/blob/58dd8e5770f16a9bef9009a1c05f00e15a5263c7/py-polars/polars/expr/expr.py#L2870-L2872
return data
if is_expr(data):
expr = data._to_compliant_expr(self)
assert isinstance(expr, self._expr) # noqa: S101
return expr
if isinstance(data, str) and not str_as_lit:
return self.col(data)
return self.lit(data.to_native() if is_series(data) else data, None)

@overload
def from_native(self, data: pl.DataFrame, /) -> PolarsDataFrame: ...
@overload
Expand Down
8 changes: 3 additions & 5 deletions narwhals/_polars/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
from typing_extensions import Self, TypeAlias, TypeIs

from narwhals._polars.dataframe import Method, PolarsDataFrame
from narwhals._polars.expr import PolarsExpr
from narwhals._polars.namespace import PolarsNamespace
from narwhals._utils import Version, _LimitedContext
from narwhals.dtypes import DType
Expand Down Expand Up @@ -236,9 +235,6 @@ def _from_native_object(
# scalar
return series

def _to_expr(self) -> PolarsExpr:
return self.__narwhals_namespace__()._expr._from_series(self)

def __getattr__(self, attr: str) -> Any:
if attr not in INHERITED_METHODS:
msg = f"{self.__class__.__name__} has not attribute '{attr}'."
Expand Down Expand Up @@ -509,7 +505,9 @@ def is_close(
if self._backend_version < (1, 32, 0):
name = self.name
ns = self.__narwhals_namespace__()
other_expr = other._to_expr() if isinstance(other, PolarsSeries) else other
other_expr = (
ns.lit(other.native, None) if isinstance(other, PolarsSeries) else other
)
expr = ns.col(name).is_close(
other_expr, abs_tol=abs_tol, rel_tol=rel_tol, nans_equal=nans_equal
)
Expand Down
95 changes: 32 additions & 63 deletions narwhals/dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ExprKind,
all_exprs_are_scalar_like,
check_expressions_preserve_length,
is_into_expr_eager,
is_scalar_like,
)
from narwhals._typing import Arrow, Pandas, _LazyAllowedImpl, _LazyFrameCollectImpl
Expand All @@ -42,12 +43,7 @@
supports_arrow_c_stream,
zip_strict,
)
from narwhals.dependencies import (
get_polars,
is_numpy_array,
is_numpy_array_2d,
is_pyarrow_table,
)
from narwhals.dependencies import is_numpy_array_2d, is_pyarrow_table
from narwhals.exceptions import (
ColumnNotFoundError,
InvalidIntoExprError,
Expand Down Expand Up @@ -451,26 +447,9 @@ class DataFrame(BaseFrame[DataFrameT]):
_version: ClassVar[Version] = Version.MAIN

def _extract_compliant(self, arg: Any) -> Any:
from narwhals.expr import Expr
from narwhals.series import Series

plx: EagerNamespaceAny = self.__narwhals_namespace__()
if isinstance(arg, Series):
return arg._compliant_series._to_expr()
if isinstance(arg, Expr):
return arg._to_compliant_expr(self.__narwhals_namespace__())
if isinstance(arg, str):
return plx.col(arg)
if get_polars() is not None and "polars" in str(type(arg)): # pragma: no cover
msg = (
f"Expected Narwhals object, got: {type(arg)}.\n\n"
"Perhaps you:\n"
"- Forgot a `nw.from_native` somewhere?\n"
"- Used `pl.col` instead of `nw.col`?"
)
raise TypeError(msg)
if is_numpy_array(arg):
return plx._series.from_numpy(arg, context=plx)._to_expr()
if is_into_expr_eager(arg):
plx: EagerNamespaceAny = self.__narwhals_namespace__()
return plx.parse_into_expr(arg, str_as_lit=False)
raise InvalidIntoExprError.from_invalid_type(type(arg))

@property
Expand Down Expand Up @@ -2304,43 +2283,33 @@ def _extract_compliant(self, arg: Any) -> Any:
if isinstance(arg, Series): # pragma: no cover
msg = "Binary operations between Series and LazyFrame are not supported."
raise TypeError(msg)
if isinstance(arg, str): # pragma: no cover
plx = self.__narwhals_namespace__()
return plx.col(arg)
if isinstance(arg, Expr):
if arg._metadata.n_orderable_ops:
msg = (
"Order-dependent expressions are not supported for use in LazyFrame.\n\n"
"Hint: To make the expression valid, use `.over` with `order_by` specified.\n\n"
"For example, if you wrote `nw.col('price').cum_sum()` and you have a column\n"
"`'date'` which orders your data, then replace:\n\n"
" nw.col('price').cum_sum()\n\n"
" with:\n\n"
" nw.col('price').cum_sum().over(order_by='date')\n"
" ^^^^^^^^^^^^^^^^^^^^^^\n\n"
"See https://narwhals-dev.github.io/narwhals/concepts/order_dependence/."
)
raise InvalidOperationError(msg)
if arg._metadata.is_filtration:
msg = (
"Length-changing expressions are not supported for use in LazyFrame, unless\n"
"followed by an aggregation.\n\n"
"Hints:\n"
"- Instead of `lf.select(nw.col('a').head())`, use `lf.select('a').head()\n"
"- Instead of `lf.select(nw.col('a').drop_nulls()).select(nw.sum('a'))`,\n"
" use `lf.select(nw.col('a').drop_nulls().sum())\n"
)
raise InvalidOperationError(msg)
return arg._to_compliant_expr(self.__narwhals_namespace__())
if get_polars() is not None and "polars" in str(type(arg)): # pragma: no cover
msg = (
f"Expected Narwhals object, got: {type(arg)}.\n\n"
"Perhaps you:\n"
"- Forgot a `nw.from_native` somewhere?\n"
"- Used `pl.col` instead of `nw.col`?"
)
raise TypeError(msg)
raise InvalidIntoExprError.from_invalid_type(type(arg)) # pragma: no cover
if isinstance(arg, (Expr, str)):
if isinstance(arg, Expr):
if arg._metadata.n_orderable_ops:
msg = (
"Order-dependent expressions are not supported for use in LazyFrame.\n\n"
"Hint: To make the expression valid, use `.over` with `order_by` specified.\n\n"
"For example, if you wrote `nw.col('price').cum_sum()` and you have a column\n"
"`'date'` which orders your data, then replace:\n\n"
" nw.col('price').cum_sum()\n\n"
" with:\n\n"
" nw.col('price').cum_sum().over(order_by='date')\n"
" ^^^^^^^^^^^^^^^^^^^^^^\n\n"
"See https://narwhals-dev.github.io/narwhals/concepts/order_dependence/."
)
raise InvalidOperationError(msg)
if arg._metadata.is_filtration:
msg = (
"Length-changing expressions are not supported for use in LazyFrame, unless\n"
"followed by an aggregation.\n\n"
"Hints:\n"
"- Instead of `lf.select(nw.col('a').head())`, use `lf.select('a').head()\n"
"- Instead of `lf.select(nw.col('a').drop_nulls()).select(nw.sum('a'))`,\n"
" use `lf.select(nw.col('a').drop_nulls().sum())\n"
)
raise InvalidOperationError(msg)
return self.__narwhals_namespace__().parse_into_expr(arg, str_as_lit=False)
raise InvalidIntoExprError.from_invalid_type(type(arg))

@property
def _dataframe(self) -> type[DataFrame[Any]]:
Expand Down
Loading
Loading