Skip to content
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

feat: Raise warning if reactive.Effect function returns a value #427

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
11 changes: 10 additions & 1 deletion shiny/reactive/_reactives.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,16 @@ async def _run(self) -> None:
with session_context(self._session):
try:
with ctx():
await self._fn()
res = await self._fn()

if res is not None:
warnings.warn(
"reactive.Effect function returned a value, but should return None; "
"reactive.Effects should only be used for side effects. "
"Did you mean to use reactive.Calc instead?",
ReactiveWarning,
stacklevel=2,
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this PR--can you just tell me if the warning here gives any helpful indication as to which reactive.Effect is causing the problem? If not, we could use inspect.unwrap(foobar._fn).__code__.co_filename and .co_firstlineno

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, the problem is that we don't know how many times the user-defined function has been wrapped, so we don't know how many levels in the stack we need to skip.

For example, if the user function is synchronous, then we wrap it once in an async function. But they may decorated it with @reactive.event, which would add another layer.

Now, if the user function threw an exception, I think we would be able to catch that and find the source of it, but that's not the situation here.

except SilentException:
# It's OK for SilentException to cause an Effect to stop running
pass
Expand Down
21 changes: 21 additions & 0 deletions tests/test_reactives.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,27 @@ async def o():
assert o._exec_count == 2


# ======================================================================
# Effects warn if user function returns a value
# ======================================================================
@pytest.mark.asyncio
async def test_effect_no_return_value():
# Should warn because of non-None return value
@Effect # pyright: ignore[reportGeneralTypeIssues]
def o1():
return 1

with pytest.warns(ReactiveWarning):
await flush()

# Should not warn because of None return value
@Effect() # pyright: ignore[reportGeneralTypeIssues]
def o2():
...

await flush()


# ======================================================================
# Priority for effects
# ======================================================================
Expand Down