Skip to content

Do not allow builtin functions as tasks. #519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/source/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions src/_pytask/task_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 = (
Expand All @@ -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 "<string>" in raw_path:
path = Path(unwrapped.__globals__["__file__"]).absolute().resolve()
Expand Down
17 changes: 17 additions & 0 deletions tests/test_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -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