Skip to content

Commit 96f990b

Browse files
timsaucerclaude
andcommitted
feat: accept native ints and a single argument in range and gen_series
range() required all three of start, stop, and step as Expr, although upstream also accepts range(stop) and range(start, stop). Make stop and step optional in the bindings for both range and gen_series, so a single argument is the upper bound starting at 0, like Python's built-in range. Accept plain ints for start, stop, and step, coerced to literals, so callers no longer need lit(). Passing step without stop raises ValueError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8173554 commit 96f990b

3 files changed

Lines changed: 120 additions & 32 deletions

File tree

‎crates/core/src/functions.rs‎

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,20 +131,46 @@ fn array_to_string(array: PyExpr, delimiter: PyExpr, null_string: Option<PyExpr>
131131
.into()
132132
}
133133

134-
#[pyfunction]
135-
#[pyo3(signature = (start, stop, step=None))]
136-
fn gen_series(start: PyExpr, stop: PyExpr, step: Option<PyExpr>) -> PyExpr {
137-
let mut args = vec![start.into(), stop.into()];
138-
if let Some(step) = step {
139-
args.push(step.into());
140-
}
134+
/// Builds `range` or `gen_series` from its one, two, or three arguments.
135+
fn series_expr(
136+
udf: std::sync::Arc<datafusion::logical_expr::ScalarUDF>,
137+
start: PyExpr,
138+
stop: Option<PyExpr>,
139+
step: Option<PyExpr>,
140+
) -> PyExpr {
141+
let args = std::iter::once(start)
142+
.chain(stop)
143+
.chain(step)
144+
.map(Into::into)
145+
.collect();
141146
Expr::ScalarFunction(datafusion::logical_expr::expr::ScalarFunction::new_udf(
142-
datafusion::functions_nested::range::gen_series_udf(),
143-
args,
147+
udf, args,
144148
))
145149
.into()
146150
}
147151

152+
#[pyfunction]
153+
#[pyo3(signature = (start, stop=None, step=None))]
154+
fn range(start: PyExpr, stop: Option<PyExpr>, step: Option<PyExpr>) -> PyExpr {
155+
series_expr(
156+
datafusion::functions_nested::range::range_udf(),
157+
start,
158+
stop,
159+
step,
160+
)
161+
}
162+
163+
#[pyfunction]
164+
#[pyo3(signature = (start, stop=None, step=None))]
165+
fn gen_series(start: PyExpr, stop: Option<PyExpr>, step: Option<PyExpr>) -> PyExpr {
166+
series_expr(
167+
datafusion::functions_nested::range::gen_series_udf(),
168+
start,
169+
stop,
170+
step,
171+
)
172+
}
173+
148174
#[pyfunction]
149175
fn make_map(keys: Vec<PyExpr>, values: Vec<PyExpr>) -> PyExpr {
150176
let keys = keys.into_iter().map(|x| x.into()).collect();
@@ -701,7 +727,6 @@ array_fn!(array_min, array);
701727
array_fn!(array_reverse, array);
702728
array_fn!(cardinality, array);
703729
array_fn!(flatten, array);
704-
array_fn!(range, start stop step);
705730

706731
// Map Functions
707732
array_fn!(map_keys, map);

‎python/datafusion/functions/__init__.py‎

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3126,18 +3126,57 @@ def array(*args: Expr) -> Expr:
31263126
return make_array(*args)
31273127

31283128

3129-
def range(start: Expr, stop: Expr, step: Expr) -> Expr:
3130-
"""Create a list of values in the range between start and stop.
3129+
def _series(
3130+
fn: Callable[..., Any],
3131+
name: str,
3132+
start: Expr | int,
3133+
stop: Expr | int | None,
3134+
step: Expr | int | None,
3135+
) -> Expr:
3136+
if stop is None and step is not None:
3137+
msg = f"{name}() requires stop when step is given"
3138+
raise ValueError(msg)
3139+
stop = coerce_to_expr_or_none(stop)
3140+
step = coerce_to_expr_or_none(step)
3141+
return Expr(
3142+
fn(
3143+
coerce_to_expr(start).expr,
3144+
stop.expr if stop is not None else None,
3145+
step.expr if step is not None else None,
3146+
)
3147+
)
3148+
3149+
3150+
def range(
3151+
start: Expr | int,
3152+
stop: Expr | int | None = None,
3153+
step: Expr | int | None = None,
3154+
) -> Expr:
3155+
"""Create a list of values from ``start`` up to, but excluding, ``stop``.
3156+
3157+
With a single argument, it is the upper bound and the range starts at 0,
3158+
like Python's built-in :py:class:`range`.
31313159
31323160
Examples:
31333161
>>> ctx = dfn.SessionContext()
31343162
>>> df = ctx.from_pydict({"a": [1]})
3135-
>>> result = df.select(
3136-
... dfn.functions.range(dfn.lit(0), dfn.lit(5), dfn.lit(2)).alias("r"))
3163+
>>> result = df.select(dfn.functions.range(5).alias("r"))
3164+
>>> result.collect_column("r")[0].as_py()
3165+
[0, 1, 2, 3, 4]
3166+
3167+
Specify a ``stop``:
3168+
3169+
>>> result = df.select(dfn.functions.range(1, stop=5).alias("r"))
3170+
>>> result.collect_column("r")[0].as_py()
3171+
[1, 2, 3, 4]
3172+
3173+
Specify a ``step``:
3174+
3175+
>>> result = df.select(dfn.functions.range(0, stop=5, step=2).alias("r"))
31373176
>>> result.collect_column("r")[0].as_py()
31383177
[0, 2, 4]
31393178
"""
3140-
return Expr(f.range(start.expr, stop.expr, step.expr))
3179+
return _series(f.range, "range", start, stop, step)
31413180

31423181

31433182
def uuid() -> Expr:
@@ -5077,38 +5116,45 @@ def string_to_list(
50775116
return string_to_array(string, delimiter, null_string)
50785117

50795118

5080-
def gen_series(start: Expr, stop: Expr, step: Expr | None = None) -> Expr:
5081-
"""Creates a list of values in the range between start and stop.
5119+
def gen_series(
5120+
start: Expr | int,
5121+
stop: Expr | int | None = None,
5122+
step: Expr | int | None = None,
5123+
) -> Expr:
5124+
"""Creates a list of values from ``start`` up to and including ``stop``.
50825125
5083-
Unlike :py:func:`range`, this includes the upper bound.
5126+
Unlike :py:func:`range`, this includes the upper bound. With a single
5127+
argument, it is the upper bound and the series starts at 0.
50845128
50855129
Examples:
50865130
>>> ctx = dfn.SessionContext()
50875131
>>> df = ctx.from_pydict({"a": [0]})
5088-
>>> result = df.select(
5089-
... dfn.functions.gen_series(
5090-
... dfn.lit(1), dfn.lit(5),
5091-
... ).alias("result"))
5132+
>>> result = df.select(dfn.functions.gen_series(3).alias("result"))
5133+
>>> result.collect_column("result")[0].as_py()
5134+
[0, 1, 2, 3]
5135+
5136+
Specify a ``stop``:
5137+
5138+
>>> result = df.select(dfn.functions.gen_series(1, stop=5).alias("result"))
50925139
>>> result.collect_column("result")[0].as_py()
50935140
[1, 2, 3, 4, 5]
50945141
5095-
Specify a custom ``step``:
5142+
Specify a ``step``:
50965143
50975144
>>> result = df.select(
5098-
... dfn.functions.gen_series(
5099-
... dfn.lit(1), dfn.lit(10), step=dfn.lit(3),
5100-
... ).alias("result"))
5145+
... dfn.functions.gen_series(1, stop=10, step=3).alias("result"))
51015146
>>> result.collect_column("result")[0].as_py()
51025147
[1, 4, 7, 10]
51035148
"""
5104-
step_expr = step.expr if step is not None else None
5105-
return Expr(f.gen_series(start.expr, stop.expr, step_expr))
5149+
return _series(f.gen_series, "gen_series", start, stop, step)
51065150

51075151

5108-
def generate_series(start: Expr, stop: Expr, step: Expr | None = None) -> Expr:
5109-
"""Creates a list of values in the range between start and stop.
5110-
5111-
Unlike :py:func:`range`, this includes the upper bound.
5152+
def generate_series(
5153+
start: Expr | int,
5154+
stop: Expr | int | None = None,
5155+
step: Expr | int | None = None,
5156+
) -> Expr:
5157+
"""Creates a list of values from ``start`` up to and including ``stop``.
51125158
51135159
See Also:
51145160
This is an alias for :py:func:`gen_series`.

‎python/tests/test_functions.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2359,6 +2359,23 @@ def test_gen_series_with_step():
23592359
assert result[0].column(0)[0].as_py() == [1, 4, 7, 10]
23602360

23612361

2362+
@pytest.mark.parametrize(
2363+
("func", "expected"),
2364+
[(f.range, [[0], [0, 1]]), (f.gen_series, [[0, 1], [0, 1, 2]])],
2365+
)
2366+
def test_series_single_arg_accepts_column(func, expected):
2367+
ctx = SessionContext()
2368+
df = ctx.from_pydict({"n": [1, 2]})
2369+
result = df.select(func(column("n")).alias("v"))
2370+
assert result.collect_column("v").to_pylist() == expected
2371+
2372+
2373+
@pytest.mark.parametrize("func", [f.range, f.gen_series, f.generate_series])
2374+
def test_series_step_requires_stop(func):
2375+
with pytest.raises(ValueError, match="requires stop"):
2376+
func(0, step=2)
2377+
2378+
23622379
class TestPythonicNativeTypes:
23632380
"""Tests for accepting native Python types instead of requiring lit()."""
23642381

0 commit comments

Comments
 (0)