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

Add aeval: Asynchronous Equivalent to eval in aioconsole #120

Merged
merged 8 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 2 additions & 1 deletion aioconsole/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
It also includes an interactive event loop, and a command line interface.
"""

from .execute import aexec
from .execute import aexec, aeval
from .console import AsynchronousConsole, interact
from .stream import ainput, aprint, get_standard_streams
from .events import InteractiveEventLoop, InteractiveEventLoopPolicy
Expand All @@ -17,6 +17,7 @@

__all__ = [
"aexec",
"aeval",
"ainput",
"aprint",
"AsynchronousConsole",
Expand Down
28 changes: 28 additions & 0 deletions aioconsole/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,31 @@ async def aexec(source, local=None, stream=None, filename="<aexec>"):
if isinstance(tree, ast.Interactive):
exec_single_result(result, new_local, stream)
full_update(local, new_local)


async def aeval(source, local=None):
"""Asynchronous equivalent to *eval*."""
if local is None:
local = {}

if not isinstance(local, dict):
raise TypeError("globals must be a dict")

key = "__result__"
# Ensure that the result key is unique within the local namespace
# while key in local: key += "_"

# Perform syntax check to ensure the input is a valid eval expression
try:
ast.parse(source, mode="eval")
except SyntaxError:
raise

# Assign the result of the expression to a known variable
wrapped_code = f"{key} = {source}"

# Use aexec to evaluate the wrapped code within the given local namespace
await aexec(wrapped_code, local=local)

# Return the result from the local namespace
return local.pop(key)
129 changes: 128 additions & 1 deletion tests/test_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import asyncio

import pytest
from aioconsole import aexec
from aioconsole import aexec, aeval
from aioconsole.execute import compile_for_aexec


Expand Down Expand Up @@ -124,3 +124,130 @@ async def test_correct():
await aexec("async def x(): return")
await aexec("def x(): yield")
await aexec("async def x(): yield")


def echo(x):
return x


async def aecho(x):
return echo(x)


# Parametrized test with a variety of expressions
@pytest.mark.asyncio
@pytest.mark.parametrize(
"expression, local",
[
# Valid Simple Expressions
("1 + 2", None),
("sum([i * i for i in range(10)])", None),
# Invalid Expressions
("def foo(): return 42", None),
("x = 1", None),
("x = 1\nx + 1", None),
("for i in range(10): pass", None),
("if True: pass", None),
("while True: break", None),
("try: pass\nexcept: pass", None),
# Expressions Involving Undefined Variables
("undefined_variable", None),
("undefined_function()", None),
# Expressions with Deliberate Errors
("1/0", None),
("open('nonexistent_file.txt')", None),
# Lambda and Anonymous Functions
("(lambda x: x * 2)(5)", None),
# Expressions with Built-in Functions
("len('test')", None),
("min([3, 1, 4, 1, 5, 9])", None),
("max([x * x for x in range(10)])", None),
# Boolean and Conditional Expressions
("True and False", None),
("not True", None), # Boolean negation
("5 if True else 10", None),
# String Manipulation
("'hello' + ' ' + 'world'", None),
("f'hello {42}'", None),
# Complex List Comprehensions
("[x for x in range(5)]", None),
("[x * x for x in range(10) if x % 2 == 0]", None),
# Expressions with Syntax Errors
("return 42", None),
("yield 5", None),
# Test with await
("await aecho(5)", {"aecho": aecho, "echo": echo}),
# Test invalid local
("...", []),
("...", "string_instead_of_dict"),
("...", 42),
("...", set()),
("...", ...),
("...", 1.5),
("...", object()),
("...", asyncio),
("...", lambda: ...),
("...", {"__result__": 99}),
# Invalid expressions
("", None),
(None, None),
(0, None),
({}, None),
(object(), None),
(asyncio, None),
(..., None),
(lambda: ..., None),
],
)
async def test_aeval(expression, local):
# Capture the result or exception of the synchronous eval
sync_exc = None
result = None
try:
if isinstance(expression, str):
sync_expression = expression.lstrip("await a")
else:
sync_expression = expression

result = eval(sync_expression, local)
except Exception as exc:
sync_exc = exc

# Capture the result or exception of the asynchronous eval
async_exc = None
async_result = None
try:
async_result = await aeval(expression, local)
except Exception as exc:
async_exc = exc

# Assert that the exceptions are of the same type
assert type(sync_exc) == type(async_exc)
# Assert that the results match
assert result == async_result


# Test calling an async function without awaiting it
@pytest.mark.asyncio
async def test_aeval_async_func_without_await():
expression = "asyncio.sleep(0)"
local = {"asyncio": asyncio}
result = await aeval(expression, local)
assert asyncio.iscoroutine(result)
await result


@pytest.mark.asyncio
async def test_aeval_valid_await_syntax():
expression = "await aecho(10)"
local = {"aecho": aecho}
result = await aeval(expression, local)
assert result == 10


@pytest.mark.asyncio
async def test_aeval_coro_in_local():
expression = "await coro"
local = {"coro": aecho(10)}
result = await aeval(expression, local)
assert result == 10
Loading