Skip to content

Commit e2c4fd5

Browse files
enh: Add nw.format (#3169)
* enh: Add `nw.format`# * add to stable * so stable it could drink you under a table * fixup * Update narwhals/stable/v1/__init__.py Co-authored-by: Francesco Bruzzesi <[email protected]> * fixup --------- Co-authored-by: Francesco Bruzzesi <[email protected]>
1 parent f4787d3 commit e2c4fd5

File tree

8 files changed

+109
-0
lines changed

8 files changed

+109
-0
lines changed

docs/api-reference/narwhals.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Here are the top-level functions available in Narwhals.
1414
- concat
1515
- concat_str
1616
- exclude
17+
- format
1718
- from_arrow
1819
- from_dict
1920
- from_dicts

narwhals/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
concat,
5555
concat_str,
5656
exclude,
57+
format,
5758
from_arrow,
5859
from_dict,
5960
from_dicts,
@@ -136,6 +137,7 @@
136137
"dtypes",
137138
"exceptions",
138139
"exclude",
140+
"format",
139141
"from_arrow",
140142
"from_dict",
141143
"from_dicts",

narwhals/functions.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,3 +1746,43 @@ def coalesce(
17461746
),
17471747
ExprMetadata.from_horizontal_op(*flat_exprs),
17481748
)
1749+
1750+
1751+
def format(f_string: str, *args: IntoExpr) -> Expr:
1752+
"""Format expressions as a string.
1753+
1754+
Arguments:
1755+
f_string: A string that with placeholders.
1756+
args: Expression(s) that fill the placeholders.
1757+
1758+
Examples:
1759+
>>> import duckdb
1760+
>>> import narwhals as nw
1761+
>>> rel = duckdb.sql("select * from values ('a', 1), ('b', 2), ('c', 3) df(a, b)")
1762+
>>> df = nw.from_native(rel)
1763+
>>> df.with_columns(formatted=nw.format("foo_{}_bar_{}", nw.col("a"), "b"))
1764+
┌─────────────────────────────────┐
1765+
| Narwhals LazyFrame |
1766+
|---------------------------------|
1767+
|┌─────────┬───────┬─────────────┐|
1768+
|│ a │ b │ formatted │|
1769+
|│ varchar │ int32 │ varchar │|
1770+
|├─────────┼───────┼─────────────┤|
1771+
|│ a │ 1 │ foo_a_bar_1 │|
1772+
|│ b │ 2 │ foo_b_bar_2 │|
1773+
|│ c │ 3 │ foo_c_bar_3 │|
1774+
|└─────────┴───────┴─────────────┘|
1775+
└─────────────────────────────────┘
1776+
"""
1777+
if (n_placeholders := f_string.count("{}")) != len(args):
1778+
msg = f"number of placeholders should equal the number of arguments. Expected {n_placeholders} arguments, got {len(args)}."
1779+
raise ValueError(msg)
1780+
1781+
exprs = []
1782+
it = iter(args)
1783+
for i, s in enumerate(f_string.split("{}")):
1784+
if i > 0:
1785+
exprs.append(next(it))
1786+
if len(s) > 0:
1787+
exprs.append(lit(s))
1788+
return concat_str(exprs, separator="")

narwhals/stable/v1/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,11 @@ def concat_str(
11751175
)
11761176

11771177

1178+
def format(f_string: str, *args: IntoExpr) -> Expr:
1179+
"""Format expressions as a string."""
1180+
return _stableify(nw.format(f_string, *args))
1181+
1182+
11781183
def coalesce(exprs: IntoExpr | Iterable[IntoExpr], *more_exprs: IntoExpr) -> Expr:
11791184
return _stableify(nw.coalesce(exprs, *more_exprs))
11801185

