diff --git a/docs/source/changes.md b/docs/source/changes.md index 67626f22..f5202905 100644 --- a/docs/source/changes.md +++ b/docs/source/changes.md @@ -9,8 +9,10 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and - {pull}`515` enables tests with graphviz in CI. Thanks to {user}`NickCrews`. - {pull}`517` raises an error when the configuration file contains a non-existing path - (fixes #514). Also adds a warning if the path is configured as a string and not a list - of strings. + (fixes #514). It also warns if the path is configured as a string and not a list of + strings. +- {pull}`519` raises an error when builtin functions are wrapped with + {func}`~pytask.task`. Closes {issue}`512`. ## 0.4.4 - 2023-12-04 diff --git a/src/_pytask/task_utils.py b/src/_pytask/task_utils.py index 59af53e3..45ec3848 100644 --- a/src/_pytask/task_utils.py +++ b/src/_pytask/task_utils.py @@ -4,6 +4,7 @@ import inspect from collections import defaultdict from pathlib import Path +from types import BuiltinFunctionType from typing import Any from typing import Callable @@ -84,6 +85,9 @@ def task( """ def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: + # Omits frame when a builtin function is wrapped. + _rich_traceback_omit = True + for arg, arg_name in ((name, "name"), (id, "id")): if not (isinstance(arg, str) or arg is None): msg = ( @@ -94,6 +98,16 @@ def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: unwrapped = inspect.unwrap(func) + # We do not allow builtins as functions because we would need to use + # ``inspect.stack`` to infer their caller location and they are unable to carry + # the pytask metadata. + if isinstance(unwrapped, BuiltinFunctionType): + msg = ( + "Builtin functions cannot be wrapped with '@task'. If necessary, wrap " + "the builtin function in a function or lambda expression." + ) + raise NotImplementedError(msg) + raw_path = inspect.getfile(unwrapped) if "" in raw_path: path = Path(unwrapped.__globals__["__file__"]).absolute().resolve() diff --git a/tests/test_task.py b/tests/test_task.py index 25f57176..b95194ec 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -662,3 +662,20 @@ def task_second(): session = build(paths=tmp_path) assert session.exit_code == ExitCode.OK + + +def test_raise_error_with_builtin_function_as_task(runner, tmp_path): + source = """ + from pytask import task + from pathlib import Path + from datetime import datetime + + task( + kwargs={"format": "%y/%m/%d"}, produces=Path("time.txt") + )(datetime.utcnow().strftime) + """ + tmp_path.joinpath("task_example.py").write_text(textwrap.dedent(source)) + + result = runner.invoke(cli, [tmp_path.as_posix()]) + assert result.exit_code == ExitCode.COLLECTION_FAILED + assert "Builtin functions cannot be wrapped" in result.output