Add deprecation warnings for Expr passed to confirmed literal-only function arguments (#1605)
* feat: add deprecation warning for Expr passed to literal-only args
- Introduced shared `_warn_if_expr_for_literal_arg` in `functions/__init__.py`
- Added `DeprecationWarning` for the following methods when `Expr` is passed as argument:
- `encode(..., encoding=Expr)`
- `decode(..., encoding=Expr)`
- `digest(..., method=Expr)`
- `arrow_cast(..., data_type=Expr)`
- `arrow_try_cast(..., data_type=Expr)`
- `arrow_metadata(..., key=Expr)`
test: update tests to check for warnings
- Implemented tests in `test_functions.py` to ensure:
- Warning is raised for `Expr` form
- No warning for native literal form
* fix(tests): resolve E501 line length issue in test_functions.py
* feat: consolidate warning helpers and update temporal function calls
- Collapsed warning helpers into a single function `_warn_if_expr_for_literal_arg` in `python/datafusion/functions/__init__.py`.
- Updated callers of temporal functions `_date_part` and `_date_trunc` in `python/datafusion/functions/__init__.py`.
- Modified the digest behavior test in `python/tests/test_functions.py` to use native method strings.
- Updated the encode/decode behavior test to use native "base64" in `python/tests/test_functions.py`.
* docs: update function examples in common-operations user guide
- Replaced `string_literal("Float64")` with `"Float64"` in examples.
- Replaced `str_lit("Int32")` with `"Int32"` in examples.
- Removed unused `string_literal` and `str_lit` imports.diff --git a/docs/source/user-guide/common-operations/functions.md b/docs/source/user-guide/common-operations/functions.md
index 5041182..9939e2f 100644
--- a/docs/source/user-guide/common-operations/functions.md
+++ b/docs/source/user-guide/common-operations/functions.md
@@ -52,7 +52,7 @@
DataFusion offers mathematical functions such as {py:func}`~datafusion.functions.pow` or {py:func}`~datafusion.functions.log`
```{code-cell} ipython3
-from datafusion import col, literal, string_literal, str_lit
+from datafusion import col, literal
from datafusion import functions as f
df.select(
@@ -122,8 +122,8 @@
```{code-cell} ipython3
df.select(
- f.arrow_cast(col('"Total"'), string_literal("Float64")).alias("total_as_float"),
- f.arrow_cast(col('"Total"'), str_lit("Int32")).alias("total_as_int")
+ f.arrow_cast(col('"Total"'), "Float64").alias("total_as_float"),
+ f.arrow_cast(col('"Total"'), "Int32").alias("total_as_int")
)
```
diff --git a/python/datafusion/functions/__init__.py b/python/datafusion/functions/__init__.py
index 54783f0..9fb20dc 100644
--- a/python/datafusion/functions/__init__.py
+++ b/python/datafusion/functions/__init__.py
@@ -64,13 +64,16 @@
from datafusion.functions import spark
-def _warn_expr_for_literal_arg(function_name: str, arg_name: str) -> None:
- warnings.warn(
- f"Passing Expr for {function_name}() argument {arg_name!r} is deprecated; "
- "pass a Python literal instead.",
- DeprecationWarning,
- stacklevel=4,
- )
+def _warn_if_expr_for_literal_arg(
+ value: Any, function_name: str, arg_name: str
+) -> None:
+ if isinstance(value, Expr):
+ warnings.warn(
+ f"Passing Expr for {function_name}() argument {arg_name!r} is deprecated; "
+ "pass a Python literal instead.",
+ DeprecationWarning,
+ stacklevel=3,
+ )
__all__ = [
@@ -437,6 +440,7 @@
>>> result.collect_column("enc")[0].as_py()
'aGVsbG8'
"""
+ _warn_if_expr_for_literal_arg(encoding, "encode", "encoding")
encoding = coerce_to_expr(encoding)
return Expr(f.encode(expr.expr, encoding.expr))
@@ -452,6 +456,7 @@
>>> result.collect_column("dec")[0].as_py()
b'hello'
"""
+ _warn_if_expr_for_literal_arg(encoding, "decode", "encoding")
encoding = coerce_to_expr(encoding)
return Expr(f.decode(expr.expr, encoding.expr))
@@ -742,6 +747,7 @@
>>> len(result.collect_column("d")[0].as_py()) > 0
True
"""
+ _warn_if_expr_for_literal_arg(method, "digest", "method")
method = coerce_to_expr(method)
return Expr(f.digest(value.expr, method.expr))
@@ -2723,8 +2729,7 @@
def _date_part(part: Expr | str, date: Expr, function_name: str) -> Expr:
- if isinstance(part, Expr):
- _warn_expr_for_literal_arg(function_name, "part")
+ _warn_if_expr_for_literal_arg(part, function_name, "part")
part = coerce_to_expr(part)
return Expr(f.date_part(part.expr, date.expr))
@@ -2760,8 +2765,7 @@
def _date_trunc(part: Expr | str, date: Expr, function_name: str) -> Expr:
- if isinstance(part, Expr):
- _warn_expr_for_literal_arg(function_name, "part")
+ _warn_if_expr_for_literal_arg(part, function_name, "part")
part = coerce_to_expr(part)
return Expr(f.date_trunc(part.expr, date.expr))
@@ -3096,6 +3100,7 @@
>>> result.collect_column("c")[0].as_py()
1.0
"""
+ _warn_if_expr_for_literal_arg(data_type, "arrow_cast", "data_type")
if isinstance(data_type, pa.DataType):
return expr.cast(data_type)
if isinstance(data_type, str):
@@ -3128,6 +3133,7 @@
>>> result.collect_column("c")[0].as_py() is None
True
"""
+ _warn_if_expr_for_literal_arg(data_type, "arrow_try_cast", "data_type")
if isinstance(data_type, pa.DataType):
return expr.try_cast(data_type)
if isinstance(data_type, str):
@@ -3235,6 +3241,7 @@
"""
if key is None:
return Expr(f.arrow_metadata(expr.expr))
+ _warn_if_expr_for_literal_arg(key, "arrow_metadata", "key")
if isinstance(key, str):
key = Expr.string_literal(key)
return Expr(f.arrow_metadata(expr.expr, key.expr))
diff --git a/python/tests/test_functions.py b/python/tests/test_functions.py
index 43dd706..894d81f 100644
--- a/python/tests/test_functions.py
+++ b/python/tests/test_functions.py
@@ -1014,7 +1014,7 @@
def test_hash_functions(df):
exprs = [
- f.digest(column("a"), literal(m))
+ f.digest(column("a"), m)
for m in (
"md5",
"sha224",
@@ -1602,12 +1602,10 @@
def test_binary_string_functions(df):
df = df.select(
- f.encode(column("a").cast(pa.string()), literal("base64").cast(pa.string())),
+ f.encode(column("a").cast(pa.string()), "base64"),
f.decode(
- f.encode(
- column("a").cast(pa.string()), literal("base64").cast(pa.string())
- ),
- literal("base64").cast(pa.string()),
+ f.encode(column("a").cast(pa.string()), "base64"),
+ "base64",
),
)
result = df.collect()
@@ -2355,6 +2353,68 @@
).collect()
assert result[0].column(0)[0].as_py() == "aX bX cX"
+ @pytest.mark.parametrize(
+ ("func", "arg_name", "expr"),
+ [
+ pytest.param(
+ f.encode,
+ "encoding",
+ lambda: f.encode(column("a"), literal("base64")),
+ id="encode-encoding",
+ ),
+ pytest.param(
+ f.decode,
+ "encoding",
+ lambda: f.decode(column("a"), literal("base64")),
+ id="decode-encoding",
+ ),
+ pytest.param(
+ f.digest,
+ "method",
+ lambda: f.digest(column("a"), literal("sha256")),
+ id="digest-method",
+ ),
+ pytest.param(
+ f.arrow_cast,
+ "data_type",
+ lambda: f.arrow_cast(column("a"), literal("Float64")),
+ id="arrow-cast-data-type",
+ ),
+ pytest.param(
+ f.arrow_try_cast,
+ "data_type",
+ lambda: f.arrow_try_cast(column("a"), literal("Float64")),
+ id="arrow-try-cast-data-type",
+ ),
+ pytest.param(
+ f.arrow_metadata,
+ "key",
+ lambda: f.arrow_metadata(column("a"), literal("k")),
+ id="arrow-metadata-key",
+ ),
+ ],
+ )
+ def test_literal_only_expr_args_warn_deprecated(self, func, arg_name, expr):
+ with pytest.warns(
+ DeprecationWarning,
+ match=(
+ rf"Passing Expr for {func.__name__}\(\) argument "
+ rf"'{arg_name}' is deprecated"
+ ),
+ ):
+ result = expr()
+ assert result is not None
+
+ def test_literal_only_native_args_do_not_warn(self):
+ with warnings.catch_warnings():
+ warnings.simplefilter("error", DeprecationWarning)
+ assert f.encode(column("a"), "base64") is not None
+ assert f.decode(column("a"), "base64") is not None
+ assert f.digest(column("a"), "sha256") is not None
+ assert f.arrow_cast(column("a"), "Float64") is not None
+ assert f.arrow_try_cast(column("a"), pa.float64()) is not None
+ assert f.arrow_metadata(column("a"), "k") is not None
+
def test_backward_compat_with_lit(self):
"""Verify that existing code using lit() still works."""
ctx = SessionContext()