From 3232eeb534b5ac1ab503f7f36254227b1cfba1f9 Mon Sep 17 00:00:00 2001
From: Josh Morman <jmorman@perspectalabs.com>
Date: Fri, 30 Aug 2019 09:47:42 -0400
Subject: grc: add python snippets to GRC

This feature adds the ability to insert arbitrary code into the python
flowgraph.  It gives a little more low-level flexibility for quickly
modifying flowgraphs and adding custom bits of code rather than having
to go and edit the generated py file

One example is synchronizing multiple USRP objects - sometimes you want
different sync than what is offered in the multi-usrp object, so you can
put a bit of code in the snippet block to do the custom synchronization
---
 grc/blocks/grc.tree.yml               |  1 +
 grc/blocks/snippet.block.yml          | 45 +++++++++++++++++++++++++++++++++++
 grc/core/FlowGraph.py                 | 41 +++++++++++++++++++++++++++++++
 grc/core/base.py                      |  1 +
 grc/core/blocks/block.py              |  4 ++++
 grc/core/generator/flow_graph.py.mako | 43 +++++++++++++++++++++++++++++++--
 grc/core/generator/top_block.py       |  2 +-
 grc/core/params/param.py              | 10 +++++---
 8 files changed, 141 insertions(+), 6 deletions(-)
 create mode 100644 grc/blocks/snippet.block.yml

(limited to 'grc')

diff --git a/grc/blocks/grc.tree.yml b/grc/blocks/grc.tree.yml
index f27fb6ae47..fc2fded9ab 100644
--- a/grc/blocks/grc.tree.yml
+++ b/grc/blocks/grc.tree.yml
@@ -13,6 +13,7 @@
   - epy_module
   - note
   - import
+  - snippet 
 - Variables:
   - variable
   - variable_struct
diff --git a/grc/blocks/snippet.block.yml b/grc/blocks/snippet.block.yml
new file mode 100644
index 0000000000..17d8da0ff8
--- /dev/null
+++ b/grc/blocks/snippet.block.yml
@@ -0,0 +1,45 @@
+id: snippet
+label: Python Snippet
+flags: [ python ]
+
+parameters:
+-   id: section
+    label: Section of Flowgraph
+    dtype: string
+    options: ['main_after_init', 'main_after_start', 'main_after_stop' ]
+    option_labels: ['Main - After Init', 'Main - After Start', 'Main - After Stop']
+-   id: priority
+    label: Priority
+    dtype: int
+    hide: ${'part' if priority <= 0 else 'none'}
+-   id: code
+    label: Code Snippet
+    dtype: _multiline
+
+templates:
+    var_make: ${code}
+
+documentation: |-
+    CAUTION: This is an ADVANCED feature and can lead to unintended consequences in the rendering of a flowgraph.  Use at your own risk.
+
+    Insert a snippet of Python code directly into the flowgraph at the end of the specified section. \
+    For each snippet a function is generated with the block name of the snippet (use GRC Show Block IDs option to modify).  These functions are\
+    then grouped into their respective sections in the rendered flowgraph.
+
+    The purpose of the python snippets is to be able to exercise features from within GRC that are not entirely supported by the block callbacks, \ 
+    methods and mechanisms to generate the code.  One example of this would be calling UHD timed commands before starting the flowgraph
+
+    Indents will be handled upon insertion into the python flowgraph
+
+    Example 1:
+    epy_mod_0.some_function(self.some_block.some_property)
+
+    Will place the function call in the generated .py file using the name of the appropriate embedded python block in the proper scope
+    The scope is relative to the blocks in the flowgraph, e.g. to reference a block, it should be identified as self.block
+
+    Example 2:
+    print('The flowgraph has been stopped')
+
+    With section selected as 'Main - After Stop', will place the print statement after the flowgraph has been stopped.
+
+file_format: 1
diff --git a/grc/core/FlowGraph.py b/grc/core/FlowGraph.py
index 21c3cdb59d..b0141f8705 100644
--- a/grc/core/FlowGraph.py
+++ b/grc/core/FlowGraph.py
@@ -81,6 +81,47 @@ class FlowGraph(Element):
         parameters = [b for b in self.iter_enabled_blocks() if b.key == 'parameter']
         return parameters
 
