Skip to content

Commit 3e190db

Browse files
committed
Add functionality to support dict unpacking in csp.node
1 parent ca6a981 commit 3e190db

3 files changed

Lines changed: 121 additions & 0 deletions

File tree

csp/impl/wiring/node_parser.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@
1414
from csp.impl.wiring.base_parser import BaseParser, CspParseError, _pythonic_depr_warning
1515

1616

17+
def _csp_output_kwargs(outputs_by_name, values):
18+
try:
19+
items = values.items()
20+
except AttributeError:
21+
raise TypeError(f"csp.output(**{values!r}) requires a dict-like value") from None
22+
for k, v in items:
23+
try:
24+
proxy = outputs_by_name[k]
25+
except KeyError:
26+
raise KeyError(f"unrecognized output '{k}'") from None
27+
proxy + v
28+
29+
1730
class _SingleProxyFuncArgResolver(object):
1831
class INVALID_VALUE:
1932
pass
@@ -73,6 +86,7 @@ class NodeParser(BaseParser):
7386
_CSP_ENGINE_START_TIME_FUNC = "_engine_start_time"
7487
_CSP_ENGINE_END_TIME_FUNC = "_engine_end_time"
7588
_CSP_ENGINE_STATS_FUNC = "_csp_engine_stats"
89+
_CSP_OUTPUT_KWARGS_FUNC = "_csp_output_kwargs"
7690

7791
_CSP_STOP_ENGINE_FUNC = "_csp_stop_engine"
7892
_CSP_IN_REALTIME_FUNC = "_csp_in_realtime"
@@ -83,6 +97,7 @@ class NodeParser(BaseParser):
8397
_CSP_STOP_ENGINE_FUNC: _cspimpl._csp_stop_engine,
8498
_CSP_ENGINE_STATS_FUNC: _cspimpl._csp_engine_stats,
8599
_CSP_IN_REALTIME_FUNC: _cspimpl._csp_in_realtime,
100+
_CSP_OUTPUT_KWARGS_FUNC: _csp_output_kwargs,
86101
}
87102

