diff --git a/narwhals/_duckdb/expr.py b/narwhals/_duckdb/expr.py index 0cfddbaa3..c79acdfae 100644 --- a/narwhals/_duckdb/expr.py +++ b/narwhals/_duckdb/expr.py @@ -7,6 +7,7 @@ from typing import Sequence from narwhals._duckdb.expr_dt import DuckDBExprDateTimeNamespace +from narwhals._duckdb.expr_name import DuckDBExprNameNamespace from narwhals._duckdb.expr_str import DuckDBExprStringNamespace from narwhals._duckdb.utils import binary_operation_returns_scalar from narwhals._duckdb.utils import get_column_name @@ -571,3 +572,7 @@ def str(self: Self) -> DuckDBExprStringNamespace: @property def dt(self: Self) -> DuckDBExprDateTimeNamespace: return DuckDBExprDateTimeNamespace(self) + + @property + def name(self: Self) -> DuckDBExprNameNamespace: + return DuckDBExprNameNamespace(self) diff --git a/narwhals/_duckdb/expr_name.py b/narwhals/_duckdb/expr_name.py new file mode 100644 index 000000000..2ed2b2ea8 --- /dev/null +++ b/narwhals/_duckdb/expr_name.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Callable + +from narwhals.exceptions import AnonymousExprError + +if TYPE_CHECKING: + from typing_extensions import Self + + from narwhals._duckdb.expr import DuckDBExpr + + +class DuckDBExprNameNamespace: + def __init__(self: Self, expr: DuckDBExpr) -> None: + self._compliant_expr = expr + + def keep(self: Self) -> DuckDBExpr: + root_names = self._compliant_expr._root_names + if root_names is None: + msg = ".name.keep" + raise AnonymousExprError.from_expr_name(msg) + return self._compliant_expr.__class__( + lambda df: [ + expr.alias(name) + for expr, name in zip(self._compliant_expr._call(df), root_names) + ], + depth=self._compliant_expr._depth, + function_name=self._compliant_expr._function_name, + root_names=root_names, + output_names=root_names, + returns_scalar=self._compliant_expr._returns_scalar, + backend_version=self._compliant_expr._backend_version, + version=self._compliant_expr._version, + kwargs=self._compliant_expr._kwargs, + ) + + def map(self: Self, function: Callable[[str], str]) -> DuckDBExpr: + root_names = self._compliant_expr._root_names + if root_names is None: + msg = ".name.map" + raise AnonymousExprError.from_expr_name(msg) + + output_names = [function(str(name)) for name in root_names] + + return self._compliant_expr.__class__( + lambda df: [ + expr.alias(name) + for expr, name in zip(self._compliant_expr._call(df), output_names) + ], + depth=self._compliant_expr._depth, + function_name=self._compliant_expr._function_name, + root_names=root_names, + output_names=output_names, + returns_scalar=self._compliant_expr._returns_scalar, + backend_version=self._compliant_expr._backend_version, + version=self._compliant_expr._version, + kwargs={**self._compliant_expr._kwargs, "function": function}, + ) + + def prefix(self: Self, prefix: str) -> DuckDBExpr: + root_names = self._compliant_expr._root_names + if root_names is None: + msg = ".name.prefix" + raise AnonymousExprError.from_expr_name(msg) + + output_names = [prefix + str(name) for name in root_names] + return self._compliant_expr.__class__( + lambda df: [ + expr.alias(name) + for expr, name in zip(self._compliant_expr._call(df), output_names) + ], + depth=self._compliant_expr._depth, + function_name=self._compliant_expr._function_name, + root_names=root_names, + output_names=output_names, + returns_scalar=self._compliant_expr._returns_scalar, + backend_version=self._compliant_expr._backend_version, + version=self._compliant_expr._version, + kwargs={**self._compliant_expr._kwargs, "prefix": prefix}, + ) + + def suffix(self: Self, suffix: str) -> DuckDBExpr: + root_names = self._compliant_expr._root_names + if root_names is None: + msg = ".name.suffix" + raise AnonymousExprError.from_expr_name(msg) + + output_names = [str(name) + suffix for name in root_names] + + return self._compliant_expr.__class__( + lambda df: [ + expr.alias(name) + for expr, name in zip(self._compliant_expr._call(df), output_names) + ], + depth=self._compliant_expr._depth, + function_name=self._compliant_expr._function_name, + root_names=root_names, + output_names=output_names, + returns_scalar=self._compliant_expr._returns_scalar, + backend_version=self._compliant_expr._backend_version, + version=self._compliant_expr._version, + kwargs={**self._compliant_expr._kwargs, "suffix": suffix}, + ) + + def to_lowercase(self: Self) -> DuckDBExpr: + root_names = self._compliant_expr._root_names + if root_names is None: + msg = ".name.to_lowercase" + raise AnonymousExprError.from_expr_name(msg) + + output_names = [str(name).lower() for name in root_names] + + return self._compliant_expr.__class__( + lambda df: [ + expr.alias(name) + for expr, name in zip(self._compliant_expr._call(df), output_names) + ], + depth=self._compliant_expr._depth, + function_name=self._compliant_expr._function_name, + root_names=root_names, + output_names=output_names, + returns_scalar=self._compliant_expr._returns_scalar, + backend_version=self._compliant_expr._backend_version, + version=self._compliant_expr._version, + kwargs=self._compliant_expr._kwargs, + ) + + def to_uppercase(self: Self) -> DuckDBExpr: + root_names = self._compliant_expr._root_names + if root_names is None: + msg = ".name.to_uppercase" + raise AnonymousExprError.from_expr_name(msg) + output_names = [str(name).upper() for name in root_names] + + return self._compliant_expr.__class__( + lambda df: [ + expr.alias(name) + for expr, name in zip(self._compliant_expr._call(df), output_names) + ], + depth=self._compliant_expr._depth, + function_name=self._compliant_expr._function_name, + root_names=root_names, + output_names=output_names, + returns_scalar=self._compliant_expr._returns_scalar, + backend_version=self._compliant_expr._backend_version, + version=self._compliant_expr._version, + kwargs=self._compliant_expr._kwargs, + ) diff --git a/narwhals/_spark_like/expr.py b/narwhals/_spark_like/expr.py index 07b68d26f..4958b2ba2 100644 --- a/narwhals/_spark_like/expr.py +++ b/narwhals/_spark_like/expr.py @@ -7,6 +7,7 @@ from typing import Sequence from narwhals._expression_parsing import infer_new_root_output_names +from narwhals._spark_like.expr_name import SparkLikeExprNameNamespace from narwhals._spark_like.expr_str import SparkLikeExprStringNamespace from narwhals._spark_like.utils import get_column_name from narwhals._spark_like.utils import maybe_evaluate @@ -499,3 +500,7 @@ def is_null(self: Self) -> Self: @property def str(self: Self) -> SparkLikeExprStringNamespace: return SparkLikeExprStringNamespace(self) + + @property + def name(self: Self) -> SparkLikeExprNameNamespace: + return SparkLikeExprNameNamespace(self) diff --git a/narwhals/_spark_like/expr_name.py b/narwhals/_spark_like/expr_name.py new file mode 100644 index 000000000..c32305270 --- /dev/null +++ b/narwhals/_spark_like/expr_name.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Callable + +from narwhals.exceptions import AnonymousExprError + +if TYPE_CHECKING: + from typing_extensions import Self + + from narwhals._spark_like.expr import SparkLikeExpr + + +class SparkLikeExprNameNamespace: + def __init__(self: Self, expr: SparkLikeExpr) -> None: + self._compliant_expr = expr + + def keep(self: Self) -> SparkLikeExpr: + root_names = self._compliant_expr._root_names + if root_names is None: + msg = ".name.keep" + raise AnonymousExprError.from_expr_name(msg) + + return self._compliant_expr.__class__( + lambda df: [ + expr.alias(name) + for expr, name in zip(self._compliant_expr._call(df), root_names) + ], + depth=self._compliant_expr._depth, + function_name=self._compliant_expr._function_name, + root_names=root_names, + output_names=root_names, + returns_scalar=self._compliant_expr._returns_scalar, + backend_version=self._compliant_expr._backend_version, + version=self._compliant_expr._version, + kwargs=self._compliant_expr._kwargs, + ) + + def map(self: Self, function: Callable[[str], str]) -> SparkLikeExpr: + root_names = self._compliant_expr._root_names + if root_names is None: + msg = ".name.map" + raise AnonymousExprError.from_expr_name(msg) + + output_names = [function(str(name)) for name in root_names] + + return self._compliant_expr.__class__( + lambda df: [ + expr.alias(name) + for expr, name in zip(self._compliant_expr._call(df), output_names) + ], + depth=self._compliant_expr._depth, + function_name=self._compliant_expr._function_name, + root_names=root_names, + output_names=output_names, + returns_scalar=self._compliant_expr._returns_scalar, + backend_version=self._compliant_expr._backend_version, + version=self._compliant_expr._version, + kwargs={**self._compliant_expr._kwargs, "function": function}, + ) + + def prefix(self: Self, prefix: str) -> SparkLikeExpr: + root_names = self._compliant_expr._root_names + if root_names is None: + msg = ".name.prefix" + raise AnonymousExprError.from_expr_name(msg) + + output_names = [prefix + str(name) for name in root_names] + return self._compliant_expr.__class__( + lambda df: [ + expr.alias(name) + for expr, name in zip(self._compliant_expr._call(df), output_names) + ], + depth=self._compliant_expr._depth, + function_name=self._compliant_expr._function_name, + root_names=root_names, + output_names=output_names, + returns_scalar=self._compliant_expr._returns_scalar, + backend_version=self._compliant_expr._backend_version, + version=self._compliant_expr._version, + kwargs={**self._compliant_expr._kwargs, "prefix": prefix}, + ) + + def suffix(self: Self, suffix: str) -> SparkLikeExpr: + root_names = self._compliant_expr._root_names + if root_names is None: + msg = ".name.suffix" + raise AnonymousExprError.from_expr_name(msg) + + output_names = [str(name) + suffix for name in root_names] + + return self._compliant_expr.__class__( + lambda df: [ + expr.alias(name) + for expr, name in zip(self._compliant_expr._call(df), output_names) + ], + depth=self._compliant_expr._depth, + function_name=self._compliant_expr._function_name, + root_names=root_names, + output_names=output_names, + returns_scalar=self._compliant_expr._returns_scalar, + backend_version=self._compliant_expr._backend_version, + version=self._compliant_expr._version, + kwargs={**self._compliant_expr._kwargs, "suffix": suffix}, + ) + + def to_lowercase(self: Self) -> SparkLikeExpr: + root_names = self._compliant_expr._root_names + if root_names is None: + msg = ".name.to_lowercase" + raise AnonymousExprError.from_expr_name(msg) + output_names = [str(name).lower() for name in root_names] + + return self._compliant_expr.__class__( + lambda df: [ + expr.alias(name) + for expr, name in zip(self._compliant_expr._call(df), output_names) + ], + depth=self._compliant_expr._depth, + function_name=self._compliant_expr._function_name, + root_names=root_names, + output_names=output_names, + returns_scalar=self._compliant_expr._returns_scalar, + backend_version=self._compliant_expr._backend_version, + version=self._compliant_expr._version, + kwargs=self._compliant_expr._kwargs, + ) + + def to_uppercase(self: Self) -> SparkLikeExpr: + root_names = self._compliant_expr._root_names + if root_names is None: + msg = ".name.to_uppercase" + raise AnonymousExprError.from_expr_name(msg) + output_names = [str(name).upper() for name in root_names] + + return self._compliant_expr.__class__( + lambda df: [ + expr.alias(name) + for expr, name in zip(self._compliant_expr._call(df), output_names) + ], + depth=self._compliant_expr._depth, + function_name=self._compliant_expr._function_name, + root_names=root_names, + output_names=output_names, + returns_scalar=self._compliant_expr._returns_scalar, + backend_version=self._compliant_expr._backend_version, + version=self._compliant_expr._version, + kwargs=self._compliant_expr._kwargs, + ) diff --git a/tests/conftest.py b/tests/conftest.py index 497103d67..60dec8815 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -232,7 +232,7 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: if ( any( x in str(metafunc.module) - for x in ("list", "name", "unpivot", "from_dict", "from_numpy", "tail") + for x in ("list", "unpivot", "from_dict", "from_numpy", "tail") ) and LAZY_CONSTRUCTORS["duckdb"] in constructors ): diff --git a/tests/expr_and_series/name/keep_test.py b/tests/expr_and_series/name/keep_test.py index 54219dae6..9c172dba6 100644 --- a/tests/expr_and_series/name/keep_test.py +++ b/tests/expr_and_series/name/keep_test.py @@ -13,34 +13,21 @@ data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} -def test_keep(request: pytest.FixtureRequest, constructor: Constructor) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_keep(constructor: Constructor) -> None: df = nw.from_native(constructor(data)) result = df.select((nw.col("foo", "BAR") * 2).name.keep()) expected = {k: [e * 2 for e in v] for k, v in data.items()} assert_equal_data(result, expected) -def test_keep_after_alias( - request: pytest.FixtureRequest, constructor: Constructor -) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_keep_after_alias(constructor: Constructor) -> None: df = nw.from_native(constructor(data)) result = df.select((nw.col("foo")).alias("alias_for_foo").name.keep()) expected = {"foo": data["foo"]} assert_equal_data(result, expected) -def test_keep_raise_anonymous( - request: pytest.FixtureRequest, constructor: Constructor -) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_keep_raise_anonymous(constructor: Constructor) -> None: df_raw = constructor(data) df = nw.from_native(df_raw) diff --git a/tests/expr_and_series/name/map_test.py b/tests/expr_and_series/name/map_test.py index d86a12fbe..5b93de213 100644 --- a/tests/expr_and_series/name/map_test.py +++ b/tests/expr_and_series/name/map_test.py @@ -17,34 +17,21 @@ def map_func(s: str | None) -> str: return str(s)[::-1].lower() -def test_map(request: pytest.FixtureRequest, constructor: Constructor) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_map(constructor: Constructor) -> None: df = nw.from_native(constructor(data)) result = df.select((nw.col("foo", "BAR") * 2).name.map(function=map_func)) expected = {map_func(k): [e * 2 for e in v] for k, v in data.items()} assert_equal_data(result, expected) -def test_map_after_alias( - request: pytest.FixtureRequest, constructor: Constructor -) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_map_after_alias(constructor: Constructor) -> None: df = nw.from_native(constructor(data)) result = df.select((nw.col("foo")).alias("alias_for_foo").name.map(function=map_func)) expected = {map_func("foo"): data["foo"]} assert_equal_data(result, expected) -def test_map_raise_anonymous( - request: pytest.FixtureRequest, constructor: Constructor -) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_map_raise_anonymous(constructor: Constructor) -> None: df_raw = constructor(data) df = nw.from_native(df_raw) diff --git a/tests/expr_and_series/name/prefix_test.py b/tests/expr_and_series/name/prefix_test.py index 0f6ff2f61..5894153be 100644 --- a/tests/expr_and_series/name/prefix_test.py +++ b/tests/expr_and_series/name/prefix_test.py @@ -14,34 +14,21 @@ prefix = "with_prefix_" -def test_prefix(request: pytest.FixtureRequest, constructor: Constructor) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_prefix(constructor: Constructor) -> None: df = nw.from_native(constructor(data)) result = df.select((nw.col("foo", "BAR") * 2).name.prefix(prefix)) expected = {prefix + str(k): [e * 2 for e in v] for k, v in data.items()} assert_equal_data(result, expected) -def test_suffix_after_alias( - request: pytest.FixtureRequest, constructor: Constructor -) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_suffix_after_alias(constructor: Constructor) -> None: df = nw.from_native(constructor(data)) result = df.select((nw.col("foo")).alias("alias_for_foo").name.prefix(prefix)) expected = {prefix + "foo": data["foo"]} assert_equal_data(result, expected) -def test_prefix_raise_anonymous( - request: pytest.FixtureRequest, constructor: Constructor -) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_prefix_raise_anonymous(constructor: Constructor) -> None: df_raw = constructor(data) df = nw.from_native(df_raw) diff --git a/tests/expr_and_series/name/suffix_test.py b/tests/expr_and_series/name/suffix_test.py index 479546630..1c5816154 100644 --- a/tests/expr_and_series/name/suffix_test.py +++ b/tests/expr_and_series/name/suffix_test.py @@ -13,34 +13,21 @@ suffix = "_with_suffix" -def test_suffix(request: pytest.FixtureRequest, constructor: Constructor) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_suffix(constructor: Constructor) -> None: df = nw.from_native(constructor(data)) result = df.select((nw.col("foo", "BAR") * 2).name.suffix(suffix)) expected = {str(k) + suffix: [e * 2 for e in v] for k, v in data.items()} assert_equal_data(result, expected) -def test_suffix_after_alias( - request: pytest.FixtureRequest, constructor: Constructor -) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_suffix_after_alias(constructor: Constructor) -> None: df = nw.from_native(constructor(data)) result = df.select((nw.col("foo")).alias("alias_for_foo").name.suffix(suffix)) expected = {"foo" + suffix: data["foo"]} assert_equal_data(result, expected) -def test_suffix_raise_anonymous( - request: pytest.FixtureRequest, constructor: Constructor -) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_suffix_raise_anonymous(constructor: Constructor) -> None: df_raw = constructor(data) df = nw.from_native(df_raw) diff --git a/tests/expr_and_series/name/to_lowercase_test.py b/tests/expr_and_series/name/to_lowercase_test.py index 191e72275..7acf9af59 100644 --- a/tests/expr_and_series/name/to_lowercase_test.py +++ b/tests/expr_and_series/name/to_lowercase_test.py @@ -13,34 +13,21 @@ data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} -def test_to_lowercase(request: pytest.FixtureRequest, constructor: Constructor) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_to_lowercase(constructor: Constructor) -> None: df = nw.from_native(constructor(data)) result = df.select((nw.col("foo", "BAR") * 2).name.to_lowercase()) expected = {k.lower(): [e * 2 for e in v] for k, v in data.items()} assert_equal_data(result, expected) -def test_to_lowercase_after_alias( - request: pytest.FixtureRequest, constructor: Constructor -) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_to_lowercase_after_alias(constructor: Constructor) -> None: df = nw.from_native(constructor(data)) result = df.select((nw.col("BAR")).alias("ALIAS_FOR_BAR").name.to_lowercase()) expected = {"bar": data["BAR"]} assert_equal_data(result, expected) -def test_to_lowercase_raise_anonymous( - request: pytest.FixtureRequest, constructor: Constructor -) -> None: - if "pyspark" in str(constructor): - request.applymarker(pytest.mark.xfail) - +def test_to_lowercase_raise_anonymous(constructor: Constructor) -> None: df_raw = constructor(data) df = nw.from_native(df_raw) diff --git a/tests/expr_and_series/name/to_uppercase_test.py b/tests/expr_and_series/name/to_uppercase_test.py index b0e49c7d5..7d0bd7e57 100644 --- a/tests/expr_and_series/name/to_uppercase_test.py +++ b/tests/expr_and_series/name/to_uppercase_test.py @@ -13,31 +13,21 @@ data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]} -def test_to_uppercase(constructor: Constructor, request: pytest.FixtureRequest) -> None: - if any(x in str(constructor) for x in ("duckdb", "pyspark")): - request.applymarker(pytest.mark.xfail) +def test_to_uppercase(constructor: Constructor) -> None: df = nw.from_native(constructor(data)) result = df.select((nw.col("foo", "BAR") * 2).name.to_uppercase()) expected = {k.upper(): [e * 2 for e in v] for k, v in data.items()} assert_equal_data(result, expected) -def test_to_uppercase_after_alias( - constructor: Constructor, request: pytest.FixtureRequest -) -> None: - if any(x in str(constructor) for x in ("duckdb", "pyspark")): - request.applymarker(pytest.mark.xfail) +def test_to_uppercase_after_alias(constructor: Constructor) -> None: df = nw.from_native(constructor(data)) result = df.select((nw.col("foo")).alias("alias_for_foo").name.to_uppercase()) expected = {"FOO": data["foo"]} assert_equal_data(result, expected) -def test_to_uppercase_raise_anonymous( - constructor: Constructor, request: pytest.FixtureRequest -) -> None: - if any(x in str(constructor) for x in ("duckdb", "pyspark")): - request.applymarker(pytest.mark.xfail) +def test_to_uppercase_raise_anonymous(constructor: Constructor) -> None: df_raw = constructor(data) df = nw.from_native(df_raw)