Skip to content

Commit 29b0722

Browse files
committed
Allow callable references in sqlite-utils convert, closes #686
This allows users to pass just a callable reference like `r.parsedate` instead of `r.parsedate(value)` to the convert command. The code now detects when the input evaluates to a callable and uses it directly. Examples that now work: - sqlite-utils convert my.db table col r.parsedate - sqlite-utils convert my.db table col json.loads --import json
1 parent f9e5fbb commit 29b0722

File tree

3 files changed

+74
-3
lines changed

3 files changed

+74
-3
lines changed

docs/cli.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1849,7 +1849,19 @@ These recipes can be used in the code passed to ``sqlite-utils convert`` like th
18491849
sqlite-utils convert my.db mytable mycolumn \
18501850
'r.jsonsplit(value)'
18511851
1852-
To use any of the documented parameters, do this:
1852+
You can also pass the recipe function directly without the ``(value)`` part - sqlite-utils will detect that it is a callable and use it automatically:
1853+
1854+
.. code-block:: bash
1855+
1856+
sqlite-utils convert my.db mytable mycolumn r.parsedate
1857+
1858+
This shorter syntax works for any callable, including functions from imported modules:
1859+
1860+
.. code-block:: bash
1861+
1862+
sqlite-utils convert my.db mytable mycolumn json.loads --import json
1863+
1864+
To use any of the documented parameters, use the full function call syntax:
18531865

18541866
.. code-block:: bash
18551867

sqlite_utils/utils.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -450,13 +450,26 @@ def progressbar(*args, **kwargs):
450450

451451
def _compile_code(code, imports, variable="value"):
452452
globals = {"r": recipes, "recipes": recipes}
453+
# Handle imports first so they're available for all approaches
454+
for import_ in imports:
455+
globals[import_.split(".")[0]] = __import__(import_)
456+
453457
# If user defined a convert() function, return that
454458
try:
455459
exec(code, globals)
456460
return globals["convert"]
457461
except (AttributeError, SyntaxError, NameError, KeyError, TypeError):
458462
pass
459463

464+
# Check if code is a direct callable reference
465+
# e.g. "r.parsedate" instead of "r.parsedate(value)"
466+
try:
467+
fn = eval(code, globals)
468+
if callable(fn):
469+
return fn
470+
except Exception:
471+
pass
472+
460473
# Try compiling their code as a function instead
461474
body_variants = [code]
462475
# If single line and no 'return', try adding the return
@@ -478,8 +491,6 @@ def _compile_code(code, imports, variable="value"):
478491
if code_o is None:
479492
raise SyntaxError("Could not compile code")
480493

481-
for import_ in imports:
482-
globals[import_.split(".")[0]] = __import__(import_)
483494
exec(code_o, globals)
484495
return globals["fn"]
485496

tests/test_cli_convert.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,3 +645,51 @@ def test_convert_handles_falsey_values(fresh_db_and_path):
645645
assert result.exit_code == 0, result.output
646646
assert db["t"].get(1)["x"] == 1
647647
assert db["t"].get(2)["x"] == 2
648+
649+
650+
@pytest.mark.parametrize(
651+
"code",
652+
[
653+
# Direct callable reference (issue #686)
654+
"r.parsedate",
655+
"recipes.parsedate",
656+
# Traditional call syntax still works
657+
"r.parsedate(value)",
658+
"recipes.parsedate(value)",
659+
],
660+
)
661+
def test_convert_callable_reference(test_db_and_path, code):
662+
"""Test that callable references like r.parsedate work without (value)"""
663+
db, db_path = test_db_and_path
664+
result = CliRunner().invoke(
665+
cli.cli, ["convert", db_path, "example", "dt", code], catch_exceptions=False
666+
)
667+
assert result.exit_code == 0, result.output
668+
rows = list(db["example"].rows)
669+
assert rows[0]["dt"] == "2019-10-05"
670+
assert rows[1]["dt"] == "2019-10-06"
671+
assert rows[2]["dt"] == ""
672+
assert rows[3]["dt"] is None
673+
674+
675+
def test_convert_callable_reference_with_import(fresh_db_and_path):
676+
"""Test callable reference from an imported module"""
677+
db, db_path = fresh_db_and_path
678+
db["example"].insert({"id": 1, "data": '{"name": "test"}'})
679+
result = CliRunner().invoke(
680+
cli.cli,
681+
[
682+
"convert",
683+
db_path,
684+
"example",
685+
"data",
686+
"json.loads",
687+
"--import",
688+
"json",
689+
],
690+
catch_exceptions=False,
691+
)
692+
assert result.exit_code == 0, result.output
693+
# json.loads returns a dict, which sqlite stores as JSON string
694+
row = db["example"].get(1)
695+
assert row["data"] == '{"name": "test"}'

0 commit comments

Comments
 (0)