+    def get_snippets(self):
+        """
+        Get a set of all code snippets (Python) in this flow graph namespace.
+
+        Returns:
+            a list of code snippets
+        """
+        return [b for b in self.iter_enabled_blocks() if b.key == 'snippet']
+
+    def get_snippets_dict(self, section=None):
+        """
+        Get a dictionary of code snippet information for a particular section.
+
+        Args:
+            section: string specifier of section of snippets to return, section=None returns all
+
+        Returns:
+            a list of code snippets dicts
+        """
+        snippets = self.get_snippets()
+        if not snippets:
+            return []
+
+        output = []
+        for snip in snippets:
+            d ={}
+            sect = snip.params['section'].value
+            d['section'] = sect
+            d['priority'] = snip.params['priority'].value
+            d['lines'] = snip.params['code'].value.splitlines()
+            d['def'] = 'def snipfcn_{}(self):'.format(snip.name)
+            d['call'] = 'snipfcn_{}(tb)'.format(snip.name)
+            if not section or sect == section:
+                output.append(d)
+
+        # Sort by descending priority 
+        if section:
+            output = sorted(output, key=lambda x: x['priority'], reverse=True)
+
+        return output
+
     def get_monitors(self):
         """
         Get a list of all ControlPort monitors
diff --git a/grc/core/base.py b/grc/core/base.py
index dae9ea229b..a5de8837d6 100644
--- a/grc/core/base.py
+++ b/grc/core/base.py
@@ -139,6 +139,7 @@ class Element(object):
     is_param = False
     is_variable = False
     is_import = False
+    is_snippet = False
 
     def get_raw(self, name):
         descriptor = getattr(self.__class__, name, None)
diff --git a/grc/core/blocks/block.py b/grc/core/blocks/block.py
index ff1c8befc3..e51b730cdd 100644
--- a/grc/core/blocks/block.py
+++ b/grc/core/blocks/block.py
@@ -293,6 +293,10 @@ class Block(Element):
     def is_import(self):
         return self.key == 'import'
 
+    @lazy_property
+    def is_snippet(self):
+        return self.key == 'snippet'
+
     @property
     def comment(self):
         return self.params['comment'].value
diff --git a/grc/core/generator/flow_graph.py.mako b/grc/core/generator/flow_graph.py.mako
index 7e731d1a9d..3635c550db 100644
--- a/grc/core/generator/flow_graph.py.mako
+++ b/grc/core/generator/flow_graph.py.mako
@@ -42,6 +42,7 @@ if __name__ == '__main__':
 ##${imp.replace("  # grc-generated hier_block", "")}
 ${imp}
 % endfor
+
 ########################################################
 ##Create Class
 ##  Write the class declaration for a top or hier block.
@@ -219,6 +220,7 @@ gr.io_signaturev(${len(io_sigs)}, ${len(io_sigs)}, [${', '.join(size_strs)}])\
         ${ connection.rstrip() }
         % endfor
         % endif
+
 ########################################################
 ## QT sink close method reimplementation
 ########################################################
@@ -266,6 +268,32 @@ gr.io_signaturev(${len(io_sigs)}, ${len(io_sigs)}, [${', '.join(size_strs)}])\
         % endfor
         % endif
     % endfor
+\
+% for snip in flow_graph.get_snippets_dict():
+
+${indent(snip['def'])}
+% for line in snip['lines']:
+    ${indent(line)}
+% endfor
+% endfor
+\
+<%
+snippet_sections = ['main_after_init', 'main_after_start', 'main_after_stop']
+snippets = {}
+for section in snippet_sections:
+    snippets[section] = flow_graph.get_snippets_dict(section) 
+%>
+\
+%for section in snippet_sections:
+%if snippets[section]:
+
+def snippets_${section}(tb):
+    % for snip in snippets[section]:
+    ${indent(snip['call'])}
+    % endfor
+%endif
+%endfor
+
 ########################################################
 ##Create Main
 ##  For top block code, generate a main routine.
@@ -335,9 +363,11 @@ def main(top_block_cls=${class_name}, options=None):
     qapp = Qt.QApplication(sys.argv)
 
     tb = top_block_cls(${ ', '.join(params_eq_list) })
+    ${'snippets_main_after_init(tb)' if snippets['main_after_init'] else ''}
     % if flow_graph.get_option('run'):
     tb.start(${flow_graph.get_option('max_nouts') or ''})
     % endif
+    ${'snippets_main_after_start(tb)' if snippets['main_after_start'] else ''}
     % if flow_graph.get_option('qt_qss_theme'):
     tb.setStyleSheetFromFile("${ flow_graph.get_option('qt_qss_theme') }")
     % endif
@@ -356,6 +386,7 @@ def main(top_block_cls=${class_name}, options=None):
     def quitting():
         tb.stop()
         tb.wait()
+        ${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
     qapp.aboutToQuit.connect(quitting)
     % for m in monitors:
     % if m.params['en'].get_value() == 'True':
@@ -368,6 +399,7 @@ def main(top_block_cls=${class_name}, options=None):
     def killProc(signum, frame, tb):
         tb.stop()
         tb.wait()
+        ${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
         serverProc.terminate()
         serverProc.kill()
     time.sleep(1)
@@ -383,20 +415,23 @@ def main(top_block_cls=${class_name}, options=None):
                                url = "http://localhost:" + port + "/bokehgui")
         # Create Top Block instance
         tb = top_block_cls(doc)
+        ${'snippets_main_after_init(tb)' if snippets['main_after_init'] else ''}
         try:
             tb.start()
+            ${'snippets_main_after_start(tb)' if snippets['main_after_start'] else ''}
             signal.signal(signal.SIGTERM, functools.partial(killProc, tb=tb))
             session.loop_until_closed()
         finally:
             print("Exiting the simulation. Stopping Bokeh Server")
             tb.stop()
             tb.wait()
+            ${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
     finally:
         serverProc.terminate()
         serverProc.kill()
     % elif generate_options == 'no_gui':
     tb = top_block_cls(${ ', '.join(params_eq_list) })
-
+    ${'snippets_main_after_init(tb)' if snippets['main_after_init'] else ''}
     def sig_handler(sig=None, frame=None):
         % for m in monitors:
         % if m.params['en'].get_value() == 'True':
@@ -405,6 +440,7 @@ def main(top_block_cls=${class_name}, options=None):
         % endfor
         tb.stop()
         tb.wait()
+        ${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
         sys.exit(0)
 
     signal.signal(signal.SIGINT, sig_handler)
@@ -412,6 +448,7 @@ def main(top_block_cls=${class_name}, options=None):
 
     % if flow_graph.get_option('run_options') == 'prompt':
     tb.start(${ flow_graph.get_option('max_nouts') or '' })
+    ${'snippets_main_after_start(tb)' if snippets['main_after_start'] else ''}
     % for m in monitors:
     % if m.params['en'].get_value() == 'True':
     tb.${m.name}.start()
@@ -422,8 +459,10 @@ def main(top_block_cls=${class_name}, options=None):
     except EOFError:
         pass
     tb.stop()
+    ## ${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
     % elif flow_graph.get_option('run_options') == 'run':
     tb.start(${flow_graph.get_option('max_nouts') or ''})
+    ${'snippets_main_after_start(tb)' if snippets['main_after_start'] else ''}
     % for m in monitors:
     % if m.params['en'].get_value() == 'True':
     tb.${m.name}.start()
@@ -431,6 +470,7 @@ def main(top_block_cls=${class_name}, options=None):
     % endfor
     % endif
     tb.wait()
+    ${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
     % for m in monitors:
     % if m.params['en'].get_value() == 'True':
     tb.${m.name}.stop()
@@ -438,7 +478,6 @@ def main(top_block_cls=${class_name}, options=None):
     % endfor
     % endif
 
-
 if __name__ == '__main__':
     main()
 % endif
diff --git a/grc/core/generator/top_block.py b/grc/core/generator/top_block.py
index 27d2428888..bf7ebcc224 100644
--- a/grc/core/generator/top_block.py
+++ b/grc/core/generator/top_block.py
@@ -191,7 +191,7 @@ class TopBlockGenerator(object):
 
         blocks = [
             b for b in fg.blocks
-            if b.enabled and not (b.get_bypassed() or b.is_import or b in parameters or b.key == 'options')
+            if b.enabled and not (b.get_bypassed() or b.is_import or b.is_snippet or b in parameters or b.key == 'options')
         ]
 
         blocks = expr_utils.sort_objects(blocks, operator.attrgetter('name'), _get_block_sort_text)
diff --git a/grc/core/params/param.py b/grc/core/params/param.py
index 816588efe2..ef8d7df291 100644
--- a/grc/core/params/param.py
+++ b/grc/core/params/param.py
@@ -245,9 +245,13 @@ class Param(Element):
         elif dtype in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'):
             # Do not check if file/directory exists, that is a runtime issue
             try:
-                value = self.parent_flowgraph.evaluate(expr)
-                if not isinstance(value, str):
-                    raise Exception()
+                # Do not evaluate multiline strings (code snippets or comments)
+                if dtype not in ['_multiline','_multiline_python_external']:
+                    value = self.parent_flowgraph.evaluate(expr)
+                    if not isinstance(value, str):
+                        raise Exception()
+                else:
+                    value = str(expr)
             except Exception:
                 self._stringify_flag = True
                 value = str(expr)
-- 
cgit v1.2.3