@@ -1412,6 +1417,7 @@ def scan_parquet(
14121417
"dtypes",
14131418
"exceptions",
14141419
"exclude",
1420+
"format",
14151421
"from_arrow",
14161422
"from_dict",
14171423
"from_dicts",

narwhals/stable/v2/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,16 @@ def concat_str(
915915
)
916916

917917

918+
def format(f_string: str, *args: IntoExpr) -> Expr:
919+
"""Format expressions as a string.
920+
921+
Arguments:
922+
f_string: A string that with placeholders.
923+
args: Expression(s) that fill the placeholders.
924+
"""
925+
return _stableify(nw.format(f_string, *args))
926+
927+
918928
def coalesce(exprs: IntoExpr | Iterable[IntoExpr], *more_exprs: IntoExpr) -> Expr:
919929
"""Folds the columns from left to right, keeping the first non-null value.
920930
@@ -1233,6 +1243,7 @@ def scan_parquet(
12331243
"dtypes",
12341244
"exceptions",
12351245
"exclude",
1246+
"format",
12361247
"from_arrow",
12371248
"from_dict",
12381249
"from_dicts",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import annotations
2+
3+
import pandas as pd
4+
import pytest
5+
6+
import narwhals as nw
7+
from tests.utils import Constructor, assert_equal_data
8+
9+
10+
def test_format(constructor: Constructor) -> None:
11+
df = nw.from_native(
12+
constructor(
13+
{
14+
"name": ["bob", "alice", "dodo"],
15+
"surname": ["builder", "wonderlander", "extinct"],
16+
}
17+
)
18+
)
19+
result = df.select(fmt=nw.format("hello {} {} wassup", "name", nw.col("surname")))
20+
expected = {
21+
"fmt": [
22+
"hello bob builder wassup",
23+
"hello alice wonderlander wassup",
24+
"hello dodo extinct wassup",
25+
]
26+
}
27+
assert_equal_data(result, expected)
28+
result = df.select(fmt=nw.format("{} {} wassup", "name", nw.col("surname")))
29+
expected = {
30+
"fmt": ["bob builder wassup", "alice wonderlander wassup", "dodo extinct wassup"]
31+
}
32+
assert_equal_data(result, expected)
33+
34+
35+
def test_format_invalid() -> None:
36+
df = nw.from_native(
37+
pd.DataFrame(
38+
{
39+
"name": ["bob", "alice", "dodo"],
40+
"surname": ["builder", "wonderlander", "extinct"],
41+
}
42+
)
43+
)
44+
with pytest.raises(ValueError, match="Expected 2 arguments, got 1"):
45+
df.select(fmt=nw.format("hello {} {} wassup", "name"))

tests/v1_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def test_toplevel() -> None:
7373
mean_h=nw_v1.mean_horizontal("a"),
7474
len=nw_v1.len(),
7575
concat_str=nw_v1.concat_str(nw_v1.lit("a"), nw_v1.lit("b")),
76+
fmt=nw_v1.format("{}", "a"),
7677
any_h=nw_v1.any_horizontal(nw_v1.lit(True), nw_v1.lit(True)),
7778
all_h=nw_v1.all_horizontal(nw_v1.lit(True), nw_v1.lit(True)),
7879
first=nw_v1.nth(0),
@@ -91,6 +92,7 @@ def test_toplevel() -> None:
9192
"mean_h": [1, 2, 3],
9293
"len": [3, 3, 3],
9394
"concat_str": ["ab", "ab", "ab"],
95+
"fmt": ["1", "2", "3"],
9496
"any_h": [True, True, True],
9597
"all_h": [True, True, True],
9698
"first": [1, 2, 3],

tests/v2_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def test_toplevel() -> None:
5555
mean_h=nw_v2.mean_horizontal("a"),
5656
len=nw_v2.len(),
5757
concat_str=nw_v2.concat_str(nw_v2.lit("a"), nw_v2.lit("b")),
58+
fmt=nw_v2.format("{}", "a"),
5859
any_h=nw_v2.any_horizontal(nw_v2.lit(True), nw_v2.lit(True), ignore_nulls=True),
5960
all_h=nw_v2.all_horizontal(nw_v2.lit(True), nw_v2.lit(True), ignore_nulls=True),
6061
first=nw_v2.nth(0),
@@ -73,6 +74,7 @@ def test_toplevel() -> None:
7374
"mean_h": [1, 2, 3],
7475
"len": [3, 3, 3],
7576
"concat_str": ["ab", "ab", "ab"],
77+
"fmt": ["1", "2", "3"],
7678
"any_h": [True, True, True],
7779
"all_h": [True, True, True],
7880
"first": [1, 2, 3],

0 commit comments

Comments
 (0)