88103
_SPECIAL_BLOCKS_METH = {"alarms", "state", "start", "stop", "outputs"}
@@ -402,6 +417,10 @@ def _parse_output_or_return(self, node, is_return):
402417
node.lineno,
403418
)
404419
nodes = []
420+
for node_arg in node.args:
421+
if isinstance(node_arg, ast.Starred):
422+
raise CspParseError(f"{func_name} does not support * unpacking", node.lineno)
423+
405424
if len(node.args) == 1:
406425
if len(self._signature._outputs) > 1 and self._signature.output(0).name is not None:
407426
raise CspParseError(
@@ -464,6 +483,20 @@ def _parse_output_or_return(self, node, is_return):
464483
self._returned_outputs.add(node.args[0].id)
465484

466485
for arg in node.keywords:
486+
if arg.arg is None:
487+
# A **expr unpack, resolved at runtime, can't statically verify
488+
# which outputs it covers, so assume it may cover all of them.
489+
self._returned_outputs.update(o.name for o in self._signature._outputs if o.name is not None)
490+
nodes.append(
491+
ast.Call(
492+
func=ast.Name(id=self._CSP_OUTPUT_KWARGS_FUNC, ctx=ast.Load()),
493+
args=[self._build_outputs_by_name_dict(node), arg.value],
494+
keywords=[],
495+
lineno=node.lineno,
496+
end_lineno=node.end_lineno,
497+
)
498+
)
499+
continue
467500
if self._signature.output(arg.arg, True) is None:
468501
raise CspParseError(f"unrecognized output '{arg.arg}'", node.lineno)
469502
output = self._signature.output(arg.arg)
@@ -505,6 +538,21 @@ def _parse_output_or_return(self, node, is_return):
505538

506539
return res
507540

541+
def _build_outputs_by_name_dict(self, node):
542+
keys = []
543+
values = []
544+
for output in self._signature._outputs:
545+
if output.name is None:
546+
continue
547+
keys.append(ast.Constant(value=output.name))
548+
values.append(self._ts_outproxy_expr(output.name))
549+
return ast.Dict(
550+
keys=keys,
551+
values=values,
552+
lineno=node.lineno,
553+
end_lineno=node.end_lineno,
554+
)
555+
508556
def _parse_output(self, node):
509557
return self._parse_output_or_return(node=node, is_return=False)
510558

csp/tests/test_engine.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,71 @@ def count(x: ts[int]) -> ts[int]:
332332
result = csp.run(count, x, starttime=datetime(2020, 2, 7, 9), endtime=timedelta(seconds=10))[0]
333333
self.assertEqual([v[1] for v in result], list(x * 2 for x in range(1, 11)))
334334

335+
def test_csp_output_dict_unpack(self):
336+
@csp.node
337+
def foo(x: ts[bool]) -> csp.Outputs(a=ts[int], b=ts[int]):
338+
if csp.ticked(x):
339+
values = {"a": 1, "b": 2}
340+
csp.output(**values)
341+
342+
result = csp.run(foo, csp.const(True), starttime=datetime(2020, 1, 1), endtime=timedelta(seconds=1))
343+
self.assertEqual(result["a"][0][1], 1)
344+
self.assertEqual(result["b"][0][1], 2)
345+
346+
def test_csp_output_dict_unpack_mixed(self):
347+
@csp.node
348+
def foo(x: ts[bool]) -> csp.Outputs(a=ts[int], b=ts[int]):
349+
if csp.ticked(x):
350+
csp.output(a=1, **{"b": 2})
351+
352+
result = csp.run(foo, csp.const(True), starttime=datetime(2020, 1, 1), endtime=timedelta(seconds=1))
353+
self.assertEqual(result["a"][0][1], 1)
354+
self.assertEqual(result["b"][0][1], 2)
355+
356+
def test_csp_output_dict_unpack_multiple(self):
357+
@csp.node
358+
def foo(x: ts[bool]) -> csp.Outputs(a=ts[int], b=ts[int]):
359+
if csp.ticked(x):
360+
csp.output(**{"a": 1}, **{"b": 2})
361+
362+
result = csp.run(foo, csp.const(True), starttime=datetime(2020, 1, 1), endtime=timedelta(seconds=1))
363+
self.assertEqual(result["a"][0][1], 1)
364+
self.assertEqual(result["b"][0][1], 2)
365+
366+
def test_csp_output_dict_unpack_unknown_key(self):
367+
@csp.node
368+
def foo(x: ts[bool]) -> csp.Outputs(a=ts[int]):
369+
if csp.ticked(x):
370+
csp.output(**{"bogus": 1})
371+
372+
with self.assertRaisesRegex(KeyError, "unrecognized output 'bogus'"):
373+
csp.run(foo, csp.const(True), starttime=datetime(2020, 1, 1), endtime=timedelta(seconds=1))
374+
375+
def test_csp_output_dict_unpack_non_dict(self):
376+
@csp.node
377+
def foo(x: ts[bool]) -> csp.Outputs(a=ts[int], b=ts[int]):
378+
if csp.ticked(x):
379+
values = [1, 2] # not dict-like
380+
csp.output(**values)
381+
382+
with self.assertRaisesRegex(TypeError, "requires a dict-like value"):
383+
csp.run(foo, csp.const(True), starttime=datetime(2020, 1, 1), endtime=timedelta(seconds=1))
384+
385+
def test_csp_output_dict_unpack_basket(self):
386+
@csp.node
387+
def foo(x: ts[bool]) -> csp.Outputs(
388+
a=ts[int],
389+
b=csp.OutputBasket(Dict[str, ts[int]], shape=["k1", "k2"]),
390+
):
391+
if csp.ticked(x):
392+
values = {"a": 1, "b": {"k1": 10, "k2": 20}}
393+
csp.output(**values)
394+
395+
result = csp.run(foo, csp.const(True), starttime=datetime(2020, 1, 1), endtime=timedelta(seconds=1))
396+
self.assertEqual(result["a"][0][1], 1)
397+
self.assertEqual(result["b[k1]"][0][1], 10)
398+
self.assertEqual(result["b[k2]"][0][1], 20)
399+
335400
def test_single_csp_numpy_output(self):
336401
@csp.node
337402
def count(x: ts[int]) -> ts[int]:

csp/tests/test_parsing.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,14 @@ def foo(x: ts[int]):
357357
def foo(x: ts[int]) -> Outputs(x=ts[bool]):
358358
__return__(x[1], 7)
359359

360+
with self.assertRaisesRegex(CspParseError, "csp.output does not support \\* unpacking"):
361+
362+
@csp.node
363+
def foo(x: ts[int]):
364+
__outputs__(z=ts[bool])
365+
args = (7,)
366+
csp.output(*args)
367+
360368
with self.assertRaisesRegex(CspParseError, "unrecognized output 'x'"):
361369

362370
@csp.node

0 commit comments

Comments
 (0)