Skip to content

Commit 5a24f32

Browse files
committed
Implement the RunPyJob for eval/exec
Closes #23
1 parent 5eae068 commit 5a24f32

File tree

3 files changed

+66
-4
lines changed

3 files changed

+66
-4
lines changed

mqterm/jobs.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import logging
3+
import os
34
from binascii import hexlify
45
from hashlib import sha256
56
from io import BytesIO
@@ -22,6 +23,7 @@ def __init__(self, cmd, args=[], client_id="localhost", **kwargs):
2223
f"Wrong number of arguments for {cmd}; expected {self.argc}"
2324
)
2425

26+
self.globals = kwargs.get("globals", {})
2527
self.cmd = cmd
2628
self.args = args
2729
self.client_id = client_id
@@ -45,13 +47,13 @@ def ready(self):
4547
return True
4648

4749
@classmethod
48-
def from_cmd(cls, cmd_str, client_id=None):
50+
def from_cmd(cls, cmd_str, client_id=None, globals={}):
4951
"""Create a job from a command string, e.g. 'get_file file1.txt'."""
5052
cmd, *args = cmd_str.split()
5153
if cmd not in COMMANDS:
5254
raise ValueError(f"Unknown command: '{cmd}'")
5355
job_cls = COMMANDS[cmd]
54-
return job_cls(cmd, args, client_id)
56+
return job_cls(cmd, args, client_id, globals=globals)
5557

5658

5759
class SequentialJob(Job):
@@ -282,6 +284,39 @@ async def reboot_callback(op):
282284
return BytesIO(msg.encode("utf-8"))
283285

284286

287+
class RunPyJob(Job):
288+
"""A job to evaluate Python script on the device."""
289+
290+
argc = 1
291+
292+
def output(self):
293+
"""Eval or exec given Python and return the result."""
294+
expr = self.args[0]
295+
try:
296+
result = self.do_eval(expr)
297+
except SyntaxError: # Not an expression, try exec
298+
result = self.do_exec(expr)
299+
return BytesIO(result.encode("utf-8"))
300+
301+
def do_eval(self, expr):
302+
"""Evaluate a Python expression and return the result."""
303+
op = compile(expr, "<string>", "eval")
304+
result = eval(op, self.globals, None)
305+
return repr(result)
306+
307+
def do_exec(self, expr):
308+
"""Execute a Python statement and return the output."""
309+
out_buf = BytesIO()
310+
old_term = os.dupterm(out_buf)
311+
try:
312+
op = compile(expr, "<string>", "exec")
313+
exec(op, self.globals, None)
314+
result = out_buf.getvalue()
315+
finally:
316+
os.dupterm(old_term)
317+
return result
318+
319+
285320
# Map commands to associated job names
286321
COMMANDS = {
287322
"whoami": WhoAmIJob,
@@ -291,4 +326,5 @@ async def reboot_callback(op):
291326
"cp": PutFileJob,
292327
"ota": FirmwareUpdateJob,
293328
"reboot": RebootJob,
329+
"eval": RunPyJob,
294330
}

mqterm/terminal.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ class MqttTerminal:
4444
BUFLEN = PKTLEN * 2 # payload size for MQTT messages
4545

4646
def __init__(
47-
self, mqtt_client, topic_prefix=None, logger=logging.getLogger("mqterm")
47+
self,
48+
mqtt_client,
49+
topic_prefix=None,
50+
logger=logging.getLogger("mqterm"),
51+
globals={},
4852
):
4953
self.mqtt_client = mqtt_client
5054
self.topic_prefix = topic_prefix
@@ -55,6 +59,7 @@ def __init__(
5559
self.out_view = memoryview(self.out_buffer)
5660
self.logger = logger
5761
self.jobs = {}
62+
self.globals = globals
5863

5964
async def connect(self):
6065
"""Start processing messages in the input stream."""
@@ -102,7 +107,7 @@ async def update_job(self, client_id, seq, payload):
102107
self.logger.debug(f"Updated {job}, seq: {seq}")
103108
else:
104109
cmd = payload.decode("utf-8")
105-
job = Job.from_cmd(cmd, client_id=client_id)
110+
job = Job.from_cmd(cmd, client_id=client_id, globals=self.globals)
106111
self.jobs[client_id] = job
107112
self.logger.info(f"Created {job}")
108113

tests/test_jobs.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
PlatformInfoJob,
1515
PutFileJob,
1616
RebootJob,
17+
RunPyJob,
1718
WhoAmIJob,
1819
)
1920

@@ -230,3 +231,23 @@ def test_run_soft(self):
230231
job = RebootJob("reboot", ["soft"])
231232
output = job.output().read().decode("utf-8").strip()
232233
self.assertEqual(output, "Performing soft reboot")
234+
235+
236+
class TestRunPyJob(TestCase):
237+
def test_eval(self):
238+
"""RunPyJob should evaluate a Python expression and return result"""
239+
job = RunPyJob("eval", ["1 + 2"])
240+
output = job.output().read().decode("utf-8").strip()
241+
self.assertEqual(output, "3")
242+
243+
def test_globals(self):
244+
"""RunPyJob should use provided globals for evaluation"""
245+
job = RunPyJob("eval", ["x + y"], globals={"x": 1, "y": 2})
246+
output = job.output().read().decode("utf-8").strip()
247+
self.assertEqual(output, "3")
248+
249+
def test_exec(self):
250+
"""RunPyJob should execute a Python script and return output"""
251+
job = RunPyJob("eval", ['print("Hello, World!")'])
252+
output = job.output().read().decode("utf-8").strip()
253+
self.assertEqual(output, "Hello, World!")

0 commit comments

Comments
 (0)