summaryrefslogtreecommitdiff
path: root/grc/core
diff options
context:
space:
mode:
Diffstat (limited to 'grc/core')
-rw-r--r--grc/core/Block.py784
-rw-r--r--grc/core/Config.py9
-rw-r--r--grc/core/Connection.py113
-rw-r--r--grc/core/Constants.py15
-rw-r--r--grc/core/FlowGraph.py289
-rw-r--r--grc/core/Param.py481
-rw-r--r--grc/core/Platform.py341
-rw-r--r--grc/core/Port.py391
-rw-r--r--grc/core/base.py (renamed from grc/core/Element.py)58
-rw-r--r--grc/core/block.dtd69
-rw-r--r--grc/core/blocks/__init__.py (renamed from grc/core/Element.pyi)28
-rw-r--r--grc/core/blocks/_build.py69
-rw-r--r--grc/core/blocks/_flags.py39
-rw-r--r--grc/core/blocks/_templates.py77
-rw-r--r--grc/core/blocks/block.py416
-rw-r--r--grc/core/blocks/dummy.py54
-rw-r--r--grc/core/blocks/embedded_python.py242
-rw-r--r--grc/core/blocks/virtual.py76
-rw-r--r--grc/core/domain.dtd35
-rw-r--r--grc/core/errors.py30
-rw-r--r--grc/core/generator/FlowGraphProxy.py54
-rw-r--r--grc/core/generator/Generator.py354
-rw-r--r--grc/core/generator/flow_graph.py.mako352
-rw-r--r--grc/core/generator/flow_graph.tmpl420
-rw-r--r--grc/core/generator/hier_block.py196
-rw-r--r--grc/core/generator/top_block.py285
-rw-r--r--grc/core/io/__init__.py16
-rw-r--r--grc/core/io/yaml.py91
-rw-r--r--grc/core/platform.py430
-rw-r--r--grc/core/ports/__init__.py (renamed from grc/core/block_tree.dtd)19
-rw-r--r--grc/core/ports/_virtual_connections.py126
-rw-r--r--grc/core/ports/clone.py38
-rw-r--r--grc/core/ports/port.py207
-rw-r--r--grc/core/schema_checker/__init__.py5
-rw-r--r--grc/core/schema_checker/block.py57
-rw-r--r--grc/core/schema_checker/domain.py16
-rw-r--r--grc/core/schema_checker/flow_graph.py23
-rw-r--r--grc/core/schema_checker/utils.py27
-rw-r--r--grc/core/schema_checker/validator.py102
-rw-r--r--grc/core/utils/__init__.py8
-rw-r--r--grc/core/utils/backports/__init__.py25
-rw-r--r--grc/core/utils/backports/chainmap.py106
-rw-r--r--grc/core/utils/backports/shlex.py (renamed from grc/core/utils/shlex.py)0
-rw-r--r--grc/core/utils/descriptors/__init__.py26
-rw-r--r--grc/core/utils/descriptors/_lazy.py39
-rw-r--r--grc/core/utils/descriptors/evaluated.py112
-rw-r--r--grc/core/utils/expr_utils.py158
-rw-r--r--grc/core/utils/extract_docs.py8
-rw-r--r--grc/core/utils/flow_graph_complexity.py (renamed from grc/core/utils/_complexity.py)10
49 files changed, 3840 insertions, 3086 deletions
diff --git a/grc/core/Block.py b/grc/core/Block.py
deleted file mode 100644
index 087815b941..0000000000
--- a/grc/core/Block.py
+++ /dev/null
@@ -1,784 +0,0 @@
-"""
-Copyright 2008-2015 Free Software Foundation, Inc.
-This file is part of GNU Radio
-
-GNU Radio Companion is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-GNU Radio Companion is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-"""
-
-from __future__ import absolute_import
-
-import collections
-import itertools
-import ast
-
-import six
-from six.moves import map, range
-
-from Cheetah.Template import Template
-
-from . import utils
-
-from . Constants import (
- BLOCK_FLAG_NEED_QT_GUI,
- ADVANCED_PARAM_TAB,
- BLOCK_FLAG_THROTTLE, BLOCK_FLAG_DISABLE_BYPASS,
- BLOCK_FLAG_DEPRECATED,
-)
-from . Element import Element, lazy_property
-
-
-def _get_elem(iterable, key):
- items = list(iterable)
- for item in items:
- if item.key == key:
- return item
- return ValueError('Key "{}" not found in {}.'.format(key, items))
-
-
-class Block(Element):
-
- is_block = True
-
- STATE_LABELS = ['disabled', 'enabled', 'bypassed']
-
- def __init__(self, parent, key, name, **n):
- """Make a new block from nested data."""
- super(Block, self).__init__(parent)
-
- self.key = key
- self.name = name
- self.category = [cat.strip() for cat in n.get('category', '').split('/') if cat.strip()]
- self.flags = n.get('flags', '')
- self._doc = n.get('doc', '').strip('\n').replace('\\\n', '')
-
- # Backwards compatibility
- if n.get('throttle') and BLOCK_FLAG_THROTTLE not in self.flags:
- self.flags += BLOCK_FLAG_THROTTLE
-
- self._imports = [i.strip() for i in n.get('import', [])]
- self._make = n.get('make')
- self._var_make = n.get('var_make')
- self._var_value = n.get('var_value', '$value')
- self._checks = n.get('check', [])
- self._callbacks = n.get('callback', [])
-
- self._grc_source = n.get('grc_source', '')
- self.block_wrapper_path = n.get('block_wrapper_path')
-
- # Virtual source/sink and pad source/sink blocks are
- # indistinguishable from normal GR blocks. Make explicit
- # checks for them here since they have no work function or
- # buffers to manage.
- self.is_virtual_or_pad = self.key in (
- "virtual_source", "virtual_sink", "pad_source", "pad_sink")
- self.is_variable = self.key.startswith('variable')
- self.is_import = (self.key == 'import')
-
- # Disable blocks that are virtual/pads or variables
- if self.is_virtual_or_pad or self.is_variable:
- self.flags += BLOCK_FLAG_DISABLE_BYPASS
-
- params_n = n.get('param', [])
- sources_n = n.get('source', [])
- sinks_n = n.get('sink', [])
-
- # Get list of param tabs
- self.params = collections.OrderedDict()
- self._init_params(
- params_n=params_n,
- has_sinks=len(sinks_n),
- has_sources=len(sources_n)
- )
-
- self.sources = self._init_ports(sources_n, direction='source')
- self.sinks = self._init_ports(sinks_n, direction='sink')
- self.active_sources = [] # on rewrite
- self.active_sinks = [] # on rewrite
-
- self.states = {'_enabled': True}
-
- self._init_bus_ports(n)
-
- def _init_params(self, params_n, has_sources, has_sinks):
- param_factory = self.parent_platform.get_new_param
-
- def add_param(key, **kwargs):
- self.params[key] = param_factory(self, key=key, **kwargs)
-
- add_param(key='id', name='ID', type='id')
-
- if not (self.is_virtual_or_pad or self.is_variable or self.key == 'options'):
- add_param(key='alias', name='Block Alias', type='string',
- hide='part', tab=ADVANCED_PARAM_TAB)
-
- if not self.is_virtual_or_pad and (has_sources or has_sinks):
- add_param(key='affinity', name='Core Affinity', type='int_vector',
- hide='part', tab=ADVANCED_PARAM_TAB)
-
- if not self.is_virtual_or_pad and has_sources:
- add_param(key='minoutbuf', name='Min Output Buffer', type='int',
- hide='part', value='0', tab=ADVANCED_PARAM_TAB)
- add_param(key='maxoutbuf', name='Max Output Buffer', type='int',
- hide='part', value='0', tab=ADVANCED_PARAM_TAB)
-
- base_params_n = {}
- for param_n in params_n:
- key = param_n['key']
- if key in self.params:
- raise Exception('Key "{}" already exists in params'.format(key))
-
- base_key = param_n.get('base_key', None)
- param_n_ext = base_params_n.get(base_key, {}).copy()
- param_n_ext.update(param_n)
- self.params[key] = param_factory(self, **param_n_ext)
- base_params_n[key] = param_n_ext
-
- add_param(key='comment', name='Comment', type='_multiline', hide='part',
- value='', tab=ADVANCED_PARAM_TAB)
-
- def _init_ports(self, ports_n, direction):
- port_factory = self.parent_platform.get_new_port
- ports = []
- port_keys = set()
- stream_port_keys = itertools.count()
- for i, port_n in enumerate(ports_n):
- port_n.setdefault('key', str(next(stream_port_keys)))
- port = port_factory(parent=self, direction=direction, **port_n)
- key = port.key
- if key in port_keys:
- raise Exception('Key "{}" already exists in {}'.format(key, direction))
- port_keys.add(key)
- ports.append(port)
- return ports
-
- ##############################################
- # validation and rewrite
- ##############################################
- def rewrite(self):
- """
- Add and remove ports to adjust for the nports.
- """
- Element.rewrite(self)
-
- def rekey(ports):
- """Renumber non-message/message ports"""
- domain_specific_port_index = collections.defaultdict(int)
- for port in [p for p in ports if p.key.isdigit()]:
- domain = port.domain
- port.key = str(domain_specific_port_index[domain])
- domain_specific_port_index[domain] += 1
-
- # Adjust nports
- for ports in (self.sources, self.sinks):
- self._rewrite_nports(ports)
- self.back_ofthe_bus(ports)
- rekey(ports)
-
- self._rewrite_bus_ports()
-
- # disconnect hidden ports
- for port in itertools.chain(self.sources, self.sinks):
- if port.get_hide():
- for connection in port.get_connections():
- self.parent_flowgraph.remove_element(connection)
-
-
- self.active_sources = [p for p in self.get_sources_gui() if not p.get_hide()]
- self.active_sinks = [p for p in self.get_sinks_gui() if not p.get_hide()]
-
- def _rewrite_nports(self, ports):
- for port in ports:
- if port.is_clone: # Not a master port and no left-over clones
- continue
- nports = port.get_nports() or 1
- for clone in port.clones[nports-1:]:
- # Remove excess connections
- for connection in clone.get_connections():
- self.parent_flowgraph.remove_element(connection)
- port.remove_clone(clone)
- ports.remove(clone)
- # Add more cloned ports
- for j in range(1 + len(port.clones), nports):
- clone = port.add_clone()
- ports.insert(ports.index(port) + j, clone)
-
- def validate(self):
- """
- Validate this block.
- Call the base class validate.
- Evaluate the checks: each check must evaluate to True.
- """
- Element.validate(self)
- self._run_checks()
- self._validate_generate_mode_compat()
- self._validate_var_value()
-
- def _run_checks(self):
- """Evaluate the checks"""
- for check in self._checks:
- check_res = self.resolve_dependencies(check)
- try:
- if not self.parent.evaluate(check_res):
- self.add_error_message('Check "{}" failed.'.format(check))
- except:
- self.add_error_message('Check "{}" did not evaluate.'.format(check))
-
- def _validate_generate_mode_compat(self):
- """check if this is a GUI block and matches the selected generate option"""
- current_generate_option = self.parent.get_option('generate_options')
-
- def check_generate_mode(label, flag, valid_options):
- block_requires_mode = (
- flag in self.flags or self.name.upper().startswith(label)
- )
- if block_requires_mode and current_generate_option not in valid_options:
- self.add_error_message("Can't generate this block in mode: {} ".format(
- repr(current_generate_option)))
-
- check_generate_mode('QT GUI', BLOCK_FLAG_NEED_QT_GUI, ('qt_gui', 'hb_qt_gui'))
-
- def _validate_var_value(self):
- """or variables check the value (only if var_value is used)"""
- if self.is_variable and self._var_value != '$value':
- value = self._var_value
- try:
- value = self.get_var_value()
- self.parent.evaluate(value)
- except Exception as err:
- self.add_error_message('Value "{}" cannot be evaluated:\n{}'.format(value, err))
-
- ##############################################
- # props
- ##############################################
-
- @lazy_property
- def is_throtteling(self):
- return BLOCK_FLAG_THROTTLE in self.flags
-
- @lazy_property
- def is_deprecated(self):
- return BLOCK_FLAG_DEPRECATED in self.flags
-
- @property
- def documentation(self):
- documentation = self.parent_platform.block_docstrings.get(self.key, {})
- from_xml = self._doc.strip()
- if from_xml:
- documentation[''] = from_xml
- return documentation
-
- @property
- def comment(self):
- return self.params['comment'].get_value()
-
- @property
- def state(self):
- """Gets the block's current state."""
- try:
- return self.STATE_LABELS[int(self.states['_enabled'])]
- except ValueError:
- return 'enabled'
-
- @state.setter
- def state(self, value):
- """Sets the state for the block."""
- try:
- encoded = self.STATE_LABELS.index(value)
- except ValueError:
- encoded = 1
- self.states['_enabled'] = encoded
-
- # Enable/Disable Aliases
- @property
- def enabled(self):
- """Get the enabled state of the block"""
- return self.state != 'disabled'
-
- ##############################################
- # Getters (old)
- ##############################################
-
- def get_imports(self, raw=False):
- """
- Resolve all import statements.
- Split each import statement at newlines.
- Combine all import statements into a list.
- Filter empty imports.
-
- Returns:
- a list of import statements
- """
- if raw:
- return self._imports
- return [i for i in sum((self.resolve_dependencies(i).split('\n')
- for i in self._imports), []) if i]
-
- def get_make(self, raw=False):
- if raw:
- return self._make
- return self.resolve_dependencies(self._make)
-
- def get_var_make(self):
- return self.resolve_dependencies(self._var_make)
-
- def get_var_value(self):
- return self.resolve_dependencies(self._var_value)
-
- def get_callbacks(self):
- """
- Get a list of function callbacks for this block.
-
- Returns:
- a list of strings
- """
- def make_callback(callback):
- callback = self.resolve_dependencies(callback)
- if 'self.' in callback:
- return callback
- return 'self.{}.{}'.format(self.get_id(), callback)
- return [make_callback(c) for c in self._callbacks]
-
- def is_virtual_sink(self):
- return self.key == 'virtual_sink'
-
- def is_virtual_source(self):
- return self.key == 'virtual_source'
-
- # Block bypassing
- def get_bypassed(self):
- """
- Check if the block is bypassed
- """
- return self.state == 'bypassed'
-
- def set_bypassed(self):
- """
- Bypass the block
-
- Returns:
- True if block chagnes state
- """
- if self.state != 'bypassed' and self.can_bypass():
- self.state = 'bypassed'
- return True
- return False
-
- def can_bypass(self):
- """ Check the number of sinks and sources and see if this block can be bypassed """
- # Check to make sure this is a single path block
- # Could possibly support 1 to many blocks
- if len(self.sources) != 1 or len(self.sinks) != 1:
- return False
- if not (self.sources[0].get_type() == self.sinks[0].get_type()):
- return False
- if BLOCK_FLAG_DISABLE_BYPASS in self.flags:
- return False
- return True
-
- def __str__(self):
- return 'Block - {} - {}({})'.format(self.get_id(), self.name, self.key)
-
- def get_id(self):
- return self.params['id'].get_value()
-
- def get_ports(self):
- return self.sources + self.sinks
-
- def get_ports_gui(self):
- return self.get_sources_gui() + self.get_sinks_gui()
-
- def active_ports(self):
- return itertools.chain(self.active_sources, self.active_sinks)
-
- def get_children(self):
- return self.get_ports() + list(self.params.values())
-
- def get_children_gui(self):
- return self.get_ports_gui() + self.params.values()
-
- ##############################################
- # Access
- ##############################################
-
- def get_param(self, key):
- return self.params[key]
-
- def get_sink(self, key):
- return _get_elem(self.sinks, key)
-
- def get_sinks_gui(self):
- return self.filter_bus_port(self.sinks)
-
- def get_source(self, key):
- return _get_elem(self.sources, key)
-
- def get_sources_gui(self):
- return self.filter_bus_port(self.sources)
-
- def get_connections(self):
- return sum((port.get_connections() for port in self.get_ports()), [])
-
- ##############################################
- # Resolve
- ##############################################
- def resolve_dependencies(self, tmpl):
- """
- Resolve a paramater dependency with cheetah templates.
-
- Args:
- tmpl: the string with dependencies
-
- Returns:
- the resolved value
- """
- tmpl = str(tmpl)
- if '$' not in tmpl:
- return tmpl
- # TODO: cache that
- n = {key: param.template_arg for key, param in six.iteritems(self.params)}
- try:
- return str(Template(tmpl, n))
- except Exception as err:
- return "Template error: {}\n {}".format(tmpl, err)
-
- ##############################################
- # Import/Export Methods
- ##############################################
- def export_data(self):
- """
- Export this block's params to nested data.
-
- Returns:
- a nested data odict
- """
- n = collections.OrderedDict()
- n['key'] = self.key
-
- params = (param.export_data() for param in six.itervalues(self.params))
- states = (collections.OrderedDict([('key', key), ('value', repr(value))])
- for key, value in six.iteritems(self.states))
- n['param'] = sorted(itertools.chain(states, params), key=lambda p: p['key'])
-
- if any('bus' in a.get_type() for a in self.sinks):
- n['bus_sink'] = '1'
- if any('bus' in a.get_type() for a in self.sources):
- n['bus_source'] = '1'
- return n
-
- def import_data(self, n):
- """
- Import this block's params from nested data.
- Any param keys that do not exist will be ignored.
- Since params can be dynamically created based another param,
- call rewrite, and repeat the load until the params stick.
- This call to rewrite will also create any dynamic ports
- that are needed for the connections creation phase.
-
- Args:
- n: the nested data odict
- """
- param_data = {p['key']: p['value'] for p in n.get('param', [])}
-
- for key in self.states:
- try:
- self.states[key] = ast.literal_eval(param_data.pop(key))
- except (KeyError, SyntaxError, ValueError):
- pass
-
- def get_hash():
- return hash(tuple(hash(v) for v in self.params.values()))
-
- pre_rewrite_hash = -1
- while pre_rewrite_hash != get_hash():
- for key, value in six.iteritems(param_data):
- try:
- self.params[key].set_value(value)
- except KeyError:
- continue
- # Store hash and call rewrite
- pre_rewrite_hash = get_hash()
- self.rewrite()
-
- self._import_bus_stuff(n)
-
- ##############################################
- # Bus ports stuff
- ##############################################
-
- def get_bus_structure(self, direction):
- bus_structure = self.resolve_dependencies(self._bus_structure[direction])
- if not bus_structure:
- return
- try:
- return self.parent_flowgraph.evaluate(bus_structure)
- except:
- return
-
- @staticmethod
- def back_ofthe_bus(portlist):
- portlist.sort(key=lambda p: p._type == 'bus')
-
- @staticmethod
- def filter_bus_port(ports):
- buslist = [p for p in ports if p._type == 'bus']
- return buslist or ports
-
- def _import_bus_stuff(self, n):
- bus_sinks = n.get('bus_sink', [])
- if len(bus_sinks) > 0 and not self._bussify_sink:
- self.bussify('sink')
- elif len(bus_sinks) > 0:
- self.bussify('sink')
- self.bussify('sink')
- bus_sources = n.get('bus_source', [])
- if len(bus_sources) > 0 and not self._bussify_source:
- self.bussify('source')
- elif len(bus_sources) > 0:
- self.bussify('source')
- self.bussify('source')
-
- def form_bus_structure(self, direc):
- ports = self.sources if direc == 'source' else self.sinks
- struct = self.get_bus_structure(direc)
-
- if not struct:
- struct = [list(range(len(ports)))]
-
- elif any(isinstance(p.get_nports(), int) for p in ports):
- last = 0
- structlet = []
- for port in ports:
- nports = port.get_nports()
- if not isinstance(nports, int):
- continue
- structlet.extend(a + last for a in range(nports))
- last += nports
- struct = [structlet]
-
- self.current_bus_structure[direc] = struct
- return struct
-
- def bussify(self, direc):
- ports = self.sources if direc == 'source' else self.sinks
-
- for elt in ports:
- for connect in elt.get_connections():
- self.parent.remove_element(connect)
-
- if ports and all('bus' != p.get_type() for p in ports):
- struct = self.current_bus_structure[direc] = self.form_bus_structure(direc)
- n = {'type': 'bus'}
- if ports[0].get_nports():
- n['nports'] = '1'
-
- for i, structlet in enumerate(struct):
- name = 'bus{}#{}'.format(i, len(structlet))
- port = self.parent_platform.get_new_port(
- self, direction=direc, key=str(len(ports)), name=name, **n)
- ports.append(port)
- elif any('bus' == p.get_type() for p in ports):
- get_p_gui = self.get_sources_gui if direc == 'source' else self.get_sinks_gui
- for elt in get_p_gui():
- ports.remove(elt)
- self.current_bus_structure[direc] = ''
-
- def _init_bus_ports(self, n):
- self.current_bus_structure = {'source': '', 'sink': ''}
- self._bus_structure = {'source': n.get('bus_structure_source', ''),
- 'sink': n.get('bus_structure_sink', '')}
- self._bussify_sink = n.get('bus_sink')
- self._bussify_source = n.get('bus_source')
- if self._bussify_sink:
- self.bussify('sink')
- if self._bussify_source:
- self.bussify('source')
-
- def _rewrite_bus_ports(self):
- return # fixme: probably broken
-
- def doit(ports, ports_gui, direc):
- if not self.current_bus_structure[direc]:
- return
-
- bus_structure = self.form_bus_structure(direc)
- for port in ports_gui[len(bus_structure):]:
- for connect in port.get_connections():
- self.parent_flowgraph.remove_element(connect)
- ports.remove(port)
-
- port_factory = self.parent_platform.get_new_port
-
- if len(ports_gui) < len(bus_structure):
- for i in range(len(ports_gui), len(bus_structure)):
- port = port_factory(self, direction=direc, key=str(1 + i),
- name='bus', type='bus')
- ports.append(port)
-
- doit(self.sources, self.get_sources_gui(), 'source')
- doit(self.sinks, self.get_sinks_gui(), 'sink')
-
- if 'bus' in [a.get_type() for a in self.get_sources_gui()]:
- for i in range(len(self.get_sources_gui())):
- if not self.get_sources_gui()[i].get_connections():
- continue
- source = self.get_sources_gui()[i]
- sink = []
-
- for j in range(len(source.get_connections())):
- sink.append(source.get_connections()[j].sink_port)
- for elt in source.get_connections():
- self.parent_flowgraph.remove_element(elt)
- for j in sink:
- self.parent_flowgraph.connect(source, j)
-
-
-class EPyBlock(Block):
-
- def __init__(self, flow_graph, **n):
- super(EPyBlock, self).__init__(flow_graph, **n)
- self._epy_source_hash = -1 # for epy blocks
- self._epy_reload_error = None
-
- def rewrite(self):
- Element.rewrite(self)
-
- param_blk = self.params['_io_cache']
- param_src = self.params['_source_code']
-
- src = param_src.get_value()
- src_hash = hash((self.get_id(), src))
- if src_hash == self._epy_source_hash:
- return
-
- try:
- blk_io = utils.epy_block_io.extract(src)
-
- except Exception as e:
- self._epy_reload_error = ValueError(str(e))
- try: # Load last working block io
- blk_io_args = eval(param_blk.get_value())
- if len(blk_io_args) == 6:
- blk_io_args += ([],) # add empty callbacks
- blk_io = utils.epy_block_io.BlockIO(*blk_io_args)
- except Exception:
- return
- else:
- self._epy_reload_error = None # Clear previous errors
- param_blk.set_value(repr(tuple(blk_io)))
-
- # print "Rewriting embedded python block {!r}".format(self.get_id())
-
- self._epy_source_hash = src_hash
- self.name = blk_io.name or blk_io.cls
- self._doc = blk_io.doc
- self._imports[0] = 'import ' + self.get_id()
- self._make = '{0}.{1}({2})'.format(self.get_id(), blk_io.cls, ', '.join(
- '{0}=${{ {0} }}'.format(key) for key, _ in blk_io.params))
- self._callbacks = ['{0} = ${{ {0} }}'.format(attr) for attr in blk_io.callbacks]
- self._update_params(blk_io.params)
- self._update_ports('in', self.sinks, blk_io.sinks, 'sink')
- self._update_ports('out', self.sources, blk_io.sources, 'source')
-
- super(EPyBlock, self).rewrite()
-
- def _update_params(self, params_in_src):
- param_factory = self.parent_platform.get_new_param
- params = {}
- for param in list(self.params):
- if hasattr(param, '__epy_param__'):
- params[param.key] = param
- del self.params[param.key]
-
- for key, value in params_in_src:
- try:
- param = params[key]
- if param.default == param.value:
- param.set_value(value)
- param.default = str(value)
- except KeyError: # need to make a new param
- param = param_factory(
- parent=self, key=key, type='raw', value=value,
- name=key.replace('_', ' ').title(),
- )
- setattr(param, '__epy_param__', True)
- self.params[key] = param
-
- def _update_ports(self, label, ports, port_specs, direction):
- port_factory = self.parent_platform.get_new_port
- ports_to_remove = list(ports)
- iter_ports = iter(ports)
- ports_new = []
- port_current = next(iter_ports, None)
- for key, port_type, vlen in port_specs:
- reuse_port = (
- port_current is not None and
- port_current.get_type() == port_type and
- port_current.get_vlen() == vlen and
- (key.isdigit() or port_current.key == key)
- )
- if reuse_port:
- ports_to_remove.remove(port_current)
- port, port_current = port_current, next(iter_ports, None)
- else:
- n = dict(name=label + str(key), type=port_type, key=key)
- if port_type == 'message':
- n['name'] = key
- n['optional'] = '1'
- if vlen > 1:
- n['vlen'] = str(vlen)
- port = port_factory(self, direction=direction, **n)
- ports_new.append(port)
- # replace old port list with new one
- del ports[:]
- ports.extend(ports_new)
- # remove excess port connections
- for port in ports_to_remove:
- for connection in port.get_connections():
- self.parent_flowgraph.remove_element(connection)
-
- def validate(self):
- super(EPyBlock, self).validate()
- if self._epy_reload_error:
- self.params['_source_code'].add_error_message(str(self._epy_reload_error))
-
-
-class DummyBlock(Block):
-
- is_dummy_block = True
- build_in_param_keys = 'id alias affinity minoutbuf maxoutbuf comment'
-
- def __init__(self, parent, key, missing_key, params_n):
- super(DummyBlock, self).__init__(parent=parent, key=missing_key, name='Missing Block')
- param_factory = self.parent_platform.get_new_param
- for param_n in params_n:
- key = param_n['key']
- self.params.setdefault(key, param_factory(self, key=key, name=key, type='string'))
-
- def is_valid(self):
- return False
-
- @property
- def enabled(self):
- return False
-
- def add_missing_port(self, key, dir):
- port = self.parent_platform.get_new_port(
- parent=self, direction=dir, key=key, name='?', type='',
- )
- if port.is_source:
- self.sources.append(port)
- else:
- self.sinks.append(port)
- return port
diff --git a/grc/core/Config.py b/grc/core/Config.py
index cc199a348f..eb53e1751d 100644
--- a/grc/core/Config.py
+++ b/grc/core/Config.py
@@ -1,5 +1,4 @@
-"""
-Copyright 2016 Free Software Foundation, Inc.
+"""Copyright 2016 Free Software Foundation, Inc.
This file is part of GNU Radio
GNU Radio Companion is free software; you can redistribute it and/or
@@ -32,6 +31,8 @@ class Config(object):
hier_block_lib_dir = os.environ.get('GRC_HIER_PATH', Constants.DEFAULT_HIER_BLOCK_LIB_DIR)
+ yml_block_cache = os.path.expanduser('~/.cache/grc_gnuradio') # FIXME: remove this as soon as converter is stable
+
def __init__(self, version, version_parts=None, name=None, prefs=None):
self._gr_prefs = prefs if prefs else DummyPrefs()
self.version = version
@@ -39,6 +40,9 @@ class Config(object):
if name:
self.name = name
+ if not os.path.exists(self.yml_block_cache):
+ os.mkdir(self.yml_block_cache)
+
@property
def block_paths(self):
path_list_sep = {'/': ':', '\\': ';'}[os.path.sep]
@@ -46,6 +50,7 @@ class Config(object):
paths_sources = (
self.hier_block_lib_dir,
os.environ.get('GRC_BLOCKS_PATH', ''),
+ self.yml_block_cache,
self._gr_prefs.get_string('grc', 'local_blocks_path', ''),
self._gr_prefs.get_string('grc', 'global_blocks_path', ''),
)
diff --git a/grc/core/Connection.py b/grc/core/Connection.py
index 066532149b..01baaaf8fc 100644
--- a/grc/core/Connection.py
+++ b/grc/core/Connection.py
@@ -19,26 +19,22 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
from __future__ import absolute_import
-import collections
-
-from six.moves import range
-
-from . import Constants
-from .Element import Element, lazy_property
+from .base import Element
+from .utils.descriptors import lazy_property
class Connection(Element):
is_connection = True
- def __init__(self, parent, porta, portb):
+ def __init__(self, parent, source, sink):
"""
Make a new connection given the parent and 2 ports.
Args:
flow_graph: the parent of this element
- porta: a port (any direction)
- portb: a port (any direction)
+ source: a port (any direction)
+ sink: a port (any direction)
@throws Error cannot make connection
Returns:
@@ -46,37 +42,31 @@ class Connection(Element):
"""
Element.__init__(self, parent)
- source, sink = self._get_sink_source(porta, portb)
+ if not source.is_source:
+ source, sink = sink, source
+ if not source.is_source:
+ raise ValueError('Connection could not isolate source')
+ if not sink.is_sink:
+ raise ValueError('Connection could not isolate sink')
self.source_port = source
self.sink_port = sink
- # Ensure that this connection (source -> sink) is unique
- if self in self.parent_flowgraph.connections:
- raise LookupError('This connection between source and sink is not unique.')
-
- if self.is_bus():
- self._make_bus_connect()
+ def __str__(self):
+ return 'Connection (\n\t{}\n\t\t{}\n\t{}\n\t\t{}\n)'.format(
+ self.source_block, self.source_port, self.sink_block, self.sink_port,
+ )
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.source_port == other.source_port and self.sink_port == other.sink_port
- @staticmethod
- def _get_sink_source(porta, portb):
- source = sink = None
- # Separate the source and sink
- for port in (porta, portb):
- if port.is_source:
- source = port
- if port.is_sink:
- sink = port
- if not source:
- raise ValueError('Connection could not isolate source')
- if not sink:
- raise ValueError('Connection could not isolate sink')
- return source, sink
+ def __hash__(self):
+ return hash((self.source_port, self.sink_port))
+
+ def __iter__(self):
+ return iter((self.source_port, self.sink_port))
@lazy_property
def source_block(self):
@@ -86,6 +76,10 @@ class Connection(Element):
def sink_block(self):
return self.sink_port.parent_block
+ @lazy_property
+ def type(self):
+ return self.source_port.domain, self.sink_port.domain
+
@property
def enabled(self):
"""
@@ -96,14 +90,6 @@ class Connection(Element):
"""
return self.source_block.enabled and self.sink_block.enabled
- def __str__(self):
- return 'Connection (\n\t{}\n\t\t{}\n\t{}\n\t\t{}\n)'.format(
- self.source_block, self.source_port, self.sink_block, self.sink_port,
- )
-
- def is_bus(self):
- return self.source_port.get_type() == 'bus'
-
def validate(self):
"""
Validate the connections.
@@ -112,29 +98,12 @@ class Connection(Element):
Element.validate(self)
platform = self.parent_platform
- source_domain = self.source_port.domain
- sink_domain = self.sink_port.domain
+ if self.type not in platform.connection_templates:
+ self.add_error_message('No connection known between domains "{}" and "{}"'
+ ''.format(*self.type))
- if (source_domain, sink_domain) not in platform.connection_templates:
- self.add_error_message('No connection known for domains "{}", "{}"'.format(
- source_domain, sink_domain))
- too_many_other_sinks = (
- not platform.domains.get(source_domain, []).get('multiple_sinks', False) and
- len(self.source_port.get_enabled_connections()) > 1
- )
- too_many_other_sources = (
- not platform.domains.get(sink_domain, []).get('multiple_sources', False) and
- len(self.sink_port.get_enabled_connections()) > 1
- )
- if too_many_other_sinks:
- self.add_error_message(
- 'Domain "{}" can have only one downstream block'.format(source_domain))
- if too_many_other_sources:
- self.add_error_message(
- 'Domain "{}" can have only one upstream block'.format(sink_domain))
-
- source_size = Constants.TYPE_TO_SIZEOF[self.source_port.get_type()] * self.source_port.get_vlen()
- sink_size = Constants.TYPE_TO_SIZEOF[self.sink_port.get_type()] * self.sink_port.get_vlen()
+ source_size = self.source_port.item_size
+ sink_size = self.sink_port.item_size
if source_size != sink_size:
self.add_error_message('Source IO size "{}" does not match sink IO size "{}".'.format(source_size, sink_size))
@@ -148,23 +117,7 @@ class Connection(Element):
Returns:
a nested data odict
"""
- n = collections.OrderedDict()
- n['source_block_id'] = self.source_block.get_id()
- n['sink_block_id'] = self.sink_block.get_id()
- n['source_key'] = self.source_port.key
- n['sink_key'] = self.sink_port.key
- return n
-
- def _make_bus_connect(self):
- source, sink = self.source_port, self.sink_port
-
- if source.get_type() == sink.get_type() == 'bus':
- raise ValueError('busses must get with busses')
-
- sources = source.get_associated_ports()
- sinks = sink.get_associated_ports()
- if len(sources) != len(sinks):
- raise ValueError('port connections must have same cardinality')
-
- for ports in zip(sources, sinks):
- self.parent_flowgraph.connect(*ports)
+ return (
+ self.source_block.name, self.source_port.key,
+ self.sink_block.name, self.sink_port.key
+ )
diff --git a/grc/core/Constants.py b/grc/core/Constants.py
index caf301be60..fc5383378c 100644
--- a/grc/core/Constants.py
+++ b/grc/core/Constants.py
@@ -23,17 +23,15 @@ import os
import stat
import numpy
-import six
+
# Data files
DATA_DIR = os.path.dirname(__file__)
-FLOW_GRAPH_DTD = os.path.join(DATA_DIR, 'flow_graph.dtd')
-BLOCK_TREE_DTD = os.path.join(DATA_DIR, 'block_tree.dtd')
BLOCK_DTD = os.path.join(DATA_DIR, 'block.dtd')
DEFAULT_FLOW_GRAPH = os.path.join(DATA_DIR, 'default_flow_graph.grc')
DEFAULT_HIER_BLOCK_LIB_DIR = os.path.expanduser('~/.grc_gnuradio')
-DOMAIN_DTD = os.path.join(DATA_DIR, 'domain.dtd')
+BLOCK_DESCRIPTION_FILE_FORMAT_VERSION = 1
# File format versions:
# 0: undefined / legacy
# 1: non-numeric message port keys (label is used instead)
@@ -45,15 +43,10 @@ ADVANCED_PARAM_TAB = "Advanced"
DEFAULT_BLOCK_MODULE_NAME = '(no module specified)'
# Port domains
-GR_STREAM_DOMAIN = "gr_stream"
-GR_MESSAGE_DOMAIN = "gr_message"
+GR_STREAM_DOMAIN = "stream"
+GR_MESSAGE_DOMAIN = "message"
DEFAULT_DOMAIN = GR_STREAM_DOMAIN
-BLOCK_FLAG_THROTTLE = 'throttle'
-BLOCK_FLAG_DISABLE_BYPASS = 'disable_bypass'
-BLOCK_FLAG_NEED_QT_GUI = 'need_qt_gui'
-BLOCK_FLAG_DEPRECATED = 'deprecated'
-
# File creation modes
TOP_BLOCK_FILE_MODE = stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | \
stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH
diff --git a/grc/core/FlowGraph.py b/grc/core/FlowGraph.py
index bf5bf6d93e..3ad95eb207 100644
--- a/grc/core/FlowGraph.py
+++ b/grc/core/FlowGraph.py
@@ -17,24 +17,17 @@
from __future__ import absolute_import, print_function
-import imp
-import time
-import re
-from operator import methodcaller
import collections
+import imp
+import itertools
import sys
+from operator import methodcaller, attrgetter
-from . import Messages
+from . import Messages, blocks
from .Constants import FLOW_GRAPH_FILE_FORMAT_VERSION
-from .Element import Element
-from .utils import expr_utils, shlex
-
-_parameter_matcher = re.compile('^(parameter)$')
-_monitors_searcher = re.compile('(ctrlport_monitor)')
-_bussink_searcher = re.compile('^(bus_sink)$')
-_bussrc_searcher = re.compile('^(bus_source)$')
-_bus_struct_sink_searcher = re.compile('^(bus_structure_sink)$')
-_bus_struct_src_searcher = re.compile('^(bus_structure_source)$')
+from .base import Element
+from .utils import expr_utils
+from .utils.backports import shlex
class FlowGraph(Element):
@@ -52,11 +45,10 @@ class FlowGraph(Element):
the flow graph object
"""
Element.__init__(self, parent)
- self._timestamp = time.ctime()
- self._options_block = self.parent_platform.get_new_block(self, 'options')
+ self._options_block = self.parent_platform.make_block(self, 'options')
self.blocks = [self._options_block]
- self.connections = []
+ self.connections = set()
self._eval_cache = {}
self.namespace = {}
@@ -66,15 +58,14 @@ class FlowGraph(Element):
def __str__(self):
return 'FlowGraph - {}({})'.format(self.get_option('title'), self.get_option('id'))
- def get_imports(self):
+ def imports(self):
"""
Get a set of all import statements in this flow graph namespace.
Returns:
- a set of import statements
+ a list of import statements
"""
- imports = sum([block.get_imports() for block in self.iter_enabled_blocks()], [])
- return sorted(set(imports))
+ return [block.templates.render('imports') for block in self.iter_enabled_blocks()]
def get_variables(self):
"""
@@ -85,7 +76,7 @@ class FlowGraph(Element):
a sorted list of variable blocks in order of dependency (indep -> dep)
"""
variables = [block for block in self.iter_enabled_blocks() if block.is_variable]
- return expr_utils.sort_objects(variables, methodcaller('get_id'), methodcaller('get_var_make'))
+ return expr_utils.sort_objects(variables, attrgetter('name'), methodcaller('get_var_make'))
def get_parameters(self):
"""
@@ -94,47 +85,21 @@ class FlowGraph(Element):
Returns:
a list of parameterized variables
"""
- parameters = [b for b in self.iter_enabled_blocks() if _parameter_matcher.match(b.key)]
+ parameters = [b for b in self.iter_enabled_blocks() if b.key == 'parameter']
return parameters
def get_monitors(self):
"""
Get a list of all ControlPort monitors
"""
- monitors = [b for b in self.iter_enabled_blocks() if _monitors_searcher.search(b.key)]
+ monitors = [b for b in self.iter_enabled_blocks() if 'ctrlport_monitor' in b.key]
return monitors
def get_python_modules(self):
"""Iterate over custom code block ID and Source"""
for block in self.iter_enabled_blocks():
if block.key == 'epy_module':
- yield block.get_id(), block.get_param('source_code').get_value()
-
- def get_bussink(self):
- bussink = [b for b in self.get_enabled_blocks() if _bussink_searcher.search(b.key)]
-
- for i in bussink:
- for j in i.params.values():
- if j.name == 'On/Off' and j.get_value() == 'on':
- return True
- return False
-
- def get_bussrc(self):
- bussrc = [b for b in self.get_enabled_blocks() if _bussrc_searcher.search(b.key)]
-
- for i in bussrc:
- for j in i.params.values():
- if j.name == 'On/Off' and j.get_value() == 'on':
- return True
- return False
-
- def get_bus_structure_sink(self):
- bussink = [b for b in self.get_enabled_blocks() if _bus_struct_sink_searcher.search(b.key)]
- return bussink
-
- def get_bus_structure_src(self):
- bussrc = [b for b in self.get_enabled_blocks() if _bus_struct_src_searcher.search(b.key)]
- return bussrc
+ yield block.name, block.params[1].get_value()
def iter_enabled_blocks(self):
"""
@@ -180,7 +145,7 @@ class FlowGraph(Element):
Returns:
the value held by that param
"""
- return self._options_block.get_param(key).get_evaluated()
+ return self._options_block.params[key].get_evaluated()
def get_run_command(self, file_path, split=False):
run_command = self.get_option('run_command')
@@ -195,16 +160,19 @@ class FlowGraph(Element):
##############################################
# Access Elements
##############################################
- def get_block(self, id):
+ def get_block(self, name):
for block in self.blocks:
- if block.get_id() == id:
+ if block.name == name:
return block
- raise KeyError('No block with ID {!r}'.format(id))
+ raise KeyError('No block with name {!r}'.format(name))
def get_elements(self):
- return self.blocks + self.connections
+ elements = list(self.blocks)
+ elements.extend(self.connections)
+ return elements
- get_children = get_elements
+ def children(self):
+ return itertools.chain(self.blocks, self.connections)
def rewrite(self):
"""
@@ -216,7 +184,7 @@ class FlowGraph(Element):
def renew_namespace(self):
namespace = {}
# Load imports
- for expr in self.get_imports():
+ for expr in self.imports():
try:
exec(expr, namespace)
except:
@@ -232,19 +200,19 @@ class FlowGraph(Element):
# Load parameters
np = {} # params don't know each other
- for parameter in self.get_parameters():
+ for parameter_block in self.get_parameters():
try:
- value = eval(parameter.get_param('value').to_code(), namespace)
- np[parameter.get_id()] = value
+ value = eval(parameter_block.params['value'].to_code(), namespace)
+ np[parameter_block.name] = value
except:
pass
namespace.update(np) # Merge param namespace
# Load variables
- for variable in self.get_variables():
+ for variable_block in self.get_variables():
try:
- value = eval(variable.get_var_value(), namespace)
- namespace[variable.get_id()] = value
+ value = eval(variable_block.value, namespace, variable_block.namespace)
+ namespace[variable_block.name] = value
except:
pass
@@ -252,41 +220,37 @@ class FlowGraph(Element):
self._eval_cache.clear()
self.namespace.update(namespace)
- def evaluate(self, expr):
+ def evaluate(self, expr, namespace=None, local_namespace=None):
"""
Evaluate the expression.
-
- Args:
- expr: the string expression
- @throw Exception bad expression
-
- Returns:
- the evaluated data
"""
# Evaluate
if not expr:
raise Exception('Cannot evaluate empty statement.')
- return self._eval_cache.setdefault(expr, eval(expr, self.namespace))
+ if namespace is not None:
+ return eval(expr, namespace, local_namespace)
+ else:
+ return self._eval_cache.setdefault(expr, eval(expr, self.namespace))
##############################################
# Add/remove stuff
##############################################
- def new_block(self, key, **kwargs):
+ def new_block(self, block_id, **kwargs):
"""
Get a new block of the specified key.
Add the block to the list of elements.
Args:
- key: the block key
+ block_id: the block key
Returns:
the new block or None if not found
"""
- if key == 'options':
+ if block_id == 'options':
return self._options_block
try:
- block = self.parent_platform.get_new_block(self, key, **kwargs)
+ block = self.parent_platform.make_block(self, block_id, **kwargs)
self.blocks.append(block)
except KeyError:
block = None
@@ -304,12 +268,17 @@ class FlowGraph(Element):
Returns:
the new connection
"""
-
connection = self.parent_platform.Connection(
- parent=self, porta=porta, portb=portb)
- self.connections.append(connection)
+ parent=self, source=porta, sink=portb)
+ self.connections.add(connection)
return connection
+ def disconnect(self, *ports):
+ to_be_removed = [con for con in self.connections
+ if any(port in con for port in ports)]
+ for con in to_be_removed:
+ self.remove_element(con)
+
def remove_element(self, element):
"""
Remove the element from the list of elements.
@@ -321,21 +290,14 @@ class FlowGraph(Element):
return
if element.is_port:
- # Found a port, set to parent signal block
- element = element.parent
+ element = element.parent_block # remove parent block
if element in self.blocks:
# Remove block, remove all involved connections
- for port in element.get_ports():
- for connection in port.get_connections():
- self.remove_element(connection)
+ self.disconnect(*element.ports())
self.blocks.remove(element)
elif element in self.connections:
- if element.is_bus():
- for port in element.source_port.get_associated_ports():
- for connection in port.get_connections():
- self.remove_element(connection)
self.connections.remove(element)
##############################################
@@ -349,70 +311,61 @@ class FlowGraph(Element):
Returns:
a nested data odict
"""
- # sort blocks and connections for nicer diffs
- blocks = sorted(self.blocks, key=lambda b: (
- b.key != 'options', # options to the front
- not b.key.startswith('variable'), # then vars
- str(b)
- ))
- connections = sorted(self.connections, key=str)
- n = collections.OrderedDict()
- n['timestamp'] = self._timestamp
- n['block'] = [b.export_data() for b in blocks]
- n['connection'] = [c.export_data() for c in connections]
- instructions = collections.OrderedDict()
- instructions['created'] = '.'.join(self.parent.config.version_parts)
- instructions['format'] = FLOW_GRAPH_FILE_FORMAT_VERSION
- return {'flow_graph': n, '_instructions': instructions}
-
- def import_data(self, n):
+ def block_order(b):
+ return not b.key.startswith('variable'), b.name # todo: vars still first ?!?
+
+ data = collections.OrderedDict()
+ data['options'] = self._options_block.export_data()
+ data['blocks'] = [b.export_data() for b in sorted(self.blocks, key=block_order)
+ if b is not self._options_block]
+ data['connections'] = sorted(c.export_data() for c in self.connections)
+ data['metadata'] = {'file_format': FLOW_GRAPH_FILE_FORMAT_VERSION}
+ return data
+
+ def _build_depending_hier_block(self, block_id):
+ # we're before the initial fg update(), so no evaluated values!
+ # --> use raw value instead
+ path_param = self._options_block.params['hier_block_src_path']
+ file_path = self.parent_platform.find_file_in_paths(
+ filename=block_id + '.grc',
+ paths=path_param.get_value(),
+ cwd=self.grc_file_path
+ )
+ if file_path: # grc file found. load and get block
+ self.parent_platform.load_and_generate_flow_graph(file_path, hier_only=True)
+ return self.new_block(block_id) # can be None
+
+ def import_data(self, data):
"""
Import blocks and connections into this flow graph.
Clear this flow graph of all previous blocks and connections.
Any blocks or connections in error will be ignored.
Args:
- n: the nested data odict
+ data: the nested data odict
"""
# Remove previous elements
del self.blocks[:]
- del self.connections[:]
- # set file format
- try:
- instructions = n.get('_instructions', {})
- file_format = int(instructions.get('format', '0')) or _guess_file_format_1(n)
- except:
- file_format = 0
+ self.connections.clear()
- fg_n = n and n.get('flow_graph', {}) # use blank data if none provided
- self._timestamp = fg_n.get('timestamp', time.ctime())
+ file_format = data['metadata']['file_format']
# build the blocks
+ self._options_block.import_data(name='', **data.get('options', {}))
self.blocks.append(self._options_block)
- for block_n in fg_n.get('block', []):
- key = block_n['key']
- block = self.new_block(key)
-
- if not block:
- # we're before the initial fg update(), so no evaluated values!
- # --> use raw value instead
- path_param = self._options_block.get_param('hier_block_src_path')
- file_path = self.parent_platform.find_file_in_paths(
- filename=key + '.grc',
- paths=path_param.get_value(),
- cwd=self.grc_file_path
- )
- if file_path: # grc file found. load and get block
- self.parent_platform.load_and_generate_flow_graph(file_path, hier_only=True)
- block = self.new_block(key) # can be None
-
- if not block: # looks like this block key cannot be found
- # create a dummy block instead
- block = self.new_block('_dummy', missing_key=key,
- params_n=block_n.get('param', []))
- print('Block key "%s" not found' % key)
-
- block.import_data(block_n)
+
+ for block_data in data.get('blocks', []):
+ block_id = block_data['id']
+ block = (
+ self.new_block(block_id) or
+ self._build_depending_hier_block(block_id) or
+ self.new_block(block_id='_dummy', missing_block_id=block_id, **block_data)
+ )
+
+ if isinstance(block, blocks.DummyBlock):
+ print('Block id "%s" not found' % block_id)
+
+ block.import_data(**block_data)
self.rewrite() # evaluate stuff like nports before adding connections
@@ -420,9 +373,9 @@ class FlowGraph(Element):
def verify_and_get_port(key, block, dir):
ports = block.sinks if dir == 'sink' else block.sources
for port in ports:
- if key == port.key:
+ if key == port.key or key + '0' == port.key:
break
- if not key.isdigit() and port.get_type() == '' and key == port.name:
+ if not key.isdigit() and port.dtype == '' and key == port.name:
break
else:
if block.is_dummy_block:
@@ -431,34 +384,32 @@ class FlowGraph(Element):
raise LookupError('%s key %r not in %s block keys' % (dir, key, dir))
return port
- errors = False
- for connection_n in fg_n.get('connection', []):
- # get the block ids and port keys
- source_block_id = connection_n.get('source_block_id')
- sink_block_id = connection_n.get('sink_block_id')
- source_key = connection_n.get('source_key')
- sink_key = connection_n.get('sink_key')
+ had_connect_errors = False
+ _blocks = {block.name: block for block in self.blocks}
+ for src_blk_id, src_port_id, snk_blk_id, snk_port_id in data.get('connections', []):
try:
- source_block = self.get_block(source_block_id)
- sink_block = self.get_block(sink_block_id)
+ source_block = _blocks[src_blk_id]
+ sink_block = _blocks[snk_blk_id]
# fix old, numeric message ports keys
if file_format < 1:
- source_key, sink_key = _update_old_message_port_keys(
- source_key, sink_key, source_block, sink_block)
+ src_port_id, snk_port_id = _update_old_message_port_keys(
+ src_port_id, snk_port_id, source_block, sink_block)
# build the connection
- source_port = verify_and_get_port(source_key, source_block, 'source')
- sink_port = verify_and_get_port(sink_key, sink_block, 'sink')
+ source_port = verify_and_get_port(src_port_id, source_block, 'source')
+ sink_port = verify_and_get_port(snk_port_id, sink_block, 'sink')
+
self.connect(source_port, sink_port)
- except LookupError as e:
+
+ except (KeyError, LookupError) as e:
Messages.send_error_load(
'Connection between {}({}) and {}({}) could not be made.\n\t{}'.format(
- source_block_id, source_key, sink_block_id, sink_key, e))
- errors = True
+ src_blk_id, src_port_id, snk_blk_id, snk_port_id, e))
+ had_connect_errors = True
self.rewrite() # global rewrite
- return errors
+ return had_connect_errors
def _update_old_message_port_keys(source_key, sink_key, source_block, sink_block):
@@ -475,27 +426,11 @@ def _update_old_message_port_keys(source_key, sink_key, source_block, sink_block
message port.
"""
try:
- # get ports using the "old way" (assuming liner indexed keys)
+ # get ports using the "old way" (assuming linear indexed keys)
source_port = source_block.sources[int(source_key)]
sink_port = sink_block.sinks[int(sink_key)]
- if source_port.get_type() == "message" and sink_port.get_type() == "message":
+ if source_port.dtype == "message" and sink_port.dtype == "message":
source_key, sink_key = source_port.key, sink_port.key
except (ValueError, IndexError):
pass
return source_key, sink_key # do nothing
-
-
-def _guess_file_format_1(n):
- """
- Try to guess the file format for flow-graph files without version tag
- """
- try:
- has_non_numeric_message_keys = any(not (
- connection_n.get('source_key', '').isdigit() and
- connection_n.get('sink_key', '').isdigit()
- ) for connection_n in n.get('flow_graph', []).get('connection', []))
- if has_non_numeric_message_keys:
- return 1
- except:
- pass
- return 0
diff --git a/grc/core/Param.py b/grc/core/Param.py
index be86f0aecb..e8c81383f3 100644
--- a/grc/core/Param.py
+++ b/grc/core/Param.py
@@ -20,27 +20,27 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
from __future__ import absolute_import
import ast
+import numbers
import re
import collections
+import six
from six.moves import builtins, filter, map, range, zip
-from . import Constants
-from .Element import Element, nop_write
+from . import Constants, blocks
+from .base import Element
+from .utils.descriptors import Evaluated, EvaluatedEnum, setup_names
# Blacklist certain ids, its not complete, but should help
ID_BLACKLIST = ['self', 'options', 'gr', 'math', 'firdes'] + dir(builtins)
try:
from gnuradio import gr
ID_BLACKLIST.extend(attr for attr in dir(gr.top_block()) if not attr.startswith('_'))
-except ImportError:
+except (ImportError, AttributeError):
pass
-_check_id_matcher = re.compile('^[a-z|A-Z]\w*$')
-_show_id_matcher = re.compile('^(variable\w*|parameter|options|notebook)$')
-
-class TemplateArg(object):
+class TemplateArg(str):
"""
A cheetah template argument created from a param.
The str of this class evaluates to the param's to code method.
@@ -48,123 +48,119 @@ class TemplateArg(object):
The __call__ or () method can return the param evaluated to a raw python data type.
"""
- def __init__(self, param):
- self._param = param
+ def __new__(cls, param):
+ value = param.to_code()
+ instance = str.__new__(cls, value)
+ setattr(instance, '_param', param)
+ return instance
- def __getitem__(self, item):
+ def __getattr__(self, item):
param = self._param
- opts = param.options_opts[param.get_value()]
- return str(opts[item]) if param.is_enum() else NotImplemented
-
- def __str__(self):
- return str(self._param.to_code())
+ attributes = param.options.attributes[param.get_value()]
+ return str(attributes.get(item)) or NotImplemented
def __call__(self):
return self._param.get_evaluated()
+@setup_names
class Param(Element):
is_param = True
- def __init__(self, parent, key, name, type='raw', value='', **n):
+ name = Evaluated(str, default='no name')
+ dtype = EvaluatedEnum(Constants.PARAM_TYPE_NAMES, default='raw')
+ hide = EvaluatedEnum('none all part')
+
+ # region init
+ def __init__(self, parent, id, label='', dtype='raw', default='',
+ options=None, option_labels=None, option_attributes=None,
+ category='', hide='none', **_):
"""Make a new param from nested data"""
super(Param, self).__init__(parent)
- self.key = key
- self._name = name
- self.value = self.default = value
- self._type = type
+ self.key = id
+ self.name = label.strip() or id.title()
+ self.category = category or Constants.DEFAULT_PARAM_TAB
- self._hide = n.get('hide', '')
- self.tab_label = n.get('tab', Constants.DEFAULT_PARAM_TAB)
- self._evaluated = None
+ self.dtype = dtype
+ self.value = self.default = str(default)
- self.options = []
- self.options_names = []
- self.options_opts = {}
- self._init_options(options_n=n.get('option', []))
+ self.options = self._init_options(options or [], option_labels or [],
+ option_attributes or {})
+ self.hide = hide or 'none'
+ # end of args ########################################################
+ self._evaluated = None
+ self._stringify_flag = False
+ self._lisitify_flag = False
self._init = False
- self._hostage_cells = list()
- self.template_arg = TemplateArg(self)
-
- def _init_options(self, options_n):
- """Create the Option objects from the n data"""
- option_keys = set()
- for option_n in options_n:
- key, name = option_n['key'], option_n['name']
- # Test against repeated keys
- if key in option_keys:
- raise KeyError('Key "{}" already exists in options'.format(key))
- option_keys.add(key)
- # Store the option
- self.options.append(key)
- self.options_names.append(name)
- if self.is_enum():
- self._init_enum(options_n)
+ @property
+ def template_arg(self):
+ return TemplateArg(self)
+
+ def _init_options(self, values, labels, attributes):
+ """parse option and option attributes"""
+ options = collections.OrderedDict()
+ options.attributes = collections.defaultdict(dict)
- def _init_enum(self, options_n):
- opt_ref = None
- for option_n in options_n:
- key, opts_raw = option_n['key'], option_n.get('opt', [])
+ padding = [''] * max(len(values), len(labels))
+ attributes = {key: value + padding for key, value in six.iteritems(attributes)}
+
+ for i, option in enumerate(values):
+ # Test against repeated keys
+ if option in options:
+ raise KeyError('Value "{}" already exists in options'.format(option))
+ # get label
try:
- self.options_opts[key] = opts = dict(opt.split(':') for opt in opts_raw)
- except TypeError:
- raise ValueError('Error separating opts into key:value')
-
- if opt_ref is None:
- opt_ref = set(opts.keys())
- elif opt_ref != set(opts):
- raise ValueError('Opt keys ({}) are not identical across all options.'
- ''.format(', '.join(opt_ref)))
+ label = str(labels[i])
+ except IndexError:
+ label = str(option)
+ # Store the option
+ options[option] = label
+ options.attributes[option] = {attrib: values[i] for attrib, values in six.iteritems(attributes)}
+
+ default = next(iter(options)) if options else ''
if not self.value:
- self.value = self.default = self.options[0]
- elif self.value not in self.options:
- self.value = self.default = self.options[0] # TODO: warn
+ self.value = self.default = default
+
+ if self.is_enum() and self.value not in options:
+ self.value = self.default = default # TODO: warn
# raise ValueError('The value {!r} is not in the possible values of {}.'
# ''.format(self.get_value(), ', '.join(self.options)))
+ return options
+ # endregion
def __str__(self):
return 'Param - {}({})'.format(self.name, self.key)
- def get_hide(self):
- """
- Get the hide value from the base class.
- Hide the ID parameter for most blocks. Exceptions below.
- If the parameter controls a port type, vlen, or nports, return part.
- If the parameter is an empty grid position, return part.
- These parameters are redundant to display in the flow graph view.
+ def __repr__(self):
+ return '{!r}.param[{}]'.format(self.parent, self.key)
- Returns:
- hide the hide property string
- """
- hide = self.parent.resolve_dependencies(self._hide).strip()
- if hide:
- return hide
- # Hide ID in non variable blocks
- if self.key == 'id' and not _show_id_matcher.match(self.parent.key):
- return 'part'
- # Hide port controllers for type and nports
- if self.key in ' '.join([' '.join([p._type, p._nports]) for p in self.parent.get_ports()]):
- return 'part'
- # Hide port controllers for vlen, when == 1
- if self.key in ' '.join(p._vlen for p in self.parent.get_ports()):
- try:
- if int(self.get_evaluated()) == 1:
- return 'part'
- except:
- pass
- return hide
+ def is_enum(self):
+ return self.get_raw('dtype') == 'enum'
- def validate(self):
- """
- Validate the param.
- The value must be evaluated and type must a possible type.
- """
- Element.validate(self)
- if self.get_type() not in Constants.PARAM_TYPE_NAMES:
- self.add_error_message('Type "{}" is not a possible type.'.format(self.get_type()))
+ def get_value(self):
+ value = self.value
+ if self.is_enum() and value not in self.options:
+ value = self.default
+ self.set_value(value)
+ return value
+
+ def set_value(self, value):
+ # Must be a string
+ self.value = str(value)
+
+ def set_default(self, value):
+ if self.default == self.value:
+ self.set_value(value)
+ self.default = str(value)
+
+ def rewrite(self):
+ Element.rewrite(self)
+ del self.name
+ del self.dtype
+ del self.hide
self._evaluated = None
try:
@@ -172,6 +168,15 @@ class Param(Element):
except Exception as e:
self.add_error_message(str(e))
+ def validate(self):
+ """
+ Validate the param.
+ The value must be evaluated and type must a possible type.
+ """
+ Element.validate(self)
+ if self.dtype not in Constants.PARAM_TYPE_NAMES:
+ self.add_error_message('Type "{}" is not a possible type.'.format(self.dtype))
+
def get_evaluated(self):
return self._evaluated
@@ -185,150 +190,112 @@ class Param(Element):
self._init = True
self._lisitify_flag = False
self._stringify_flag = False
- self._hostage_cells = list()
- t = self.get_type()
- v = self.get_value()
+ dtype = self.dtype
+ expr = self.get_value()
#########################
# Enum Type
#########################
if self.is_enum():
- return v
+ return expr
#########################
# Numeric Types
#########################
- elif t in ('raw', 'complex', 'real', 'float', 'int', 'hex', 'bool'):
+ elif dtype in ('raw', 'complex', 'real', 'float', 'int', 'hex', 'bool'):
# Raise exception if python cannot evaluate this value
try:
- e = self.parent_flowgraph.evaluate(v)
- except Exception as e:
- raise Exception('Value "{}" cannot be evaluated:\n{}'.format(v, e))
+ value = self.parent_flowgraph.evaluate(expr)
+ except Exception as value:
+ raise Exception('Value "{}" cannot be evaluated:\n{}'.format(expr, value))
# Raise an exception if the data is invalid
- if t == 'raw':
- return e
- elif t == 'complex':
- if not isinstance(e, Constants.COMPLEX_TYPES):
- raise Exception('Expression "{}" is invalid for type complex.'.format(str(e)))
- return e
- elif t == 'real' or t == 'float':
- if not isinstance(e, Constants.REAL_TYPES):
- raise Exception('Expression "{}" is invalid for type float.'.format(str(e)))
- return e
- elif t == 'int':
- if not isinstance(e, Constants.INT_TYPES):
- raise Exception('Expression "{}" is invalid for type integer.'.format(str(e)))
- return e
- elif t == 'hex':
- return hex(e)
- elif t == 'bool':
- if not isinstance(e, bool):
- raise Exception('Expression "{}" is invalid for type bool.'.format(str(e)))
- return e
+ if dtype == 'raw':
+ return value
+ elif dtype == 'complex':
+ if not isinstance(value, Constants.COMPLEX_TYPES):
+ raise Exception('Expression "{}" is invalid for type complex.'.format(str(value)))
+ return value
+ elif dtype in ('real', 'float'):
+ if not isinstance(value, Constants.REAL_TYPES):
+ raise Exception('Expression "{}" is invalid for type float.'.format(str(value)))
+ return value
+ elif dtype == 'int':
+ if not isinstance(value, Constants.INT_TYPES):
+ raise Exception('Expression "{}" is invalid for type integer.'.format(str(value)))
+ return value
+ elif dtype == 'hex':
+ return hex(value)
+ elif dtype == 'bool':
+ if not isinstance(value, bool):
+ raise Exception('Expression "{}" is invalid for type bool.'.format(str(value)))
+ return value
else:
- raise TypeError('Type "{}" not handled'.format(t))
+ raise TypeError('Type "{}" not handled'.format(dtype))
#########################
# Numeric Vector Types
#########################
- elif t in ('complex_vector', 'real_vector', 'float_vector', 'int_vector'):
- if not v:
- # Turn a blank string into an empty list, so it will eval
- v = '()'
- # Raise exception if python cannot evaluate this value
+ elif dtype in ('complex_vector', 'real_vector', 'float_vector', 'int_vector'):
+ default = []
+
+ if not expr:
+ return default # Turn a blank string into an empty list, so it will eval
+
try:
- e = self.parent.parent.evaluate(v)
- except Exception as e:
- raise Exception('Value "{}" cannot be evaluated:\n{}'.format(v, e))
+ value = self.parent.parent.evaluate(expr)
+ except Exception as value:
+ raise Exception('Value "{}" cannot be evaluated:\n{}'.format(expr, value))
+
+ if not isinstance(value, Constants.VECTOR_TYPES):
+ self._lisitify_flag = True
+ value = [value]
+
# Raise an exception if the data is invalid
- if t == 'complex_vector':
- if not isinstance(e, Constants.VECTOR_TYPES):
- self._lisitify_flag = True
- e = [e]
- if not all([isinstance(ei, Constants.COMPLEX_TYPES) for ei in e]):
- raise Exception('Expression "{}" is invalid for type complex vector.'.format(str(e)))
- return e
- elif t == 'real_vector' or t == 'float_vector':
- if not isinstance(e, Constants.VECTOR_TYPES):
- self._lisitify_flag = True
- e = [e]
- if not all([isinstance(ei, Constants.REAL_TYPES) for ei in e]):
- raise Exception('Expression "{}" is invalid for type float vector.'.format(str(e)))
- return e
- elif t == 'int_vector':
- if not isinstance(e, Constants.VECTOR_TYPES):
- self._lisitify_flag = True
- e = [e]
- if not all([isinstance(ei, Constants.INT_TYPES) for ei in e]):
- raise Exception('Expression "{}" is invalid for type integer vector.'.format(str(e)))
- return e
+ if dtype == 'complex_vector' and not all(isinstance(item, numbers.Complex) for item in value):
+ raise Exception('Expression "{}" is invalid for type complex vector.'.format(value))
+ elif dtype in ('real_vector', 'float_vector') and not all(isinstance(item, numbers.Real) for item in value):
+ raise Exception('Expression "{}" is invalid for type float vector.'.format(value))
+ elif dtype == 'int_vector' and not all(isinstance(item, Constants.INT_TYPES) for item in value):
+ raise Exception('Expression "{}" is invalid for type integer vector.'.format(str(value)))
+ return value
#########################
# String Types
#########################
- elif t in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'):
+ 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:
- e = self.parent.parent.evaluate(v)
- if not isinstance(e, str):
+ value = self.parent.parent.evaluate(expr)
+ if not isinstance(value, str):
raise Exception()
except:
self._stringify_flag = True
- e = str(v)
- if t == '_multiline_python_external':
- ast.parse(e) # Raises SyntaxError
- return e
+ value = str(expr)
+ if dtype == '_multiline_python_external':
+ ast.parse(value) # Raises SyntaxError
+ return value
#########################
# Unique ID Type
#########################
- elif t == 'id':
- # Can python use this as a variable?
- if not _check_id_matcher.match(v):
- raise Exception('ID "{}" must begin with a letter and may contain letters, numbers, and underscores.'.format(v))
- ids = [param.get_value() for param in self.get_all_params(t, 'id')]
-
- if v in ID_BLACKLIST:
- raise Exception('ID "{}" is blacklisted.'.format(v))
-
- if self.key == 'id':
- # Id should only appear once, or zero times if block is disabled
- if ids.count(v) > 1:
- raise Exception('ID "{}" is not unique.'.format(v))
- else:
- # Id should exist to be a reference
- if ids.count(v) < 1:
- raise Exception('ID "{}" does not exist.'.format(v))
-
- return v
+ elif dtype == 'id':
+ self.validate_block_id()
+ return expr
#########################
# Stream ID Type
#########################
- elif t == 'stream_id':
- # Get a list of all stream ids used in the virtual sinks
- ids = [param.get_value() for param in filter(
- lambda p: p.parent.is_virtual_sink(),
- self.get_all_params(t),
- )]
- # Check that the virtual sink's stream id is unique
- if self.parent.is_virtual_sink():
- # Id should only appear once, or zero times if block is disabled
- if ids.count(v) > 1:
- raise Exception('Stream ID "{}" is not unique.'.format(v))
- # Check that the virtual source's steam id is found
- if self.parent.is_virtual_source():
- if v not in ids:
- raise Exception('Stream ID "{}" is not found.'.format(v))
- return v
+ elif dtype == 'stream_id':
+ self.validate_stream_id()
+ return expr
#########################
# GUI Position/Hint
#########################
- elif t == 'gui_hint':
- if ':' in v:
- tab, pos = v.split(':')
- elif '@' in v:
- tab, pos = v, ''
+ elif dtype == 'gui_hint':
+ if ':' in expr:
+ tab, pos = expr.split(':')
+ elif '@' in expr:
+ tab, pos = expr, ''
else:
- tab, pos = '', v
+ tab, pos = '', expr
if '@' in tab:
tab, index = tab.split('@')
@@ -358,20 +325,51 @@ class Param(Element):
#########################
# Import Type
#########################
- elif t == 'import':
+ elif dtype == 'import':
# New namespace
n = dict()
try:
- exec(v, n)
+ exec(expr, n)
except ImportError:
- raise Exception('Import "{}" failed.'.format(v))
+ raise Exception('Import "{}" failed.'.format(expr))
except Exception:
- raise Exception('Bad import syntax: "{}".'.format(v))
+ raise Exception('Bad import syntax: "{}".'.format(expr))
return [k for k in list(n.keys()) if str(k) != '__builtins__']
#########################
else:
- raise TypeError('Type "{}" not handled'.format(t))
+ raise TypeError('Type "{}" not handled'.format(dtype))
+
+ def validate_block_id(self):
+ value = self.value
+ # Can python use this as a variable?
+ if not re.match(r'^[a-z|A-Z]\w*$', value):
+ raise Exception('ID "{}" must begin with a letter and may contain letters, numbers, '
+ 'and underscores.'.format(value))
+ if value in ID_BLACKLIST:
+ raise Exception('ID "{}" is blacklisted.'.format(value))
+ block_names = [block.name for block in self.parent_flowgraph.iter_enabled_blocks()]
+ # Id should only appear once, or zero times if block is disabled
+ if self.key == 'id' and block_names.count(value) > 1:
+ raise Exception('ID "{}" is not unique.'.format(value))
+ elif value not in block_names:
+ raise Exception('ID "{}" does not exist.'.format(value))
+ return value
+
+ def validate_stream_id(self):
+ value = self.value
+ stream_ids = [
+ block.params['stream_id'].value
+ for block in self.parent_flowgraph.iter_enabled_blocks()
+ if isinstance(block, blocks.VirtualSink)
+ ]
+ # Check that the virtual sink's stream id is unique
+ if isinstance(self.parent_block, blocks.VirtualSink) and stream_ids.count(value) >= 2:
+ # Id should only appear once, or zero times if block is disabled
+ raise Exception('Stream ID "{}" is not unique.'.format(value))
+ # Check that the virtual source's steam id is found
+ elif isinstance(self.parent_block, blocks.VirtualSource) and value not in stream_ids:
+ raise Exception('Stream ID "{}" is not found.'.format(value))
def to_code(self):
"""
@@ -382,8 +380,9 @@ class Param(Element):
Returns:
a string representing the code
"""
+ self._init = True
v = self.get_value()
- t = self.get_type()
+ t = self.dtype
# String types
if t in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'):
if not self._init:
@@ -400,71 +399,3 @@ class Param(Element):
return '(%s)' % v
else:
return v
-
- def get_all_params(self, type, key=None):
- """
- Get all the params from the flowgraph that have the given type and
- optionally a given key
-
- Args:
- type: the specified type
- key: the key to match against
-
- Returns:
- a list of params
- """
- params = []
- for block in self.parent_flowgraph.get_enabled_blocks():
- params.extend(p for k, p in block.params.items() if p.get_type() == type and (key is None or key == k))
- return params
-
- def is_enum(self):
- return self._type == 'enum'
-
- def get_value(self):
- value = self.value
- if self.is_enum() and value not in self.options:
- value = self.options[0]
- self.set_value(value)
- return value
-
- def set_value(self, value):
- # Must be a string
- self.value = str(value)
-
- def set_default(self, value):
- if self.default == self.value:
- self.set_value(value)
- self.default = str(value)
-
- def get_type(self):
- return self.parent.resolve_dependencies(self._type)
-
- def get_tab_label(self):
- return self.tab_label
-
- @nop_write
- @property
- def name(self):
- return self.parent.resolve_dependencies(self._name).strip()
-
- ##############################################
- # Access Options
- ##############################################
- def opt_value(self, key):
- return self.options_opts[self.get_value()][key]
-
- ##############################################
- # Import/Export Methods
- ##############################################
- def export_data(self):
- """
- Export this param's key/value.
-
- Returns:
- a nested data odict
- """
- n = collections.OrderedDict()
- n['key'] = self.key
- n['value'] = self.get_value()
- return n
diff --git a/grc/core/Platform.py b/grc/core/Platform.py
deleted file mode 100644
index 73937f1299..0000000000
--- a/grc/core/Platform.py
+++ /dev/null
@@ -1,341 +0,0 @@
-"""
-Copyright 2008-2016 Free Software Foundation, Inc.
-This file is part of GNU Radio
-
-GNU Radio Companion is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-GNU Radio Companion is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-"""
-
-from __future__ import absolute_import, print_function
-
-import os
-import sys
-
-import six
-from six.moves import range
-
-from . import ParseXML, Messages, Constants
-
-from .Config import Config
-from .Element import Element
-from .generator import Generator
-from .FlowGraph import FlowGraph
-from .Connection import Connection
-from . import Block
-from .Port import Port, PortClone
-from .Param import Param
-
-from .utils import extract_docs
-
-
-class Platform(Element):
-
- is_platform = True
-
- def __init__(self, *args, **kwargs):
- """ Make a platform for GNU Radio """
- Element.__init__(self, parent=None)
-
- self.config = self.Config(*args, **kwargs)
- self.block_docstrings = {}
- self.block_docstrings_loaded_callback = lambda: None # dummy to be replaced by BlockTreeWindow
-
- self._docstring_extractor = extract_docs.SubprocessLoader(
- callback_query_result=self._save_docstring_extraction_result,
- callback_finished=lambda: self.block_docstrings_loaded_callback()
- )
-
- self.blocks = {}
- self._blocks_n = {}
- self._block_categories = {}
- self.domains = {}
- self.connection_templates = {}
-
- self._auto_hier_block_generate_chain = set()
-
- # Create a dummy flow graph for the blocks
- self._flow_graph = Element.__new__(FlowGraph)
- Element.__init__(self._flow_graph, self)
- self._flow_graph.connections = []
-
- self.build_block_library()
-
- def __str__(self):
- return 'Platform - {}'.format(self.config.name)
-
- @staticmethod
- def find_file_in_paths(filename, paths, cwd):
- """Checks the provided paths relative to cwd for a certain filename"""
- if not os.path.isdir(cwd):
- cwd = os.path.dirname(cwd)
- if isinstance(paths, str):
- paths = (p for p in paths.split(':') if p)
-
- for path in paths:
- path = os.path.expanduser(path)
- if not os.path.isabs(path):
- path = os.path.normpath(os.path.join(cwd, path))
- file_path = os.path.join(path, filename)
- if os.path.exists(os.path.normpath(file_path)):
- return file_path
-
- def load_and_generate_flow_graph(self, file_path, out_path=None, hier_only=False):
- """Loads a flow graph from file and generates it"""
- Messages.set_indent(len(self._auto_hier_block_generate_chain))
- Messages.send('>>> Loading: {}\n'.format(file_path))
- if file_path in self._auto_hier_block_generate_chain:
- Messages.send(' >>> Warning: cyclic hier_block dependency\n')
- return None, None
- self._auto_hier_block_generate_chain.add(file_path)
- try:
- flow_graph = self.get_new_flow_graph()
- flow_graph.grc_file_path = file_path
- # Other, nested hier_blocks might be auto-loaded here
- flow_graph.import_data(self.parse_flow_graph(file_path))
- flow_graph.rewrite()
- flow_graph.validate()
- if not flow_graph.is_valid():
- raise Exception('Flowgraph invalid')
- if hier_only and not flow_graph.get_option('generate_options').startswith('hb'):
- raise Exception('Not a hier block')
- except Exception as e:
- Messages.send('>>> Load Error: {}: {}\n'.format(file_path, str(e)))
- return None, None
- finally:
- self._auto_hier_block_generate_chain.discard(file_path)
- Messages.set_indent(len(self._auto_hier_block_generate_chain))
-
- try:
- generator = self.Generator(flow_graph, out_path or file_path)
- Messages.send('>>> Generating: {}\n'.format(generator.file_path))
- generator.write()
- except Exception as e:
- Messages.send('>>> Generate Error: {}: {}\n'.format(file_path, str(e)))
- return None, None
-
- if flow_graph.get_option('generate_options').startswith('hb'):
- self.load_block_xml(generator.file_path_xml)
- return flow_graph, generator.file_path
-
- def build_block_library(self):
- """load the blocks and block tree from the search paths"""
- self._docstring_extractor.start()
-
- # Reset
- self.blocks.clear()
- self._blocks_n.clear()
- self._block_categories.clear()
- self.domains.clear()
- self.connection_templates.clear()
- ParseXML.xml_failures.clear()
-
- # Try to parse and load blocks
- for xml_file in self.iter_xml_files():
- try:
- if xml_file.endswith("block_tree.xml"):
- self.load_category_tree_xml(xml_file)
- elif xml_file.endswith('domain.xml'):
- self.load_domain_xml(xml_file)
- else:
- self.load_block_xml(xml_file)
- except ParseXML.XMLSyntaxError as e:
- # print >> sys.stderr, 'Warning: Block validation failed:\n\t%s\n\tIgnoring: %s' % (e, xml_file)
- pass
- except Exception as e:
- raise
- print('Warning: XML parsing failed:\n\t%r\n\tIgnoring: %s' % (e, xml_file), file=sys.stderr)
-
- # Add blocks to block tree
- for key, block in six.iteritems(self.blocks):
- category = self._block_categories.get(key, block.category)
- # Blocks with empty categories are hidden
- if not category:
- continue
- root = category[0]
- if root.startswith('[') and root.endswith(']'):
- category[0] = root[1:-1]
- else:
- category.insert(0, Constants.DEFAULT_BLOCK_MODULE_NAME)
- block.category = category
-
- self._docstring_extractor.finish()
- # self._docstring_extractor.wait()
-
- def iter_xml_files(self):
- """Iterator for block descriptions and category trees"""
- for block_path in self.config.block_paths:
- if os.path.isfile(block_path):
- yield block_path
- elif os.path.isdir(block_path):
- for dirpath, dirnames, filenames in os.walk(block_path):
- for filename in sorted(f for f in filenames if f.endswith('.xml')):
- yield os.path.join(dirpath, filename)
-
- def load_block_xml(self, xml_file):
- """Load block description from xml file"""
- # Validate and import
- ParseXML.validate_dtd(xml_file, Constants.BLOCK_DTD)
- n = ParseXML.from_file(xml_file).get('block', {})
- n['block_wrapper_path'] = xml_file # inject block wrapper path
- key = n.pop('key')
-
- if key in self.blocks:
- print('Warning: Block with key "{}" already exists.\n'
- '\tIgnoring: {}'.format(key, xml_file), file=sys.stderr)
- return
-
- # Store the block
- self.blocks[key] = block = self.get_new_block(self._flow_graph, key, **n)
- self._blocks_n[key] = n
- self._docstring_extractor.query(
- key,
- block.get_imports(raw=True),
- block.get_make(raw=True)
- )
-
- def load_category_tree_xml(self, xml_file):
- """Validate and parse category tree file and add it to list"""
- ParseXML.validate_dtd(xml_file, Constants.BLOCK_TREE_DTD)
- xml = ParseXML.from_file(xml_file)
- path = []
-
- def load_category(cat_n):
- path.append(cat_n.get('name').strip())
- for block_key in cat_n.get('block', []):
- if block_key not in self._block_categories:
- self._block_categories[block_key] = list(path)
- for sub_cat_n in cat_n.get('cat', []):
- load_category(sub_cat_n)
- path.pop()
-
- load_category(xml.get('cat', {}))
-
- def load_domain_xml(self, xml_file):
- """Load a domain properties and connection templates from XML"""
- ParseXML.validate_dtd(xml_file, Constants.DOMAIN_DTD)
- n = ParseXML.from_file(xml_file).get('domain')
-
- key = n.get('key')
- if not key:
- print('Warning: Domain with emtpy key.\n\tIgnoring: {}'.format(xml_file), file=sys.stderr)
- return
- if key in self.domains: # test against repeated keys
- print('Warning: Domain with key "{}" already exists.\n\tIgnoring: {}'.format(key, xml_file), file=sys.stderr)
- return
-
- # to_bool = lambda s, d: d if s is None else s.lower() not in ('false', 'off', '0', '')
- def to_bool(s, d):
- if s is not None:
- return s.lower() not in ('false', 'off', '0', '')
- return d
-
- color = n.get('color') or ''
- try:
- chars_per_color = 2 if len(color) > 4 else 1
- tuple(int(color[o:o + 2], 16) / 255.0 for o in range(1, 3 * chars_per_color, chars_per_color))
- except ValueError:
- if color: # no color is okay, default set in GUI
- print('Warning: Can\'t parse color code "{}" for domain "{}" '.format(color, key), file=sys.stderr)
- color = None
-
- self.domains[key] = dict(
- name=n.get('name') or key,
- multiple_sinks=to_bool(n.get('multiple_sinks'), True),
- multiple_sources=to_bool(n.get('multiple_sources'), False),
- color=color
- )
- for connection_n in n.get('connection', []):
- key = (connection_n.get('source_domain'), connection_n.get('sink_domain'))
- if not all(key):
- print('Warning: Empty domain key(s) in connection template.\n\t{}'.format(xml_file), file=sys.stderr)
- elif key in self.connection_templates:
- print('Warning: Connection template "{}" already exists.\n\t{}'.format(key, xml_file), file=sys.stderr)
- else:
- self.connection_templates[key] = connection_n.get('make') or ''
-
- def _save_docstring_extraction_result(self, key, docstrings):
- docs = {}
- for match, docstring in six.iteritems(docstrings):
- if not docstring or match.endswith('_sptr'):
- continue
- docstring = docstring.replace('\n\n', '\n').strip()
- docs[match] = docstring
- self.block_docstrings[key] = docs
-
- ##############################################
- # Access
- ##############################################
-
- def parse_flow_graph(self, flow_graph_file):
- """
- Parse a saved flow graph file.
- Ensure that the file exists, and passes the dtd check.
-
- Args:
- flow_graph_file: the flow graph file
-
- Returns:
- nested data
- @throws exception if the validation fails
- """
- flow_graph_file = flow_graph_file or self.config.default_flow_graph
- open(flow_graph_file, 'r').close() # Test open
- ParseXML.validate_dtd(flow_graph_file, Constants.FLOW_GRAPH_DTD)
- return ParseXML.from_file(flow_graph_file)
-
- def get_blocks(self):
- return list(self.blocks.values())
-
- def get_generate_options(self):
- gen_opts = self.blocks['options'].get_param('generate_options')
- generate_mode_default = gen_opts.get_value()
- return [(key, name, key == generate_mode_default)
- for key, name in zip(gen_opts.options, gen_opts.options_names)]
-
- ##############################################
- # Factories
- ##############################################
- Config = Config
- Generator = Generator
- FlowGraph = FlowGraph
- Connection = Connection
- block_classes = {
- None: Block.Block, # default
- 'epy_block': Block.EPyBlock,
- '_dummy': Block.DummyBlock,
- }
- port_classes = {
- None: Port, # default
- 'clone': PortClone, # default
- }
- param_classes = {
- None: Param, # default
- }
-
- def get_new_flow_graph(self):
- return self.FlowGraph(parent=self)
-
- def get_new_block(self, parent, key, **kwargs):
- cls = self.block_classes.get(key, self.block_classes[None])
- if not kwargs:
- kwargs = self._blocks_n[key]
- return cls(parent, key=key, **kwargs)
-
- def get_new_param(self, parent, **kwargs):
- cls = self.param_classes[kwargs.pop('cls_key', None)]
- return cls(parent, **kwargs)
-
- def get_new_port(self, parent, **kwargs):
- cls = self.port_classes[kwargs.pop('cls_key', None)]
- return cls(parent, **kwargs)
diff --git a/grc/core/Port.py b/grc/core/Port.py
deleted file mode 100644
index 9ca443efa1..0000000000
--- a/grc/core/Port.py
+++ /dev/null
@@ -1,391 +0,0 @@
-"""
-Copyright 2008-2017 Free Software Foundation, Inc.
-This file is part of GNU Radio
-
-GNU Radio Companion is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-GNU Radio Companion is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-"""
-
-from __future__ import absolute_import
-
-from itertools import chain
-
-from six.moves import filter
-
-from .Element import Element, lazy_property
-from . import Constants
-
-
-class LoopError(Exception):
- pass
-
-
-def _upstream_ports(port):
- if port.is_sink:
- return _sources_from_virtual_sink_port(port)
- else:
- return _sources_from_virtual_source_port(port)
-
-
-def _sources_from_virtual_sink_port(sink_port, _traversed=None):
- """
- Resolve the source port that is connected to the given virtual sink port.
- Use the get source from virtual source to recursively resolve subsequent ports.
- """
- source_ports_per_virtual_connection = (
- # there can be multiple ports per virtual connection
- _sources_from_virtual_source_port(c.source_port, _traversed) # type: list
- for c in sink_port.get_enabled_connections()
- )
- return list(chain(*source_ports_per_virtual_connection)) # concatenate generated lists of ports
-
-
-def _sources_from_virtual_source_port(source_port, _traversed=None):
- """
- Recursively resolve source ports over the virtual connections.
- Keep track of traversed sources to avoid recursive loops.
- """
- _traversed = set(_traversed or []) # a new set!
- if source_port in _traversed:
- raise LoopError('Loop found when resolving port type')
- _traversed.add(source_port)
-
- block = source_port.parent_block
- flow_graph = source_port.parent_flow_graph
-
- if not block.is_virtual_source():
- return [source_port] # nothing to resolve, we're done
-
- stream_id = block.get_param('stream_id').get_value()
-
- # currently the validation does not allow multiple virtual sinks and one virtual source
- # but in the future it may...
- connected_virtual_sink_blocks = (
- b for b in flow_graph.iter_enabled_blocks()
- if b.is_virtual_sink() and b.get_param('stream_id').get_value() == stream_id
- )
- source_ports_per_virtual_connection = (
- _sources_from_virtual_sink_port(b.sinks[0], _traversed) # type: list
- for b in connected_virtual_sink_blocks
- )
- return list(chain(*source_ports_per_virtual_connection)) # concatenate generated lists of ports
-
-
-def _downstream_ports(port):
- if port.is_source:
- return _sinks_from_virtual_source_port(port)
- else:
- return _sinks_from_virtual_sink_port(port)
-
-
-def _sinks_from_virtual_source_port(source_port, _traversed=None):
- """
- Resolve the sink port that is connected to the given virtual source port.
- Use the get sink from virtual sink to recursively resolve subsequent ports.
- """
- sink_ports_per_virtual_connection = (
- # there can be multiple ports per virtual connection
- _sinks_from_virtual_sink_port(c.sink_port, _traversed) # type: list
- for c in source_port.get_enabled_connections()
- )
- return list(chain(*sink_ports_per_virtual_connection)) # concatenate generated lists of ports
-
-
-def _sinks_from_virtual_sink_port(sink_port, _traversed=None):
- """
- Recursively resolve sink ports over the virtual connections.
- Keep track of traversed sinks to avoid recursive loops.
- """
- _traversed = set(_traversed or []) # a new set!
- if sink_port in _traversed:
- raise LoopError('Loop found when resolving port type')
- _traversed.add(sink_port)
-
- block = sink_port.parent_block
- flow_graph = sink_port.parent_flow_graph
-
- if not block.is_virtual_sink():
- return [sink_port]
-
- stream_id = block.get_param('stream_id').get_value()
-
- connected_virtual_source_blocks = (
- b for b in flow_graph.iter_enabled_blocks()
- if b.is_virtual_source() and b.get_param('stream_id').get_value() == stream_id
- )
- sink_ports_per_virtual_connection = (
- _sinks_from_virtual_source_port(b.sources[0], _traversed) # type: list
- for b in connected_virtual_source_blocks
- )
- return list(chain(*sink_ports_per_virtual_connection)) # concatenate generated lists of ports
-
-
-class Port(Element):
-
- is_port = True
- is_clone = False
-
- def __init__(self, parent, direction, **n):
- """
- Make a new port from nested data.
-
- Args:
- block: the parent element
- n: the nested odict
- dir: the direction
- """
- self._n = n
- if n['type'] == 'message':
- n['domain'] = Constants.GR_MESSAGE_DOMAIN
-
- if 'domain' not in n:
- n['domain'] = Constants.DEFAULT_DOMAIN
- elif n['domain'] == Constants.GR_MESSAGE_DOMAIN:
- n['key'] = n['name']
- n['type'] = 'message' # For port color
-
- # Build the port
- Element.__init__(self, parent)
- # Grab the data
- self.name = n['name']
- self.key = n['key']
- self.domain = n.get('domain')
- self._type = n.get('type', '')
- self.inherit_type = not self._type
- self._hide = n.get('hide', '')
- self._dir = direction
- self._hide_evaluated = False # Updated on rewrite()
-
- self._nports = n.get('nports', '')
- self._vlen = n.get('vlen', '')
- self._optional = bool(n.get('optional'))
- self._optional_evaluated = False # Updated on rewrite()
- self.clones = [] # References to cloned ports (for nports > 1)
-
- def __str__(self):
- if self.is_source:
- return 'Source - {}({})'.format(self.name, self.key)
- if self.is_sink:
- return 'Sink - {}({})'.format(self.name, self.key)
-
- def validate(self):
- Element.validate(self)
- if self.get_type() not in Constants.TYPE_TO_SIZEOF.keys():
- self.add_error_message('Type "{}" is not a possible type.'.format(self.get_type()))
- if self.domain not in self.parent_platform.domains:
- self.add_error_message('Domain key "{}" is not registered.'.format(self.domain))
- if not self.get_enabled_connections() and not self.get_optional():
- self.add_error_message('Port is not connected.')
-
- def rewrite(self):
- """
- Handle the port cloning for virtual blocks.
- """
- del self._error_messages[:]
- if self.inherit_type:
- self.resolve_empty_type()
-
- hide = self.parent_block.resolve_dependencies(self._hide).strip().lower()
- self._hide_evaluated = False if hide in ('false', 'off', '0') else bool(hide)
- optional = self.parent_block.resolve_dependencies(self._optional).strip().lower()
- self._optional_evaluated = False if optional in ('false', 'off', '0') else bool(optional)
-
- # Update domain if was deduced from (dynamic) port type
- type_ = self.get_type()
- if self.domain == Constants.GR_STREAM_DOMAIN and type_ == "message":
- self.domain = Constants.GR_MESSAGE_DOMAIN
- self.key = self.name
- if self.domain == Constants.GR_MESSAGE_DOMAIN and type_ != "message":
- self.domain = Constants.GR_STREAM_DOMAIN
- self.key = '0' # Is rectified in rewrite()
-
- def resolve_virtual_source(self):
- """Only used by Generator after validation is passed"""
- return _upstream_ports(self)
-
- def resolve_empty_type(self):
- def find_port(finder):
- try:
- return next((p for p in finder(self) if not p.inherit_type), None)
- except LoopError as error:
- self.add_error_message(str(error))
- except (StopIteration, Exception) as error:
- pass
-
- try:
- port = find_port(_upstream_ports) or find_port(_downstream_ports)
- self._type = str(port.get_type())
- self._vlen = str(port.get_vlen())
- except Exception:
- # Reset type and vlen
- self._type = self._vlen = ''
-
- def get_vlen(self):
- """
- Get the vector length.
- If the evaluation of vlen cannot be cast to an integer, return 1.
-
- Returns:
- the vector length or 1
- """
- vlen = self.parent_block.resolve_dependencies(self._vlen)
- try:
- return max(1, int(self.parent_flowgraph.evaluate(vlen)))
- except:
- return 1
-
- def get_nports(self):
- """
- Get the number of ports.
- If already blank, return a blank
- If the evaluation of nports cannot be cast to a positive integer, return 1.
-
- Returns:
- the number of ports or 1
- """
- if self._nports == '':
- return 1
-
- nports = self.parent_block.resolve_dependencies(self._nports)
- try:
- return max(1, int(self.parent_flowgraph.evaluate(nports)))
- except:
- return 1
-
- def get_optional(self):
- return self._optional_evaluated
-
- def add_clone(self):
- """
- Create a clone of this (master) port and store a reference in self._clones.
-
- The new port name (and key for message ports) will have index 1... appended.
- If this is the first clone, this (master) port will get a 0 appended to its name (and key)
-
- Returns:
- the cloned port
- """
- # Add index to master port name if there are no clones yet
- if not self.clones:
- self.name = self._n['name'] + '0'
- # Also update key for none stream ports
- if not self.key.isdigit():
- self.key = self.name
-
- name = self._n['name'] + str(len(self.clones) + 1)
- # Dummy value 99999 will be fixed later
- key = '99999' if self.key.isdigit() else name
-
- # Clone
- port_factory = self.parent_platform.get_new_port
- port = port_factory(self.parent, direction=self._dir,
- name=name, key=key,
- master=self, cls_key='clone')
-
- self.clones.append(port)
- return port
-
- def remove_clone(self, port):
- """
- Remove a cloned port (from the list of clones only)
- Remove the index 0 of the master port name (and key9 if there are no more clones left
- """
- self.clones.remove(port)
- # Remove index from master port name if there are no more clones
- if not self.clones:
- self.name = self._n['name']
- # Also update key for none stream ports
- if not self.key.isdigit():
- self.key = self.name
-
- @lazy_property
- def is_sink(self):
- return self._dir == 'sink'
-
- @lazy_property
- def is_source(self):
- return self._dir == 'source'
-
- def get_type(self):
- return self.parent_block.resolve_dependencies(self._type)
-
- def get_hide(self):
- return self._hide_evaluated
-
- def get_connections(self):
- """
- Get all connections that use this port.
-
- Returns:
- a list of connection objects
- """
- connections = self.parent_flowgraph.connections
- connections = [c for c in connections if c.source_port is self or c.sink_port is self]
- return connections
-
- def get_enabled_connections(self):
- """
- Get all enabled connections that use this port.
-
- Returns:
- a list of connection objects
- """
- return [c for c in self.get_connections() if c.enabled]
-
- def get_associated_ports(self):
- if not self.get_type() == 'bus':
- return [self]
-
- block = self.parent_block
- if self.is_source:
- block_ports = block.sources
- bus_structure = block.current_bus_structure['source']
- else:
- block_ports = block.sinks
- bus_structure = block.current_bus_structure['sink']
-
- ports = [i for i in block_ports if not i.get_type() == 'bus']
- if bus_structure:
- bus_index = [i for i in block_ports if i.get_type() == 'bus'].index(self)
- ports = [p for i, p in enumerate(ports) if i in bus_structure[bus_index]]
- return ports
-
-
-class PortClone(Port):
-
- is_clone = True
-
- def __init__(self, parent, direction, master, name, key):
- """
- Make a new port from nested data.
-
- Args:
- block: the parent element
- n: the nested odict
- dir: the direction
- """
- Element.__init__(self, parent)
- self.master = master
- self.name = name
- self._key = key
- self._nports = '1'
-
- def __getattr__(self, item):
- return getattr(self.master, item)
-
- def add_clone(self):
- raise NotImplementedError()
-
- def remove_clone(self, port):
- raise NotImplementedError()
diff --git a/grc/core/Element.py b/grc/core/base.py
index 86e0746655..e5ff657d85 100644
--- a/grc/core/Element.py
+++ b/grc/core/base.py
@@ -16,28 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
import weakref
-import functools
-
-class lazy_property(object):
-
- def __init__(self, func):
- self.func = func
- functools.update_wrapper(self, func)
-
- def __get__(self, instance, cls):
- if instance is None:
- return self
- value = self.func(instance)
- setattr(instance, self.func.__name__, value)
- return value
-
-
-def nop_write(prop):
- """Make this a property with a nop setter"""
- def nop(self, value):
- pass
- return prop.setter(nop)
+from .utils.descriptors import lazy_property
class Element(object):
@@ -56,7 +36,7 @@ class Element(object):
"""
del self._error_messages[:]
- for child in self.get_children():
+ for child in self.children():
child.validate()
def is_valid(self):
@@ -97,7 +77,7 @@ class Element(object):
"""
for msg in self._error_messages:
yield self, msg
- for child in self.get_children():
+ for child in self.children():
if not child.enabled or child.get_bypassed():
continue
for element_msg in child.iter_error_messages():
@@ -108,7 +88,7 @@ class Element(object):
Rewrite this element and call rewrite on all children.
Call this base method before rewriting the element.
"""
- for child in self.get_children():
+ for child in self.children():
child.rewrite()
@property
@@ -136,7 +116,7 @@ class Element(object):
@lazy_property
def parent_platform(self):
- from .Platform import Platform
+ from .platform import Platform
return self.get_parent_by_type(Platform)
@lazy_property
@@ -146,7 +126,7 @@ class Element(object):
@lazy_property
def parent_block(self):
- from .Block import Block
+ from .blocks import Block
return self.get_parent_by_type(Block)
def reset_parents_by_type(self):
@@ -155,26 +135,30 @@ class Element(object):
if isinstance(obj, lazy_property):
delattr(self, name)
- def get_children(self):
- return list()
+ def children(self):
+ return
+ yield # empty generator
##############################################
# Type testing
##############################################
- is_platform = False
-
is_flow_graph = False
-
is_block = False
-
is_dummy_block = False
-
is_connection = False
-
is_port = False
-
is_param = False
-
is_variable = False
-
is_import = False
+
+ def get_raw(self, name):
+ descriptor = getattr(self.__class__, name, None)
+ if not descriptor:
+ raise ValueError("No evaluated property '{}' found".format(name))
+ return getattr(self, descriptor.name_raw, None) or getattr(self, descriptor.name, None)
+
+ def set_evaluated(self, name, value):
+ descriptor = getattr(self.__class__, name, None)
+ if not descriptor:
+ raise ValueError("No evaluated property '{}' found".format(name))
+ self.__dict__[descriptor.name] = value
diff --git a/grc/core/block.dtd b/grc/core/block.dtd
deleted file mode 100644
index 145f4d8610..0000000000
--- a/grc/core/block.dtd
+++ /dev/null
@@ -1,69 +0,0 @@
-<!--
-Copyright 2008 Free Software Foundation, Inc.
-This file is part of GNU Radio
-
-GNU Radio Companion is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-GNU Radio Companion is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
--->
-<!--
- gnuradio_python.blocks.dtd
- Josh Blum
- The document type definition for blocks.
- -->
-<!--
- Top level element.
- A block contains a name, ...parameters list, and list of IO ports.
- -->
-<!ELEMENT block (name, key, category?, throttle?, flags?, import*, var_make?, var_value?,
- make, callback*, param_tab_order?, param*, bus_sink?, bus_source?, check*,
- sink*, source*, bus_structure_sink?, bus_structure_source?, doc?, grc_source?)>
-<!--
- Sub level elements.
- -->
-<!ELEMENT param_tab_order (tab+)>
-<!ELEMENT param (base_key?, name, key, value?, type?, hide?, option*, tab?)>
-<!ELEMENT option (name, key, opt*)>
-<!ELEMENT sink (name, type, vlen?, domain?, nports?, optional?, hide?)>
-<!ELEMENT source (name, type, vlen?, domain?, nports?, optional?, hide?)>
-<!--
- Bottom level elements.
- Character data only.
- -->
-<!ELEMENT category (#PCDATA)>
-<!ELEMENT import (#PCDATA)>
-<!ELEMENT doc (#PCDATA)>
-<!ELEMENT grc_source (#PCDATA)>
-<!ELEMENT tab (#PCDATA)>
-<!ELEMENT name (#PCDATA)>
-<!ELEMENT base_key (#PCDATA)>
-<!ELEMENT key (#PCDATA)>
-<!ELEMENT check (#PCDATA)>
-<!ELEMENT bus_sink (#PCDATA)>
-<!ELEMENT bus_source (#PCDATA)>
-<!ELEMENT opt (#PCDATA)>
-<!ELEMENT type (#PCDATA)>
-<!ELEMENT hide (#PCDATA)>
-<!ELEMENT vlen (#PCDATA)>
-<!ELEMENT domain (#PCDATA)>
-<!ELEMENT nports (#PCDATA)>
-<!ELEMENT bus_structure_sink (#PCDATA)>
-<!ELEMENT bus_structure_source (#PCDATA)>
-<!ELEMENT var_make (#PCDATA)>
-<!ELEMENT var_value (#PCDATA)>
-<!ELEMENT make (#PCDATA)>
-<!ELEMENT value (#PCDATA)>
-<!ELEMENT callback (#PCDATA)>
-<!ELEMENT optional (#PCDATA)>
-<!ELEMENT throttle (#PCDATA)>
-<!ELEMENT flags (#PCDATA)>
diff --git a/grc/core/Element.pyi b/grc/core/blocks/__init__.py
index 2a2aed401c..e4a085d477 100644
--- a/grc/core/Element.pyi
+++ b/grc/core/blocks/__init__.py
@@ -15,27 +15,23 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-from typing import Union
+from __future__ import absolute_import
-from . import Platform, FlowGraph, Block
+from ._flags import Flags
+from ._templates import MakoTemplates
-lazy_property = property # fixme: descriptors don't seems to be supported
+from .block import Block
+from ._build import build
-class Element(object):
- def __init__(self, parent: Union[None, 'Element'] = None): ...
+build_ins = {}
- @lazy_property
- def parent(self) -> 'Element': ...
- def get_parent_by_type(self, cls) -> Union[None, 'Element']: ...
+def register_build_in(cls):
+ build_ins[cls.key] = cls
+ return cls
- @lazy_property
- def parent_platform(self) -> Platform.Platform: ...
-
- @lazy_property
- def parent_flowgraph(self) -> FlowGraph.FlowGraph: ...
-
- @lazy_property
- def parent_block(self) -> Block.Block: ...
+from .dummy import DummyBlock
+from .embedded_python import EPyBlock, EPyModule
+from .virtual import VirtualSink, VirtualSource
diff --git a/grc/core/blocks/_build.py b/grc/core/blocks/_build.py
new file mode 100644
index 0000000000..9a50086cea
--- /dev/null
+++ b/grc/core/blocks/_build.py
@@ -0,0 +1,69 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+import re
+
+from .block import Block
+from ._flags import Flags
+from ._templates import MakoTemplates
+
+
+def build(id, label='', category='', flags='', documentation='',
+ checks=None, value=None,
+ parameters=None, inputs=None, outputs=None, templates=None, **kwargs):
+ block_id = id
+
+ cls = type(block_id, (Block,), {})
+ cls.key = block_id
+
+ cls.label = label or block_id.title()
+ cls.category = [cat.strip() for cat in category.split('/') if cat.strip()]
+
+ cls.flags = Flags(flags)
+ if re.match(r'options$|variable|virtual', block_id):
+ cls.flags += Flags.NOT_DSP + Flags.DISABLE_BYPASS
+
+ cls.documentation = {'': documentation.strip('\n\t ').replace('\\\n', '')}
+
+ cls.checks = [_single_mako_expr(check, block_id) for check in (checks or [])]
+
+ cls.parameters_data = parameters or []
+ cls.inputs_data = inputs or []
+ cls.outputs_data = outputs or []
+ cls.extra_data = kwargs
+
+ templates = templates or {}
+ cls.templates = MakoTemplates(
+ imports=templates.get('imports', ''),
+ make=templates.get('make', ''),
+ callbacks=templates.get('callbacks', []),
+ var_make=templates.get('var_make', ''),
+ )
+ # todo: MakoTemplates.compile() to check for errors
+
+ cls.value = _single_mako_expr(value, block_id)
+
+ return cls
+
+
+def _single_mako_expr(value, block_id):
+ match = re.match(r'\s*\$\{\s*(.*?)\s*\}\s*', str(value))
+ if value and not match:
+ raise ValueError('{} is not a mako substitution in {}'.format(value, block_id))
+ return match.group(1) if match else None
diff --git a/grc/core/blocks/_flags.py b/grc/core/blocks/_flags.py
new file mode 100644
index 0000000000..ffea2ad569
--- /dev/null
+++ b/grc/core/blocks/_flags.py
@@ -0,0 +1,39 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+import six
+
+
+class Flags(six.text_type):
+
+ THROTTLE = 'throttle'
+ DISABLE_BYPASS = 'disable_bypass'
+ NEED_QT_GUI = 'need_qt_gui'
+ DEPRECATED = 'deprecated'
+ NOT_DSP = 'not_dsp'
+
+ def __getattr__(self, item):
+ return item in self
+
+ def __add__(self, other):
+ if not isinstance(other, six.string_types):
+ return NotImplemented
+ return self.__class__(str(self) + other)
+
+ __iadd__ = __add__
diff --git a/grc/core/blocks/_templates.py b/grc/core/blocks/_templates.py
new file mode 100644
index 0000000000..0b15166423
--- /dev/null
+++ b/grc/core/blocks/_templates.py
@@ -0,0 +1,77 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+This dict class holds a (shared) cache of compiled mako templates.
+These
+
+"""
+from __future__ import absolute_import, print_function
+
+from mako.template import Template
+from mako.exceptions import SyntaxException
+
+from ..errors import TemplateError
+
+
+class MakoTemplates(dict):
+
+ _template_cache = {}
+
+ def __init__(self, _bind_to=None, *args, **kwargs):
+ self.instance = _bind_to
+ dict.__init__(self, *args, **kwargs)
+
+ def __get__(self, instance, owner):
+ if instance is None or self.instance is not None:
+ return self
+ copy = self.__class__(_bind_to=instance, **self)
+ if getattr(instance.__class__, 'templates', None) is self:
+ setattr(instance, 'templates', copy)
+ return copy
+
+ @classmethod
+ def compile(cls, text):
+ text = str(text)
+ try:
+ template = Template(text)
+ except SyntaxException as error:
+ raise TemplateError(text, *error.args)
+
+ cls._template_cache[text] = template
+ return template
+
+ def _get_template(self, text):
+ try:
+ return self._template_cache[str(text)]
+ except KeyError:
+ return self.compile(text)
+
+ def render(self, item):
+ text = self.get(item)
+ if not text:
+ return ''
+ namespace = self.instance.namespace_templates
+
+ try:
+ if isinstance(text, list):
+ templates = (self._get_template(t) for t in text)
+ return [template.render(**namespace) for template in templates]
+ else:
+ template = self._get_template(text)
+ return template.render(**namespace)
+ except Exception as error:
+ raise TemplateError(error, text)
diff --git a/grc/core/blocks/block.py b/grc/core/blocks/block.py
new file mode 100644
index 0000000000..e6695083a1
--- /dev/null
+++ b/grc/core/blocks/block.py
@@ -0,0 +1,416 @@
+"""
+Copyright 2008-2015 Free Software Foundation, Inc.
+This file is part of GNU Radio
+
+GNU Radio Companion is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+GNU Radio Companion is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+
+from __future__ import absolute_import
+
+import ast
+import collections
+import itertools
+
+import six
+from six.moves import range
+
+from ._templates import MakoTemplates
+from ._flags import Flags
+
+from ..Constants import ADVANCED_PARAM_TAB
+from ..base import Element
+from ..utils.descriptors import lazy_property
+
+
+def _get_elem(iterable, key):
+ items = list(iterable)
+ for item in items:
+ if item.key == key:
+ return item
+ return ValueError('Key "{}" not found in {}.'.format(key, items))
+
+
+class Block(Element):
+
+ is_block = True
+
+ STATE_LABELS = ['disabled', 'enabled', 'bypassed']
+
+ key = ''
+ label = ''
+ category = ''
+ flags = Flags('')
+ documentation = {'': ''}
+
+ value = None
+ checks = []
+
+ templates = MakoTemplates()
+ parameters_data = []
+ inputs_data = []
+ outputs_data = []
+
+ extra_data = {}
+
+ # region Init
+ def __init__(self, parent):
+ """Make a new block from nested data."""
+ super(Block, self).__init__(parent)
+ self.params = self._init_params()
+ self.sinks = self._init_ports(self.inputs_data, direction='sink')
+ self.sources = self._init_ports(self.outputs_data, direction='source')
+
+ self.active_sources = [] # on rewrite
+ self.active_sinks = [] # on rewrite
+
+ self.states = {'state': True}
+
+ def _init_params(self):
+ is_dsp_block = not self.flags.not_dsp
+ has_inputs = bool(self.inputs_data)
+ has_outputs = bool(self.outputs_data)
+
+ params = collections.OrderedDict()
+ param_factory = self.parent_platform.make_param
+
+ def add_param(id, **kwargs):
+ params[id] = param_factory(self, id=id, **kwargs)
+
+ add_param(id='id', name='ID', dtype='id',
+ hide='none' if (self.key == 'options' or self.is_variable) else 'part')
+
+ if is_dsp_block:
+ add_param(id='alias', name='Block Alias', dtype='string',
+ hide='part', category=ADVANCED_PARAM_TAB)
+
+ if has_outputs or has_inputs:
+ add_param(id='affinity', name='Core Affinity', dtype='int_vector',
+ hide='part', category=ADVANCED_PARAM_TAB)
+
+ if has_outputs:
+ add_param(id='minoutbuf', name='Min Output Buffer', dtype='int',
+ hide='part', value='0', category=ADVANCED_PARAM_TAB)
+ add_param(id='maxoutbuf', name='Max Output Buffer', dtype='int',
+ hide='part', value='0', category=ADVANCED_PARAM_TAB)
+
+ base_params_n = {}
+ for param_data in self.parameters_data:
+ param_id = param_data['id']
+ if param_id in params:
+ raise Exception('Param id "{}" is not unique'.format(param_id))
+
+ base_key = param_data.get('base_key', None)
+ param_data_ext = base_params_n.get(base_key, {}).copy()
+ param_data_ext.update(param_data)
+
+ add_param(**param_data_ext)
+ base_params_n[param_id] = param_data_ext
+
+ add_param(id='comment', name='Comment', dtype='_multiline', hide='part',
+ value='', category=ADVANCED_PARAM_TAB)
+ return params
+
+ def _init_ports(self, ports_n, direction):
+ ports = []
+ port_factory = self.parent_platform.make_port
+ port_ids = set()
+
+ def make_stream_port_id(_pool=itertools.count()):
+ return {'sink': 'in', 'source': 'out'}[direction] + str(next(_pool))
+
+ for i, port_data in enumerate(ports_n):
+ port_id = port_data.setdefault('id', make_stream_port_id())
+ if port_id in port_ids:
+ raise Exception('Port id "{}" already exists in {}s'.format(port_id, direction))
+ port_ids.add(port_id)
+
+ port = port_factory(parent=self, direction=direction, **port_data)
+ ports.append(port)
+ return ports
+ # endregion
+
+ # region Rewrite_and_Validation
+ def rewrite(self):
+ """
+ Add and remove ports to adjust for the nports.
+ """
+ Element.rewrite(self)
+
+ def rekey(ports):
+ """Renumber non-message/message ports"""
+ domain_specific_port_index = collections.defaultdict(int)
+ for port in ports:
+ if not port.key.isdigit():
+ continue
+ domain = port.domain
+ port.key = str(domain_specific_port_index[domain])
+ domain_specific_port_index[domain] += 1
+
+ # Adjust nports
+ for ports in (self.sources, self.sinks):
+ self._rewrite_nports(ports)
+ rekey(ports)
+
+ # disconnect hidden ports
+ self.parent_flowgraph.disconnect(*[p for p in self.ports() if p.hidden])
+
+ self.active_sources = [p for p in self.sources if not p.hidden]
+ self.active_sinks = [p for p in self.sinks if not p.hidden]
+
+ def _rewrite_nports(self, ports):
+ for port in ports:
+ if hasattr(port, 'master_port'): # Not a master port and no left-over clones
+ continue
+ nports = port.multiplicity
+ for clone in port.clones[nports-1:]:
+ # Remove excess connections
+ self.parent_flowgraph.disconnect(clone)
+ port.remove_clone(clone)
+ ports.remove(clone)
+ # Add more cloned ports
+ for j in range(1 + len(port.clones), nports):
+ clone = port.add_clone()
+ ports.insert(ports.index(port) + j, clone)
+
+ def validate(self):
+ """
+ Validate this block.
+ Call the base class validate.
+ Evaluate the checks: each check must evaluate to True.
+ """
+ Element.validate(self)
+ self._run_checks()
+ self._validate_generate_mode_compat()
+ self._validate_var_value()
+
+ def _run_checks(self):
+ """Evaluate the checks"""
+ for check in self.checks:
+ try:
+ if not self.evaluate(check):
+ self.add_error_message('Check "{}" failed.'.format(check))
+ except:
+ self.add_error_message('Check "{}" did not evaluate.'.format(check))
+
+ def _validate_generate_mode_compat(self):
+ """check if this is a GUI block and matches the selected generate option"""
+ current_generate_option = self.parent.get_option('generate_options')
+
+ def check_generate_mode(label, flag, valid_options):
+ block_requires_mode = (
+ flag in self.flags or self.label.upper().startswith(label)
+ )
+ if block_requires_mode and current_generate_option not in valid_options:
+ self.add_error_message("Can't generate this block in mode: {} ".format(
+ repr(current_generate_option)))
+
+ check_generate_mode('QT GUI', Flags.NEED_QT_GUI, ('qt_gui', 'hb_qt_gui'))
+
+ def _validate_var_value(self):
+ """or variables check the value (only if var_value is used)"""
+ if self.is_variable and self.value != 'value':
+ try:
+ self.parent_flowgraph.evaluate(self.value, local_namespace=self.namespace)
+ except Exception as err:
+ self.add_error_message('Value "{}" cannot be evaluated:\n{}'.format(self.value, err))
+ # endregion
+
+ # region Properties
+
+ def __str__(self):
+ return 'Block - {} - {}({})'.format(self.name, self.label, self.key)
+
+ def __repr__(self):
+ try:
+ name = self.name
+ except Exception:
+ name = self.key
+ return 'block[' + name + ']'
+
+ @property
+ def name(self):
+ return self.params['id'].value
+
+ @lazy_property
+ def is_virtual_or_pad(self):
+ return self.key in ("virtual_source", "virtual_sink", "pad_source", "pad_sink")
+
+ @lazy_property
+ def is_variable(self):
+ return bool(self.value)
+
+ @lazy_property
+ def is_import(self):
+ return self.key == 'import'
+
+ @property
+ def comment(self):
+ return self.params['comment'].value
+
+ @property
+ def state(self):
+ """Gets the block's current state."""
+ state = self.states['state']
+ return state if state in self.STATE_LABELS else 'enabled'
+
+ @state.setter
+ def state(self, value):
+ """Sets the state for the block."""
+ self.states['state'] = value
+
+ # Enable/Disable Aliases
+ @property
+ def enabled(self):
+ """Get the enabled state of the block"""
+ return self.state != 'disabled'
+
+ # endregion
+
+ ##############################################
+ # Getters (old)
+ ##############################################
+ def get_var_make(self):
+ return self.templates.render('var_make')
+
+ def get_var_value(self):
+ return self.templates.render('var_value')
+
+ def get_callbacks(self):
+ """
+ Get a list of function callbacks for this block.
+
+ Returns:
+ a list of strings
+ """
+ def make_callback(callback):
+ if 'self.' in callback:
+ return callback
+ return 'self.{}.{}'.format(self.name, callback)
+ return [make_callback(c) for c in self.templates.render('callbacks')]
+
+ def is_virtual_sink(self):
+ return self.key == 'virtual_sink'
+
+ def is_virtual_source(self):
+ return self.key == 'virtual_source'
+
+ # Block bypassing
+ def get_bypassed(self):
+ """
+ Check if the block is bypassed
+ """
+ return self.state == 'bypassed'
+
+ def set_bypassed(self):
+ """
+ Bypass the block
+
+ Returns:
+ True if block chagnes state
+ """
+ if self.state != 'bypassed' and self.can_bypass():
+ self.state = 'bypassed'
+ return True
+ return False
+
+ def can_bypass(self):
+ """ Check the number of sinks and sources and see if this block can be bypassed """
+ # Check to make sure this is a single path block
+ # Could possibly support 1 to many blocks
+ if len(self.sources) != 1 or len(self.sinks) != 1:
+ return False
+ if not (self.sources[0].dtype == self.sinks[0].dtype):
+ return False
+ if self.flags.disable_bypass:
+ return False
+ return True
+
+ def ports(self):
+ return itertools.chain(self.sources, self.sinks)
+
+ def active_ports(self):
+ return itertools.chain(self.active_sources, self.active_sinks)
+
+ def children(self):
+ return itertools.chain(six.itervalues(self.params), self.ports())
+
+ ##############################################
+ # Access
+ ##############################################
+
+ def get_sink(self, key):
+ return _get_elem(self.sinks, key)
+
+ def get_source(self, key):
+ return _get_elem(self.sources, key)
+
+ ##############################################
+ # Resolve
+ ##############################################
+ @property
+ def namespace(self):
+ return {key: param.get_evaluated() for key, param in six.iteritems(self.params)}
+
+ @property
+ def namespace_templates(self):
+ return {key: param.template_arg for key, param in six.iteritems(self.params)}
+
+ def evaluate(self, expr):
+ return self.parent_flowgraph.evaluate(expr, self.namespace)
+
+ ##############################################
+ # Import/Export Methods
+ ##############################################
+ def export_data(self):
+ """
+ Export this block's params to nested data.
+
+ Returns:
+ a nested data odict
+ """
+ data = collections.OrderedDict()
+ if self.key != 'options':
+ data['name'] = self.name
+ data['id'] = self.key
+ data['parameters'] = collections.OrderedDict(sorted(
+ (param_id, param.value) for param_id, param in self.params.items()
+ if param_id != 'id'
+ ))
+ data['states'] = collections.OrderedDict(sorted(self.states.items()))
+ return data
+
+ def import_data(self, name, states, parameters, **_):
+ """
+ Import this block's params from nested data.
+ Any param keys that do not exist will be ignored.
+ Since params can be dynamically created based another param,
+ call rewrite, and repeat the load until the params stick.
+ """
+ self.params['id'].value = name
+ self.states.update(states)
+
+ def get_hash():
+ return hash(tuple(hash(v) for v in self.params.values()))
+
+ pre_rewrite_hash = -1
+ while pre_rewrite_hash != get_hash():
+ for key, value in six.iteritems(parameters):
+ try:
+ self.params[key].set_value(value)
+ except KeyError:
+ continue
+ # Store hash and call rewrite
+ pre_rewrite_hash = get_hash()
+ self.rewrite()
diff --git a/grc/core/blocks/dummy.py b/grc/core/blocks/dummy.py
new file mode 100644
index 0000000000..6a5ec2fa72
--- /dev/null
+++ b/grc/core/blocks/dummy.py
@@ -0,0 +1,54 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from . import Block, register_build_in
+
+
+@register_build_in
+class DummyBlock(Block):
+
+ is_dummy_block = True
+
+ label = 'Missing Block'
+ key = '_dummy'
+
+ def __init__(self, parent, missing_block_id, parameters, **_):
+ self.key = missing_block_id
+ super(DummyBlock, self).__init__(parent=parent)
+
+ param_factory = self.parent_platform.make_param
+ for param_id in parameters:
+ self.params.setdefault(param_id, param_factory(parent=self, id=param_id, dtype='string'))
+
+ def is_valid(self):
+ return False
+
+ @property
+ def enabled(self):
+ return False
+
+ def add_missing_port(self, port_id, direction):
+ port = self.parent_platform.make_port(
+ parent=self, direction=direction, id=port_id, name='?', dtype='',
+ )
+ if port.is_source:
+ self.sources.append(port)
+ else:
+ self.sinks.append(port)
+ return port
diff --git a/grc/core/blocks/embedded_python.py b/grc/core/blocks/embedded_python.py
new file mode 100644
index 0000000000..0b5a7a21c5
--- /dev/null
+++ b/grc/core/blocks/embedded_python.py
@@ -0,0 +1,242 @@
+# Copyright 2015-16 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from ast import literal_eval
+from textwrap import dedent
+
+from . import Block, register_build_in
+from ._templates import MakoTemplates
+
+from .. import utils
+from ..base import Element
+
+
+DEFAULT_CODE = '''\
+"""
+Embedded Python Blocks:
+
+Each time this file is saved, GRC will instantiate the first class it finds
+to get ports and parameters of your block. The arguments to __init__ will
+be the parameters. All of them are required to have default values!
+"""
+
+import numpy as np
+from gnuradio import gr
+
+
+class blk(gr.sync_block): # other base classes are basic_block, decim_block, interp_block
+ """Embedded Python Block example - a simple multiply const"""
+
+ def __init__(self, example_param=1.0): # only default arguments here
+ """arguments to this function show up as parameters in GRC"""
+ gr.sync_block.__init__(
+ self,
+ name='Embedded Python Block', # will show up in GRC
+ in_sig=[np.complex64],
+ out_sig=[np.complex64]
+ )
+ # if an attribute with the same name as a parameter is found,
+ # a callback is registered (properties work, too).
+ self.example_param = example_param
+
+ def work(self, input_items, output_items):
+ """example: multiply with constant"""
+ output_items[0][:] = input_items[0] * self.example_param
+ return len(output_items[0])
+'''
+
+DOC = """
+This block represents an arbitrary GNU Radio Python Block.
+
+Its source code can be accessed through the parameter 'Code' which opens your editor. \
+Each time you save changes in the editor, GRC will update the block. \
+This includes the number, names and defaults of the parameters, \
+the ports (stream and message) and the block name and documentation.
+
+Block Documentation:
+(will be replaced the docstring of your block class)
+"""
+
+
+@register_build_in
+class EPyBlock(Block):
+
+ key = 'epy_block'
+ label = 'Python Block'
+ documentation = {'': DOC}
+
+ parameters_data = [dict(
+ label='Code',
+ id='_source_code',
+ dtype='_multiline_python_external',
+ value=DEFAULT_CODE,
+ hide='part',
+ )]
+ inputs_data = []
+ outputs_data = []
+
+ def __init__(self, flow_graph, **kwargs):
+ super(EPyBlock, self).__init__(flow_graph, **kwargs)
+ self.states['_io_cache'] = ''
+
+ self._epy_source_hash = -1
+ self._epy_reload_error = None
+
+ def rewrite(self):
+ Element.rewrite(self)
+
+ param_src = self.params['_source_code']
+
+ src = param_src.get_value()
+ src_hash = hash((self.name, src))
+ if src_hash == self._epy_source_hash:
+ return
+
+ try:
+ blk_io = utils.epy_block_io.extract(src)
+
+ except Exception as e:
+ self._epy_reload_error = ValueError(str(e))
+ try: # Load last working block io
+ blk_io_args = literal_eval(self.states['_io_cache'])
+ if len(blk_io_args) == 6:
+ blk_io_args += ([],) # add empty callbacks
+ blk_io = utils.epy_block_io.BlockIO(*blk_io_args)
+ except Exception:
+ return
+ else:
+ self._epy_reload_error = None # Clear previous errors
+ self.states['_io_cache'] = repr(tuple(blk_io))
+
+ # print "Rewriting embedded python block {!r}".format(self.name)
+ self._epy_source_hash = src_hash
+
+ self.label = blk_io.name or blk_io.cls
+ self.documentation = {'': blk_io.doc}
+
+ self.templates['imports'] = 'import ' + self.name
+ self.templates['make'] = '{mod}.{cls}({args})'.format(
+ mod=self.name,
+ cls=blk_io.cls,
+ args=', '.join('{0}=${{ {0} }}'.format(key) for key, _ in blk_io.params))
+ self.templates['callbacks'] = [
+ '{0} = ${{ {0} }}'.format(attr) for attr in blk_io.callbacks
+ ]
+
+ self._update_params(blk_io.params)
+ self._update_ports('in', self.sinks, blk_io.sinks, 'sink')
+ self._update_ports('out', self.sources, blk_io.sources, 'source')
+
+ super(EPyBlock, self).rewrite()
+
+ def _update_params(self, params_in_src):
+ param_factory = self.parent_platform.make_param
+ params = {}
+ for param in list(self.params):
+ if hasattr(param, '__epy_param__'):
+ params[param.key] = param
+ del self.params[param.key]
+
+ for id_, value in params_in_src:
+ try:
+ param = params[id_]
+ if param.default == param.value:
+ param.set_value(value)
+ param.default = str(value)
+ except KeyError: # need to make a new param
+ param = param_factory(
+ parent=self, id=id_, dtype='raw', value=value,
+ name=id_.replace('_', ' ').title(),
+ )
+ setattr(param, '__epy_param__', True)
+ self.params[id_] = param
+
+ def _update_ports(self, label, ports, port_specs, direction):
+ port_factory = self.parent_platform.make_port
+ ports_to_remove = list(ports)
+ iter_ports = iter(ports)
+ ports_new = []
+ port_current = next(iter_ports, None)
+ for key, port_type, vlen in port_specs:
+ reuse_port = (
+ port_current is not None and
+ port_current.dtype == port_type and
+ port_current.vlen == vlen and
+ (key.isdigit() or port_current.key == key)
+ )
+ if reuse_port:
+ ports_to_remove.remove(port_current)
+ port, port_current = port_current, next(iter_ports, None)
+ else:
+ n = dict(name=label + str(key), dtype=port_type, id=key)
+ if port_type == 'message':
+ n['name'] = key
+ n['optional'] = '1'
+ if vlen > 1:
+ n['vlen'] = str(vlen)
+ port = port_factory(self, direction=direction, **n)
+ ports_new.append(port)
+ # replace old port list with new one
+ del ports[:]
+ ports.extend(ports_new)
+ # remove excess port connections
+ self.parent_flowgraph.disconnect(*ports_to_remove)
+
+ def validate(self):
+ super(EPyBlock, self).validate()
+ if self._epy_reload_error:
+ self.params['_source_code'].add_error_message(str(self._epy_reload_error))
+
+
+@register_build_in
+class EPyModule(Block):
+ key = 'epy_module'
+ label = 'Python Module'
+ documentation = {'': dedent("""
+ This block lets you embed a python module in your flowgraph.
+
+ Code you put in this module is accessible in other blocks using the ID of this
+ block. Example:
+
+ If you put
+
+ a = 2
+
+ def double(arg):
+ return 2 * arg
+
+ in a Python Module Block with the ID 'stuff' you can use code like
+
+ stuff.a # evals to 2
+ stuff.double(3) # evals to 6
+
+ to set parameters of other blocks in your flowgraph.
+ """)}
+
+ parameters_data = [dict(
+ label='Code',
+ id='source_code',
+ dtype='_multiline_python_external',
+ value='# this module will be imported in the into your flowgraph',
+ hide='part',
+ )]
+
+ templates = MakoTemplates(
+ imports='import ${ id } # embedded python module',
+ )
diff --git a/grc/core/blocks/virtual.py b/grc/core/blocks/virtual.py
new file mode 100644
index 0000000000..a10853ad1b
--- /dev/null
+++ b/grc/core/blocks/virtual.py
@@ -0,0 +1,76 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+import itertools
+
+from . import Block, register_build_in
+
+
+@register_build_in
+class VirtualSink(Block):
+ count = itertools.count()
+
+ key = 'virtual_sink'
+ label = 'Virtual Sink'
+ documentation = {'': ''}
+
+ parameters_data = [dict(
+ label='Stream ID',
+ id='stream_id',
+ dtype='stream_id',
+ )]
+ inputs_data = [dict(
+ domain='stream',
+ dtype=''
+ )]
+
+ def __init__(self, parent, **kwargs):
+ super(VirtualSink, self).__init__(parent, **kwargs)
+ self.params['id'].hide = 'all'
+
+ @property
+ def stream_id(self):
+ return self.params['stream_id'].value
+
+
+@register_build_in
+class VirtualSource(Block):
+ count = itertools.count()
+
+ key = 'virtual_source'
+ label = 'Virtual Source'
+ documentation = {'': ''}
+
+ parameters_data = [dict(
+ label='Stream ID',
+ id='stream_id',
+ dtype='stream_id',
+ )]
+ outputs_data = [dict(
+ domain='stream',
+ dtype=''
+ )]
+
+ def __init__(self, parent, **kwargs):
+ super(VirtualSource, self).__init__(parent, **kwargs)
+ self.params['id'].hide = 'all'
+
+ @property
+ def stream_id(self):
+ return self.params['stream_id'].value
diff --git a/grc/core/domain.dtd b/grc/core/domain.dtd
deleted file mode 100644
index b5b0b8bf39..0000000000
--- a/grc/core/domain.dtd
+++ /dev/null
@@ -1,35 +0,0 @@
-<!--
-Copyright 2014 Free Software Foundation, Inc.
-This file is part of GNU Radio
-
-GNU Radio Companion is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-GNU Radio Companion is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
--->
-<!ELEMENT domain (name, key, color?, multiple_sinks?, multiple_sources?, connection*)>
-<!--
- Sub level elements.
- -->
-<!ELEMENT connection (source_domain, sink_domain, make)>
-<!--
- Bottom level elements.
- Character data only.
- -->
-<!ELEMENT name (#PCDATA)>
-<!ELEMENT key (#PCDATA)>
-<!ELEMENT multiple_sinks (#PCDATA)>
-<!ELEMENT multiple_sources (#PCDATA)>
-<!ELEMENT color (#PCDATA)>
-<!ELEMENT make (#PCDATA)>
-<!ELEMENT source_domain (#PCDATA)>
-<!ELEMENT sink_domain (#PCDATA)>
diff --git a/grc/core/errors.py b/grc/core/errors.py
new file mode 100644
index 0000000000..6437cc4fa1
--- /dev/null
+++ b/grc/core/errors.py
@@ -0,0 +1,30 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import, print_function
+
+
+class GRCError(Exception):
+ """Generic error class"""
+
+
+class BlockLoadError(GRCError):
+ """Error during block loading"""
+
+
+class TemplateError(BlockLoadError):
+ """Mako Template Error"""
diff --git a/grc/core/generator/FlowGraphProxy.py b/grc/core/generator/FlowGraphProxy.py
index 23ccf95c4b..f438fa0d39 100644
--- a/grc/core/generator/FlowGraphProxy.py
+++ b/grc/core/generator/FlowGraphProxy.py
@@ -66,14 +66,15 @@ class FlowGraphProxy(object): # TODO: move this in a refactored Generator
self.get_pad_sinks() if direction in ('source', 'out') else []
ports = []
for pad in pads:
+ type_param = pad.params['type']
master = {
- 'label': str(pad.get_param('label').get_evaluated()),
- 'type': str(pad.get_param('type').get_evaluated()),
- 'vlen': str(pad.get_param('vlen').get_value()),
- 'size': pad.get_param('type').opt_value('size'),
- 'optional': bool(pad.get_param('optional').get_evaluated()),
+ 'label': str(pad.params['label'].get_evaluated()),
+ 'type': str(pad.params['type'].get_evaluated()),
+ 'vlen': str(pad.params['vlen'].get_value()),
+ 'size': type_param.options.attributes[type_param.get_value()]['size'],
+ 'optional': bool(pad.params['optional'].get_evaluated()),
}
- num_ports = pad.get_param('num_streams').get_evaluated()
+ num_ports = pad.params['num_streams'].get_evaluated()
if num_ports > 1:
for i in range(num_ports):
clone = master.copy()
@@ -91,7 +92,7 @@ class FlowGraphProxy(object): # TODO: move this in a refactored Generator
a list of pad source blocks in this flow graph
"""
pads = [b for b in self.get_enabled_blocks() if b.key == 'pad_source']
- return sorted(pads, lambda x, y: cmp(x.get_id(), y.get_id()))
+ return sorted(pads, key=lambda x: x.name)
def get_pad_sinks(self):
"""
@@ -101,7 +102,7 @@ class FlowGraphProxy(object): # TODO: move this in a refactored Generator
a list of pad sink blocks in this flow graph
"""
pads = [b for b in self.get_enabled_blocks() if b.key == 'pad_sink']
- return sorted(pads, lambda x, y: cmp(x.get_id(), y.get_id()))
+ return sorted(pads, key=lambda x: x.name)
def get_pad_port_global_key(self, port):
"""
@@ -116,15 +117,46 @@ class FlowGraphProxy(object): # TODO: move this in a refactored Generator
for pad in pads:
# using the block param 'type' instead of the port domain here
# to emphasize that hier block generation is domain agnostic
- is_message_pad = pad.get_param('type').get_evaluated() == "message"
+ is_message_pad = pad.params['type'].get_evaluated() == "message"
if port.parent == pad:
if is_message_pad:
- key = pad.get_param('label').get_value()
+ key = pad.params['label'].get_value()
else:
key = str(key_offset + int(port.key))
return key
else:
# assuming we have either only sources or sinks
if not is_message_pad:
- key_offset += len(pad.get_ports())
+ key_offset += len(pad.sinks) + len(pad.sources)
return -1
+
+
+def get_hier_block_io(flow_graph, direction, domain=None):
+ """
+ Get a list of io ports for this flow graph.
+
+ Returns a list of dicts with: type, label, vlen, size, optional
+ """
+ pads = flow_graph.get_pad_sources() if direction in ('sink', 'in') else \
+ flow_graph.get_pad_sinks() if direction in ('source', 'out') else []
+ ports = []
+ for pad in pads:
+ type_param = pad.params['type']
+ master = {
+ 'label': str(pad.params['label'].get_evaluated()),
+ 'type': str(pad.params['type'].get_evaluated()),
+ 'vlen': str(pad.params['vlen'].get_value()),
+ 'size': type_param.options.attributes[type_param.get_value()]['size'],
+ 'optional': bool(pad.params['optional'].get_evaluated()),
+ }
+ num_ports = pad.params['num_streams'].get_evaluated()
+ if num_ports > 1:
+ for i in range(num_ports):
+ clone = master.copy()
+ clone['label'] += str(i)
+ ports.append(clone)
+ else:
+ ports.append(master)
+ if domain is not None:
+ ports = [p for p in ports if p.domain == domain]
+ return ports
diff --git a/grc/core/generator/Generator.py b/grc/core/generator/Generator.py
index 316ed5014d..62dc26b8a8 100644
--- a/grc/core/generator/Generator.py
+++ b/grc/core/generator/Generator.py
@@ -18,25 +18,16 @@
from __future__ import absolute_import
-import codecs
import os
-import tempfile
-import operator
-import collections
-from Cheetah.Template import Template
-import six
+from mako.template import Template
-from .FlowGraphProxy import FlowGraphProxy
-from .. import ParseXML, Messages
-from ..Constants import (
- TOP_BLOCK_FILE_MODE, BLOCK_FLAG_NEED_QT_GUI,
- HIER_BLOCK_FILE_MODE, BLOCK_DTD
-)
-from ..utils import expr_utils
+from .hier_block import HierBlockGenerator, QtHierBlockGenerator
+from .top_block import TopBlockGenerator
DATA_DIR = os.path.dirname(__file__)
-FLOW_GRAPH_TEMPLATE = os.path.join(DATA_DIR, 'flow_graph.tmpl')
+FLOW_GRAPH_TEMPLATE = os.path.join(DATA_DIR, 'flow_graph.py.mako')
+flow_graph_template = Template(filename=FLOW_GRAPH_TEMPLATE)
class Generator(object):
@@ -64,338 +55,3 @@ class Generator(object):
def __getattr__(self, item):
"""get all other attrib from actual generator object"""
return getattr(self._generator, item)
-
-
-class TopBlockGenerator(object):
-
- def __init__(self, flow_graph, file_path):
- """
- Initialize the top block generator object.
-
- Args:
- flow_graph: the flow graph object
- file_path: the path to write the file to
- """
- self._flow_graph = FlowGraphProxy(flow_graph)
- self._generate_options = self._flow_graph.get_option('generate_options')
- self._mode = TOP_BLOCK_FILE_MODE
- dirname = os.path.dirname(file_path)
- # Handle the case where the directory is read-only
- # In this case, use the system's temp directory
- if not os.access(dirname, os.W_OK):
- dirname = tempfile.gettempdir()
- filename = self._flow_graph.get_option('id') + '.py'
- self.file_path = os.path.join(dirname, filename)
- self._dirname = dirname
-
- def write(self):
- """generate output and write it to files"""
- # Do throttle warning
- throttling_blocks = [b for b in self._flow_graph.get_enabled_blocks()
- if b.is_throtteling]
- if not throttling_blocks and not self._generate_options.startswith('hb'):
- Messages.send_warning("This flow graph may not have flow control: "
- "no audio or RF hardware blocks found. "
- "Add a Misc->Throttle block to your flow "
- "graph to avoid CPU congestion.")
- if len(throttling_blocks) > 1:
- keys = set([b.key for b in throttling_blocks])
- if len(keys) > 1 and 'blocks_throttle' in keys:
- Messages.send_warning("This flow graph contains a throttle "
- "block and another rate limiting block, "
- "e.g. a hardware source or sink. "
- "This is usually undesired. Consider "
- "removing the throttle block.")
- # Generate
- for filename, data in self._build_python_code_from_template():
- with codecs.open(filename, 'w', encoding='utf-8') as fp:
- fp.write(data)
- if filename == self.file_path:
- try:
- os.chmod(filename, self._mode)
- except:
- pass
-
- def _build_python_code_from_template(self):
- """
- Convert the flow graph to python code.
-
- Returns:
- a string of python code
- """
- output = list()
-
- fg = self._flow_graph
- title = fg.get_option('title') or fg.get_option('id').replace('_', ' ').title()
- imports = fg.get_imports()
- variables = fg.get_variables()
- parameters = fg.get_parameters()
- monitors = fg.get_monitors()
-
- # List of blocks not including variables and imports and parameters and disabled
- def _get_block_sort_text(block):
- code = block.get_make().replace(block.get_id(), ' ')
- try:
- code += block.get_param('gui_hint').get_value() # Newer gui markup w/ qtgui
- except:
- pass
- return code
-
- blocks_all = expr_utils.sort_objects(
- [b for b in fg.blocks if b.enabled and not b.get_bypassed()],
- operator.methodcaller('get_id'), _get_block_sort_text
- )
- deprecated_block_keys = set(b.name for b in blocks_all if b.is_deprecated)
- for key in deprecated_block_keys:
- Messages.send_warning("The block {!r} is deprecated.".format(key))
-
- # List of regular blocks (all blocks minus the special ones)
- blocks = [b for b in blocks_all if b not in imports and b not in parameters]
-
- for block in blocks:
- key = block.key
- file_path = os.path.join(self._dirname, block.get_id() + '.py')
- if key == 'epy_block':
- src = block.get_param('_source_code').get_value()
- output.append((file_path, src))
- elif key == 'epy_module':
- src = block.get_param('source_code').get_value()
- output.append((file_path, src))
-
- # Filter out bus and virtual sink connections
- connections = [con for con in fg.get_enabled_connections()
- if not (con.is_bus() or con.sink_block.is_virtual_sink())]
-
- # Get the virtual blocks and resolve their connections
- connection_factory = fg.parent_platform.Connection
- virtual = [c for c in connections if c.source_block.is_virtual_source()]
- for connection in virtual:
- sink = connection.sink_port
- for source in connection.source_port.resolve_virtual_source():
- resolved = connection_factory(fg.orignal_flowgraph, source, sink)
- connections.append(resolved)
- # Remove the virtual connection
- connections.remove(connection)
-
- # Bypassing blocks: Need to find all the enabled connections for the block using
- # the *connections* object rather than get_connections(). Create new connections
- # that bypass the selected block and remove the existing ones. This allows adjacent
- # bypassed blocks to see the newly created connections to downstream blocks,
- # allowing them to correctly construct bypass connections.
- bypassed_blocks = fg.get_bypassed_blocks()
- for block in bypassed_blocks:
- # Get the upstream connection (off of the sink ports)
- # Use *connections* not get_connections()
- source_connection = [c for c in connections if c.sink_port == block.sinks[0]]
- # The source connection should never have more than one element.
- assert (len(source_connection) == 1)
-
- # Get the source of the connection.
- source_port = source_connection[0].source_port
-
- # Loop through all the downstream connections
- for sink in (c for c in connections if c.source_port == block.sources[0]):
- if not sink.enabled:
- # Ignore disabled connections
- continue
- connection = connection_factory(fg.orignal_flowgraph, source_port, sink.sink_port)
- connections.append(connection)
- # Remove this sink connection
- connections.remove(sink)
- # Remove the source connection
- connections.remove(source_connection[0])
-
- # List of connections where each endpoint is enabled (sorted by domains, block names)
- connections.sort(key=lambda c: (
- c.source_port.domain, c.sink_port.domain,
- c.source_block.get_id(), c.sink_block.get_id()
- ))
-
- connection_templates = fg.parent.connection_templates
-
- # List of variable names
- var_ids = [var.get_id() for var in parameters + variables]
- replace_dict = dict((var_id, 'self.' + var_id) for var_id in var_ids)
- callbacks_all = []
- for block in blocks_all:
- callbacks_all.extend(expr_utils.expr_replace(cb, replace_dict) for cb in block.get_callbacks())
-
- # Map var id to callbacks
- def uses_var_id():
- used = expr_utils.get_variable_dependencies(callback, [var_id])
- return used and 'self.' + var_id in callback # callback might contain var_id itself
-
- callbacks = {}
- for var_id in var_ids:
- callbacks[var_id] = [callback for callback in callbacks_all if uses_var_id()]
-
- # Load the namespace
- namespace = {
- 'title': title,
- 'imports': imports,
- 'flow_graph': fg,
- 'variables': variables,
- 'parameters': parameters,
- 'monitors': monitors,
- 'blocks': blocks,
- 'connections': connections,
- 'connection_templates': connection_templates,
- 'generate_options': self._generate_options,
- 'callbacks': callbacks,
- }
- # Build the template
- t = Template(open(FLOW_GRAPH_TEMPLATE, 'r').read(), namespace)
- output.append((self.file_path, "\n".join(line.rstrip() for line in str(t).split("\n"))))
- return output
-
-
-class HierBlockGenerator(TopBlockGenerator):
- """Extends the top block generator to also generate a block XML file"""
-
- def __init__(self, flow_graph, file_path):
- """
- Initialize the hier block generator object.
-
- Args:
- flow_graph: the flow graph object
- file_path: where to write the py file (the xml goes into HIER_BLOCK_LIB_DIR)
- """
- TopBlockGenerator.__init__(self, flow_graph, file_path)
- platform = flow_graph.parent
-
- hier_block_lib_dir = platform.config.hier_block_lib_dir
- if not os.path.exists(hier_block_lib_dir):
- os.mkdir(hier_block_lib_dir)
-
- self._mode = HIER_BLOCK_FILE_MODE
- self.file_path = os.path.join(hier_block_lib_dir, self._flow_graph.get_option('id') + '.py')
- self.file_path_xml = self.file_path + '.xml'
-
- def write(self):
- """generate output and write it to files"""
- TopBlockGenerator.write(self)
- ParseXML.to_file(self._build_block_n_from_flow_graph_io(), self.file_path_xml)
- ParseXML.validate_dtd(self.file_path_xml, BLOCK_DTD)
- try:
- os.chmod(self.file_path_xml, self._mode)
- except:
- pass
-
- def _build_block_n_from_flow_graph_io(self):
- """
- Generate a block XML nested data from the flow graph IO
-
- Returns:
- a xml node tree
- """
- # Extract info from the flow graph
- block_key = self._flow_graph.get_option('id')
- parameters = self._flow_graph.get_parameters()
-
- def var_or_value(name):
- if name in (p.get_id() for p in parameters):
- return "$" + name
- return name
-
- # Build the nested data
- block_n = collections.OrderedDict()
- block_n['name'] = self._flow_graph.get_option('title') or \
- self._flow_graph.get_option('id').replace('_', ' ').title()
- block_n['key'] = block_key
- block_n['category'] = self._flow_graph.get_option('category')
- block_n['import'] = "from {0} import {0} # grc-generated hier_block".format(
- self._flow_graph.get_option('id'))
- # Make data
- if parameters:
- block_n['make'] = '{cls}(\n {kwargs},\n)'.format(
- cls=block_key,
- kwargs=',\n '.join(
- '{key}=${key}'.format(key=param.get_id()) for param in parameters
- ),
- )
- else:
- block_n['make'] = '{cls}()'.format(cls=block_key)
- # Callback data
- block_n['callback'] = [
- 'set_{key}(${key})'.format(key=param.get_id()) for param in parameters
- ]
-
- # Parameters
- block_n['param'] = list()
- for param in parameters:
- param_n = collections.OrderedDict()
- param_n['name'] = param.get_param('label').get_value() or param.get_id()
- param_n['key'] = param.get_id()
- param_n['value'] = param.get_param('value').get_value()
- param_n['type'] = 'raw'
- param_n['hide'] = param.get_param('hide').get_value()
- block_n['param'].append(param_n)
-
- # Bus stuff
- if self._flow_graph.get_bussink():
- block_n['bus_sink'] = '1'
- if self._flow_graph.get_bussrc():
- block_n['bus_source'] = '1'
-
- # Sink/source ports
- for direction in ('sink', 'source'):
- block_n[direction] = list()
- for port in self._flow_graph.get_hier_block_io(direction):
- port_n = collections.OrderedDict()
- port_n['name'] = port['label']
- port_n['type'] = port['type']
- if port['type'] != "message":
- port_n['vlen'] = var_or_value(port['vlen'])
- if port['optional']:
- port_n['optional'] = '1'
- block_n[direction].append(port_n)
-
- # More bus stuff
- bus_struct_sink = self._flow_graph.get_bus_structure_sink()
- if bus_struct_sink:
- block_n['bus_structure_sink'] = bus_struct_sink[0].get_param('struct').get_value()
- bus_struct_src = self._flow_graph.get_bus_structure_src()
- if bus_struct_src:
- block_n['bus_structure_source'] = bus_struct_src[0].get_param('struct').get_value()
-
- # Documentation
- block_n['doc'] = "\n".join(field for field in (
- self._flow_graph.get_option('author'),
- self._flow_graph.get_option('description'),
- self.file_path
- ) if field)
- block_n['grc_source'] = str(self._flow_graph.grc_file_path)
-
- n = {'block': block_n}
- return n
-
-
-class QtHierBlockGenerator(HierBlockGenerator):
-
- def _build_block_n_from_flow_graph_io(self):
- n = HierBlockGenerator._build_block_n_from_flow_graph_io(self)
- block_n = collections.OrderedDict()
-
- # insert flags after category
- for key, value in six.iteritems(n['block']):
- block_n[key] = value
- if key == 'category':
- block_n['flags'] = BLOCK_FLAG_NEED_QT_GUI
-
- if not block_n['name'].upper().startswith('QT GUI'):
- block_n['name'] = 'QT GUI ' + block_n['name']
-
- gui_hint_param = collections.OrderedDict()
- gui_hint_param['name'] = 'GUI Hint'
- gui_hint_param['key'] = 'gui_hint'
- gui_hint_param['value'] = ''
- gui_hint_param['type'] = 'gui_hint'
- gui_hint_param['hide'] = 'part'
- block_n['param'].append(gui_hint_param)
-
- block_n['make'] += (
- "\n#set $win = 'self.%s' % $id"
- "\n${gui_hint()($win)}"
- )
-
- return {'block': block_n}
diff --git a/grc/core/generator/flow_graph.py.mako b/grc/core/generator/flow_graph.py.mako
new file mode 100644
index 0000000000..484441f00f
--- /dev/null
+++ b/grc/core/generator/flow_graph.py.mako
@@ -0,0 +1,352 @@
+% if not generate_options.startswith('hb'):
+#!/usr/bin/env python2
+% endif
+# -*- coding: utf-8 -*-
+<%def name="indent(code)">${ '\n '.join(str(code).splitlines()) }</%def>
+"""
+GNU Radio Python Flow Graph
+
+Title: ${title}
+% if flow_graph.get_option('author'):
+Author: ${flow_graph.get_option('author')}
+% endif
+% if flow_graph.get_option('description'):
+Description: ${flow_graph.get_option('description')}
+% endif
+Generated: ${ generated_time }
+"""
+
+% if generate_options == 'qt_gui':
+from distutils.version import StrictVersion
+
+if __name__ == '__main__':
+ import ctypes
+ import sys
+ if sys.platform.startswith('linux'):
+ try:
+ x11 = ctypes.cdll.LoadLibrary('libX11.so')
+ x11.XInitThreads()
+ except:
+ print "Warning: failed to XInitThreads()"
+
+% endif
+########################################################
+##Create Imports
+########################################################
+% for imp in imports:
+##${imp.replace(" # grc-generated hier_block", "")}
+${imp}
+% endfor
+########################################################
+##Create Class
+## Write the class declaration for a top or hier block.
+## The parameter names are the arguments to __init__.
+## Setup the IO signature (hier block only).
+########################################################
+<%
+ class_name = flow_graph.get_option('id')
+ param_str = ', '.join(['self'] + ['%s=%s'%(param.name, param.templates.render('make')) for param in parameters])
+%>\
+% if generate_options == 'qt_gui':
+from gnuradio import qtgui
+
+class ${class_name}(gr.top_block, Qt.QWidget):
+
+ def __init__(${param_str}):
+ gr.top_block.__init__(self, "${title}")
+ Qt.QWidget.__init__(self)
+ self.setWindowTitle("${title}")
+ qtgui.util.check_set_qss()
+ try:
+ self.setWindowIcon(Qt.QIcon.fromTheme('gnuradio-grc'))
+ except:
+ pass
+ self.top_scroll_layout = Qt.QVBoxLayout()
+ self.setLayout(self.top_scroll_layout)
+ self.top_scroll = Qt.QScrollArea()
+ self.top_scroll.setFrameStyle(Qt.QFrame.NoFrame)
+ self.top_scroll_layout.addWidget(self.top_scroll)
+ self.top_scroll.setWidgetResizable(True)
+ self.top_widget = Qt.QWidget()
+ self.top_scroll.setWidget(self.top_widget)
+ self.top_layout = Qt.QVBoxLayout(self.top_widget)
+ self.top_grid_layout = Qt.QGridLayout()
+ self.top_layout.addLayout(self.top_grid_layout)
+
+ self.settings = Qt.QSettings("GNU Radio", "${class_name}")
+
+ try:
+ if StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"):
+ self.restoreGeometry(self.settings.value("geometry").toByteArray())
+ else:
+ self.restoreGeometry(self.settings.value("geometry"))
+ except:
+ pass
+% elif generate_options == 'no_gui':
+
+class ${class_name}(gr.top_block):
+
+ def __init__(${param_str}):
+ gr.top_block.__init__(self, "${title}")
+% elif generate_options.startswith('hb'):
+ <% in_sigs = flow_graph.get_hier_block_stream_io('in') %>
+ <% out_sigs = flow_graph.get_hier_block_stream_io('out') %>
+
+
+% if generate_options == 'hb_qt_gui':
+class ${class_name}(gr.hier_block2, Qt.QWidget):
+% else:
+class ${class_name}(gr.hier_block2):
+% endif
+<%def name="make_io_sig(io_sigs)">
+ <% size_strs = ['%s*%s'%(io_sig['size'], io_sig['vlen']) for io_sig in io_sigs] %>
+ % if len(io_sigs) == 0:
+gr.io_signature(0, 0, 0)\
+ #elif len(${io_sigs}) == 1
+gr.io_signature(1, 1, ${size_strs[0]})
+ % else:
+gr.io_signaturev(${len(io_sigs)}, ${len(io_sigs)}, [${', '.join(ize_strs)}])
+ % endif
+</%def>
+
+ def __init__(${param_str}):
+ gr.hier_block2.__init__(
+ self, "${ title }",
+ ${make_io_sig(in_sigs)},
+ ${make_io_sig(out_sigs)},
+ )
+ % for pad in flow_graph.get_hier_block_message_io('in'):
+ self.message_port_register_hier_in("${ pad['label'] }")
+ % endfor
+ % for pad in flow_graph.get_hier_block_message_io('out'):
+ self.message_port_register_hier_out("${ pad['label'] }")
+ % endfor
+ % if generate_options == 'hb_qt_gui':
+
+ Qt.QWidget.__init__(self)
+ self.top_layout = Qt.QVBoxLayout()
+ self.top_grid_layout = Qt.QGridLayout()
+ self.top_layout.addLayout(self.top_grid_layout)
+ self.setLayout(self.top_layout)
+ % endif
+% endif
+% if flow_graph.get_option('thread_safe_setters'):
+
+ self._lock = threading.RLock()
+% endif
+########################################################
+##Create Parameters
+## Set the parameter to a property of self.
+########################################################
+% if parameters:
+
+ ${'##################################################'}
+ # Parameters
+ ${'##################################################'}
+% endif
+% for param in parameters:
+ ${indent(param.get_var_make())}
+% endfor
+########################################################
+##Create Variables
+########################################################
+% if variables:
+
+ ${'##################################################'}
+ # Variables
+ ${'##################################################'}
+% endif
+% for var in variables:
+ ${indent(var.templates.render('var_make'))}
+% endfor
+ % if blocks:
+
+ ${'##################################################'}
+ # Blocks
+ ${'##################################################'}
+ % endif
+ % for blk, blk_make in blocks:
+ ${ indent(blk_make.strip('\n')) }
+## % if 'alias' in blk.params and blk.params['alias'].get_evaluated():
+## (self.${blk.name}).set_block_alias("${blk.params['alias'].get_evaluated()}")
+## % endif
+## % if 'affinity' in blk.params and blk.params['affinity'].get_evaluated():
+## (self.${blk.name}).set_processor_affinity(${blk.params['affinity'].get_evaluated()})
+## % endif
+## % if len(blk.sources) > 0 and 'minoutbuf' in blk.params and int(blk.params['minoutbuf'].get_evaluated()) > 0:
+## (self.${blk.name}).set_min_output_buffer(${blk.params['minoutbuf'].get_evaluated()})
+## % endif
+## % if len(blk.sources) > 0 and 'maxoutbuf' in blk.params and int(blk.params['maxoutbuf'].get_evaluated()) > 0:
+## (self.${blk.name}).set_max_output_buffer(${blk.params['maxoutbuf'].get_evaluated()})
+## % endif
+ % endfor
+ % if connections:
+
+ ${'##################################################'}
+ # Connections
+ ${'##################################################'}
+ % for connection in connections:
+ ${ connection.rstrip() }
+ % endfor
+ % endif
+########################################################
+## QT sink close method reimplementation
+########################################################
+% if generate_options == 'qt_gui':
+
+ def closeEvent(self, event):
+ self.settings = Qt.QSettings("GNU Radio", "${class_name}")
+ self.settings.setValue("geometry", self.saveGeometry())
+ event.accept()
+ % if flow_graph.get_option('qt_qss_theme'):
+
+ def setStyleSheetFromFile(self, filename):
+ try:
+ if not os.path.exists(filename):
+ filename = os.path.join(
+ gr.prefix(), "share", "gnuradio", "themes", filename)
+ with open(filename) as ss:
+ self.setStyleSheet(ss.read())
+ except Exception as e:
+ print >> sys.stderr, e
+ % endif
+% endif
+##
+##
+##
+## Create Callbacks
+## Write a set method for this variable that calls the callbacks
+########################################################
+ % for var in parameters + variables:
+
+ def get_${ var.name }(self):
+ return self.${ var.name }
+
+ def set_${ var.name }(self, ${ var.name }):
+ % if flow_graph.get_option('thread_safe_setters'):
+ with self._lock:
+ self.${ var.name } = ${ var.name }
+ % for callback in callbacks[var.name]:
+ ${ indent(callback) }
+ % endfor
+ % else:
+ self.${ var.name } = ${ var.name }
+ % for callback in callbacks[var.name]:
+ ${ indent(callback) }
+ % endfor
+ % endif
+ % endfor
+########################################################
+##Create Main
+## For top block code, generate a main routine.
+## Instantiate the top block and run as gui or cli.
+########################################################
+<%def name="make_default(type_, param)">
+ % if type_ == 'eng_float':
+eng_notation.num_to_str(${param.templates.render('make')})
+ % else:
+${param.templates.render('make')}
+ % endif
+</%def>\
+% if not generate_options.startswith('hb'):
+<% params_eq_list = list() %>
+% if parameters:
+
+<% arg_parser_args = '' %>\
+def argument_parser():
+ % if flow_graph.get_option('description'):
+ <%
+ arg_parser_args = 'description=description'
+ %>description = ${repr(flow_graph.get_option('description'))}
+ % endif
+ parser = ArgumentParser(${arg_parser_args})
+ % for param in parameters:
+<%
+ switches = ['"--{}"'.format(param.name.replace('_', '-'))]
+ short_id = param.params['short_id'].get_value()
+ if short_id:
+ switches.insert(0, '"-{}"'.format(short_id))
+
+ type_ = param.params['type'].get_value()
+ if type_:
+ params_eq_list.append('%s=options.%s' % (param.name, param.name))
+
+ default = param.templates.render('make')
+ if type_ == 'eng_float':
+ default = eng_notation.num_to_str(default)
+ # FIXME:
+ if type_ == 'string':
+ type_ = 'str'
+ %>\
+ % if type_:
+ parser.add_argument(
+ ${ ', '.join(switches) }, dest="${param.name}", type=${type_}, default=${ default },
+ help="Set ${param.params['label'].get_evaluated() or param.name} [default=%(default)r]")
+ % endif
+ % endfor
+ return parser
+% endif
+
+
+def main(top_block_cls=${class_name}, options=None):
+ % if parameters:
+ if options is None:
+ options = argument_parser().parse_args()
+ % endif
+ % if flow_graph.get_option('realtime_scheduling'):
+ if gr.enable_realtime_scheduling() != gr.RT_OK:
+ print "Error: failed to enable real-time scheduling."
+ % endif
+ % if generate_options == 'qt_gui':
+
+ if StrictVersion("4.5.0") <= StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"):
+ style = gr.prefs().get_string('qtgui', 'style', 'raster')
+ Qt.QApplication.setGraphicsSystem(style)
+ qapp = Qt.QApplication(sys.argv)
+
+ tb = top_block_cls(${ ', '.join(params_eq_list) })
+ % if flow_graph.get_option('run'):
+ tb.start(${flow_graph.get_option('max_nouts') or ''})
+ % endif
+ % if flow_graph.get_option('qt_qss_theme'):
+ tb.setStyleSheetFromFile(${ flow_graph.get_option('qt_qss_theme') })
+ % endif
+ tb.show()
+
+ def quitting():
+ tb.stop()
+ tb.wait()
+ qapp.aboutToQuit.connect(quitting)
+ % for m in monitors:
+ if 'en' in m.params:
+ if m.params['en'].get_value():
+ (tb.${m.name}).start()
+ else:
+ sys.stderr.write("Monitor '{0}' does not have an enable ('en') parameter.".format("tb.${m.name}"))
+ % endfor
+ qapp.exec_()
+ % elif generate_options == 'no_gui':
+ tb = top_block_cls(${ ', '.join(params_eq_list) })
+ % if flow_graph.get_option('run_options') == 'prompt':
+ tb.start(${ flow_graph.get_option('max_nouts') or '' })
+ % for m in monitors:
+ (tb.${m.name}).start()
+ % endfor
+ try:
+ raw_input('Press Enter to quit: ')
+ except EOFError:
+ pass
+ tb.stop()
+ % elif flow_graph.get_option('run_options') == 'run':
+ tb.start(${flow_graph.get_option('max_nouts') or ''})
+ % endif
+ % for m in monitors:
+ (tb.${m.name}).start()
+ % endfor
+ tb.wait()
+ % endif
+
+
+if __name__ == '__main__':
+ main()
+% endif
diff --git a/grc/core/generator/flow_graph.tmpl b/grc/core/generator/flow_graph.tmpl
deleted file mode 100644
index 202362c925..0000000000
--- a/grc/core/generator/flow_graph.tmpl
+++ /dev/null
@@ -1,420 +0,0 @@
-#if not $generate_options.startswith('hb')
-#!/usr/bin/env python2
-#end if
-# -*- coding: utf-8 -*-
-########################################################
-##Cheetah template - gnuradio_python
-##
-##@param imports the import statements
-##@param flow_graph the flow_graph
-##@param variables the variable blocks
-##@param parameters the parameter blocks
-##@param blocks the signal blocks
-##@param connections the connections
-##@param generate_options the type of flow graph
-##@param callbacks variable id map to callback strings
-########################################################
-#def indent($code)
-#set $code = '\n '.join(str($code).splitlines())
-$code#slurp
-#end def
-#import time
-#set $DIVIDER = '#'*50
-$DIVIDER
-# GNU Radio Python Flow Graph
-# Title: $title
-#if $flow_graph.get_option('author')
-# Author: $flow_graph.get_option('author')
-#end if
-#if $flow_graph.get_option('description')
-# Description: $flow_graph.get_option('description')
-#end if
-# Generated: $time.ctime()
-$DIVIDER
-#if $flow_graph.get_option('thread_safe_setters')
-import threading
-#end if
-
-#if $generate_options == 'qt_gui'
-from distutils.version import StrictVersion
-#end if
-
-## Call XInitThreads as the _very_ first thing.
-## After some Qt import, it's too late
-#if $generate_options == 'qt_gui'
-if __name__ == '__main__':
- import ctypes
- import sys
- if sys.platform.startswith('linux'):
- try:
- x11 = ctypes.cdll.LoadLibrary('libX11.so')
- x11.XInitThreads()
- except:
- print "Warning: failed to XInitThreads()"
-
-#end if
-#
-########################################################
-##Create Imports
-########################################################
-#if $flow_graph.get_option('qt_qss_theme')
-#set imports = $sorted(set($imports + ["import os", "import sys"]))
-#end if
-#if any(imp.endswith("# grc-generated hier_block") for imp in $imports)
-import os
-import sys
-#set imports = $filter(lambda i: i not in ("import os", "import sys"), $imports)
-sys.path.append(os.environ.get('GRC_HIER_PATH', os.path.expanduser('~/.grc_gnuradio')))
-
-#end if
-#for $imp in $imports
-##$(imp.replace(" # grc-generated hier_block", ""))
-$imp
-#end for
-########################################################
-##Create Class
-## Write the class declaration for a top or hier block.
-## The parameter names are the arguments to __init__.
-## Setup the IO signature (hier block only).
-########################################################
-#set $class_name = $flow_graph.get_option('id')
-#set $param_str = ', '.join(['self'] + ['%s=%s'%(param.get_id(), param.get_make()) for param in $parameters])
-#if $generate_options == 'qt_gui'
-from gnuradio import qtgui
-
-class $(class_name)(gr.top_block, Qt.QWidget):
-
- def __init__($param_str):
- gr.top_block.__init__(self, "$title")
- Qt.QWidget.__init__(self)
- self.setWindowTitle("$title")
- qtgui.util.check_set_qss()
- try:
- self.setWindowIcon(Qt.QIcon.fromTheme('gnuradio-grc'))
- except:
- pass
- self.top_scroll_layout = Qt.QVBoxLayout()
- self.setLayout(self.top_scroll_layout)
- self.top_scroll = Qt.QScrollArea()
- self.top_scroll.setFrameStyle(Qt.QFrame.NoFrame)
- self.top_scroll_layout.addWidget(self.top_scroll)
- self.top_scroll.setWidgetResizable(True)
- self.top_widget = Qt.QWidget()
- self.top_scroll.setWidget(self.top_widget)
- self.top_layout = Qt.QVBoxLayout(self.top_widget)
- self.top_grid_layout = Qt.QGridLayout()
- self.top_layout.addLayout(self.top_grid_layout)
-
- self.settings = Qt.QSettings("GNU Radio", "$class_name")
-
- try:
- if StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"):
- self.restoreGeometry(self.settings.value("geometry").toByteArray())
- else:
- self.restoreGeometry(self.settings.value("geometry"))
- except:
- pass
-#elif $generate_options == 'no_gui'
-
-
-class $(class_name)(gr.top_block):
-
- def __init__($param_str):
- gr.top_block.__init__(self, "$title")
-#elif $generate_options.startswith('hb')
- #set $in_sigs = $flow_graph.get_hier_block_stream_io('in')
- #set $out_sigs = $flow_graph.get_hier_block_stream_io('out')
-
-
-#if $generate_options == 'hb_qt_gui'
-class $(class_name)(gr.hier_block2, Qt.QWidget):
-#else
-class $(class_name)(gr.hier_block2):
-#end if
-#def make_io_sig($io_sigs)
- #set $size_strs = ['%s*%s'%(io_sig['size'], io_sig['vlen']) for io_sig in $io_sigs]
- #if len($io_sigs) == 0
-gr.io_signature(0, 0, 0)#slurp
- #elif len($io_sigs) == 1
-gr.io_signature(1, 1, $size_strs[0])#slurp
- #else
-gr.io_signaturev($(len($io_sigs)), $(len($io_sigs)), [$(', '.join($size_strs))])#slurp
- #end if
-#end def
-
- def __init__($param_str):
- gr.hier_block2.__init__(
- self, "$title",
- $make_io_sig($in_sigs),
- $make_io_sig($out_sigs),
- )
- #for $pad in $flow_graph.get_hier_block_message_io('in')
- self.message_port_register_hier_in("$pad['label']")
- #end for
- #for $pad in $flow_graph.get_hier_block_message_io('out')
- self.message_port_register_hier_out("$pad['label']")
- #end for
- #if $generate_options == 'hb_qt_gui'
-
- Qt.QWidget.__init__(self)
- self.top_layout = Qt.QVBoxLayout()
- self.top_grid_layout = Qt.QGridLayout()
- self.top_layout.addLayout(self.top_grid_layout)
- self.setLayout(self.top_layout)
- #end if
-#end if
-#if $flow_graph.get_option('thread_safe_setters')
-
- self._lock = threading.RLock()
-#end if
-########################################################
-##Create Parameters
-## Set the parameter to a property of self.
-########################################################
-#if $parameters
-
- $DIVIDER
- # Parameters
- $DIVIDER
-#end if
-#for $param in $parameters
- $indent($param.get_var_make())
-#end for
-########################################################
-##Create Variables
-########################################################
-#if $variables
-
- $DIVIDER
- # Variables
- $DIVIDER
-#end if
-#for $var in $variables
- $indent($var.get_var_make())
-#end for
-########################################################
-##Create Blocks
-########################################################
-#if $blocks
-
- $DIVIDER
- # Blocks
- $DIVIDER
-#end if
-#for $blk in filter(lambda b: b.get_make(), $blocks)
- #if $blk in $variables
- $indent($blk.get_make())
- #else
- self.$blk.get_id() = $indent($blk.get_make())
- #if 'alias' in $blk.params and $blk.params['alias'].get_evaluated()
- (self.$blk.get_id()).set_block_alias("$blk.params['alias'].get_evaluated()")
- #end if
- #if 'affinity' in $blk.params and $blk.params['affinity'].get_evaluated()
- (self.$blk.get_id()).set_processor_affinity($blk.params['affinity'].get_evaluated())
- #end if
- #if len($blk.sources) > 0 and 'minoutbuf' in $blk.params and int($blk.params['minoutbuf'].get_evaluated()) > 0
- (self.$blk.get_id()).set_min_output_buffer($blk.params['minoutbuf'].get_evaluated())
- #end if
- #if len($blk.sources) > 0 and 'maxoutbuf' in $blk.params and int($blk.params['maxoutbuf'].get_evaluated()) > 0
- (self.$blk.get_id()).set_max_output_buffer($blk.params['maxoutbuf'].get_evaluated())
- #end if
- #end if
-#end for
-########################################################
-##Create Connections
-## The port name should be the id of the parent block.
-## However, port names for IO pads should be self.
-########################################################
-#def make_port_sig($port)
- #if $port.parent.key in ('pad_source', 'pad_sink')
- #set block = 'self'
- #set key = $flow_graph.get_pad_port_global_key($port)
- #else
- #set block = 'self.' + $port.parent.get_id()
- #set key = $port.key
- #end if
- #if not $key.isdigit()
- #set key = repr($key)
- #end if
-($block, $key)#slurp
-#end def
-#if $connections
-
- $DIVIDER
- # Connections
- $DIVIDER
-#end if
-#for $con in $connections
- #set global $source = $con.source_port
- #set global $sink = $con.sink_port
- #include source=$connection_templates[($source.domain, $sink.domain)]
-
-#end for
-########################################################
-## QT sink close method reimplementation
-########################################################
-#if $generate_options == 'qt_gui'
-
- def closeEvent(self, event):
- self.settings = Qt.QSettings("GNU Radio", "$class_name")
- self.settings.setValue("geometry", self.saveGeometry())
- event.accept()
- #if $flow_graph.get_option('qt_qss_theme')
-
- def setStyleSheetFromFile(self, filename):
- try:
- if not os.path.exists(filename):
- filename = os.path.join(
- gr.prefix(), "share", "gnuradio", "themes", filename)
- with open(filename) as ss:
- self.setStyleSheet(ss.read())
- except Exception as e:
- print >> sys.stderr, e
- #end if
-#end if
-########################################################
-##Create Callbacks
-## Write a set method for this variable that calls the callbacks
-########################################################
-#for $var in $parameters + $variables
-
- #set $id = $var.get_id()
- def get_$(id)(self):
- return self.$id
-
- def set_$(id)(self, $id):
- #if $flow_graph.get_option('thread_safe_setters')
- with self._lock:
- self.$id = $id
- #for $callback in $callbacks[$id]
- $indent($callback)
- #end for
- #else
- self.$id = $id
- #for $callback in $callbacks[$id]
- $indent($callback)
- #end for
- #end if
-#end for
-########################################################
-##Create Main
-## For top block code, generate a main routine.
-## Instantiate the top block and run as gui or cli.
-########################################################
-#def make_default($type, $param)
- #if $type == 'eng_float'
-eng_notation.num_to_str($param.get_make())#slurp
- #else
-$param.get_make()#slurp
- #end if
-#end def
-#def make_short_id($param)
- #set $short_id = $param.get_param('short_id').get_evaluated()
- #if $short_id
- #set $short_id = '-' + $short_id
- #end if
-$short_id#slurp
-#end def
-#if not $generate_options.startswith('hb')
-#set $params_eq_list = list()
-#if $parameters
-
-
-def argument_parser():
- #set $arg_parser_args = ''
- #if $flow_graph.get_option('description')
- #set $arg_parser_args = 'description=description'
- description = $repr($flow_graph.get_option('description'))
- #end if
- parser = ArgumentParser($arg_parser_args)
- #for $param in $parameters
- #set $type = $param.get_param('type').get_value()
- #if $type
- #silent $params_eq_list.append('%s=options.%s'%($param.get_id(), $param.get_id()))
- parser.add_argument(
- #if $make_short_id($param)
- "$make_short_id($param)", #slurp
- #end if
- "--$param.get_id().replace('_', '-')", dest="$param.get_id()", type=$type, default=$make_default($type, $param),
- help="Set $($param.get_param('label').get_evaluated() or $param.get_id()) [default=%(default)r]")
- #end if
- #end for
- return parser
-#end if
-
-
-def main(top_block_cls=$(class_name), options=None):
- #if $parameters
- if options is None:
- options = argument_parser().parse_args()
- #end if
- #if $flow_graph.get_option('realtime_scheduling')
- if gr.enable_realtime_scheduling() != gr.RT_OK:
- print "Error: failed to enable real-time scheduling."
- #end if
-
- #if $generate_options == 'qt_gui'
- if StrictVersion("4.5.0") <= StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"):
- style = gr.prefs().get_string('qtgui', 'style', 'raster')
- Qt.QApplication.setGraphicsSystem(style)
- qapp = Qt.QApplication(sys.argv)
-
- tb = top_block_cls($(', '.join($params_eq_list)))
- #if $flow_graph.get_option('run')
- #if $flow_graph.get_option('max_nouts')
- tb.start($flow_graph.get_option('max_nouts'))
- #else
- tb.start()
- #end if
- #end if
- #if $flow_graph.get_option('qt_qss_theme')
- tb.setStyleSheetFromFile($repr($flow_graph.get_option('qt_qss_theme')))
- #end if
- tb.show()
-
- def quitting():
- tb.stop()
- tb.wait()
- qapp.aboutToQuit.connect(quitting)
- #for $m in $monitors
- if 'en' in $m.params:
- if $m.params['en'].get_value():
- (tb.$m.get_id()).start()
- else:
- sys.stderr.write("Monitor '{0}' does not have an enable ('en') parameter.".format("tb.$m.get_id()"))
- #end for
- qapp.exec_()
- #elif $generate_options == 'no_gui'
- tb = top_block_cls($(', '.join($params_eq_list)))
- #set $run_options = $flow_graph.get_option('run_options')
- #if $run_options == 'prompt'
- #if $flow_graph.get_option('max_nouts')
- tb.start($flow_graph.get_option('max_nouts'))
- #else
- tb.start()
- #end if
- #for $m in $monitors
- (tb.$m.get_id()).start()
- #end for
- try:
- raw_input('Press Enter to quit: ')
- except EOFError:
- pass
- tb.stop()
- #elif $run_options == 'run'
- #if $flow_graph.get_option('max_nouts')
- tb.start($flow_graph.get_option('max_nouts'))
- #else
- tb.start()
- #end if
- #end if
- #for $m in $monitors
- (tb.$m.get_id()).start()
- #end for
- tb.wait()
- #end if
-
-
-if __name__ == '__main__':
- main()
-#end if
diff --git a/grc/core/generator/hier_block.py b/grc/core/generator/hier_block.py
new file mode 100644
index 0000000000..ab362e0663
--- /dev/null
+++ b/grc/core/generator/hier_block.py
@@ -0,0 +1,196 @@
+import collections
+import os
+
+import six
+
+from .top_block import TopBlockGenerator
+
+from .. import ParseXML, Constants
+
+
+class HierBlockGenerator(TopBlockGenerator):
+ """Extends the top block generator to also generate a block XML file"""
+
+ def __init__(self, flow_graph, file_path):
+ """
+ Initialize the hier block generator object.
+
+ Args:
+ flow_graph: the flow graph object
+ file_path: where to write the py file (the xml goes into HIER_BLOCK_LIB_DIR)
+ """
+ TopBlockGenerator.__init__(self, flow_graph, file_path)
+ platform = flow_graph.parent
+
+ hier_block_lib_dir = platform.config.hier_block_lib_dir
+ if not os.path.exists(hier_block_lib_dir):
+ os.mkdir(hier_block_lib_dir)
+
+ self._mode = Constants.HIER_BLOCK_FILE_MODE
+ self.file_path = os.path.join(hier_block_lib_dir, self._flow_graph.get_option('id') + '.py')
+ self.file_path_xml = self.file_path + '.xml'
+
+ def write(self):
+ """generate output and write it to files"""
+ TopBlockGenerator.write(self)
+ ParseXML.to_file(self._build_block_n_from_flow_graph_io(), self.file_path_xml)
+ ParseXML.validate_dtd(self.file_path_xml, Constants.BLOCK_DTD)
+ try:
+ os.chmod(self.file_path_xml, self._mode)
+ except:
+ pass
+
+ def _build_block_n_from_flow_graph_io(self):
+ """
+ Generate a block XML nested data from the flow graph IO
+
+ Returns:
+ a xml node tree
+ """
+ # Extract info from the flow graph
+ block_id = self._flow_graph.get_option('id')
+ parameters = self._flow_graph.get_parameters()
+
+ def var_or_value(name):
+ if name in (p.name for p in parameters):
+ return "${" + name + " }"
+ return name
+
+ # Build the nested data
+ data = collections.OrderedDict()
+ data['id'] = block_id
+ data['label'] = (
+ self._flow_graph.get_option('title') or
+ self._flow_graph.get_option('id').replace('_', ' ').title()
+ )
+ data['category'] = self._flow_graph.get_option('category')
+
+ # Parameters
+ data['parameters'] = []
+ for param_block in parameters:
+ p = collections.OrderedDict()
+ p['id'] = param_block.name
+ p['label'] = param_block.params['label'].get_value() or param_block.name
+ p['dtype'] = 'raw'
+ p['value'] = param_block.params['value'].get_value()
+ p['hide'] = param_block.params['hide'].get_value()
+ data['param'].append(p)
+
+ # Ports
+ for direction in ('inputs', 'outputs'):
+ data[direction] = []
+ for port in get_hier_block_io(self._flow_graph, direction):
+ p = collections.OrderedDict()
+ if port.domain == Constants.GR_MESSAGE_DOMAIN:
+ p['id'] = port.id
+ p['label'] = port.label
+ if port.domain != Constants.DEFAULT_DOMAIN:
+ p['domain'] = port.domain
+ p['dtype'] = port.dtype
+ if port.domain != Constants.GR_MESSAGE_DOMAIN:
+ p['vlen'] = var_or_value(port.vlen)
+ if port.optional:
+ p['optional'] = True
+ data[direction].append(p)
+
+ t = data['templates'] = collections.OrderedDict()
+
+ t['import'] = "from {0} import {0} # grc-generated hier_block".format(
+ self._flow_graph.get_option('id'))
+ # Make data
+ if parameters:
+ t['make'] = '{cls}(\n {kwargs},\n)'.format(
+ cls=block_id,
+ kwargs=',\n '.join(
+ '{key}=${key}'.format(key=param.name) for param in parameters
+ ),
+ )
+ else:
+ t['make'] = '{cls}()'.format(cls=block_id)
+ # Callback data
+ t['callback'] = [
+ 'set_{key}(${key})'.format(key=param_block.name) for param_block in parameters
+ ]
+
+
+ # Documentation
+ data['doc'] = "\n".join(field for field in (
+ self._flow_graph.get_option('author'),
+ self._flow_graph.get_option('description'),
+ self.file_path
+ ) if field)
+ data['grc_source'] = str(self._flow_graph.grc_file_path)
+
+ n = {'block': data}
+ return n
+
+
+class QtHierBlockGenerator(HierBlockGenerator):
+
+ def _build_block_n_from_flow_graph_io(self):
+ n = HierBlockGenerator._build_block_n_from_flow_graph_io(self)
+ block_n = collections.OrderedDict()
+
+ # insert flags after category
+ for key, value in six.iteritems(n['block']):
+ block_n[key] = value
+ if key == 'category':
+ block_n['flags'] = 'need_qt_gui'
+
+ if not block_n['name'].upper().startswith('QT GUI'):
+ block_n['name'] = 'QT GUI ' + block_n['name']
+
+ gui_hint_param = collections.OrderedDict()
+ gui_hint_param['name'] = 'GUI Hint'
+ gui_hint_param['key'] = 'gui_hint'
+ gui_hint_param['value'] = ''
+ gui_hint_param['type'] = 'gui_hint'
+ gui_hint_param['hide'] = 'part'
+ block_n['param'].append(gui_hint_param)
+
+ block_n['make'] += (
+ "\n#set $win = 'self.%s' % $id"
+ "\n${gui_hint()($win)}"
+ )
+
+ return {'block': block_n}
+
+
+def get_hier_block_io(flow_graph, direction, domain=None):
+ """
+ Get a list of io ports for this flow graph.
+
+ Returns a list of dicts with: type, label, vlen, size, optional
+ """
+ pads = flow_graph.get_pad_sources() if direction == 'inputs' else flow_graph.get_pad_sinks()
+
+ ports = []
+ for pad in pads:
+ for port in (pad.sources if direction == 'inputs' else pad.sinks):
+ if domain and port.domain != domain:
+ continue
+ yield port
+
+ type_param = pad.params['type']
+ master = {
+ 'label': str(pad.params['label'].get_evaluated()),
+ 'type': str(pad.params['type'].get_evaluated()),
+ 'vlen': str(pad.params['vlen'].get_value()),
+ 'size': type_param.options.attributes[type_param.get_value()]['size'],
+ 'optional': bool(pad.params['optional'].get_evaluated()),
+ }
+ if domain and pad.
+
+ num_ports = pad.params['num_streams'].get_evaluated()
+ if num_ports <= 1:
+ yield master
+ else:
+ for i in range(num_ports):
+ clone = master.copy()
+ clone['label'] += str(i)
+ ports.append(clone)
+ else:
+ ports.append(master)
+ if domain is not None:
+ ports = [p for p in ports if p.domain == domain]
+ return ports
diff --git a/grc/core/generator/top_block.py b/grc/core/generator/top_block.py
new file mode 100644
index 0000000000..e4fed838ae
--- /dev/null
+++ b/grc/core/generator/top_block.py
@@ -0,0 +1,285 @@
+import codecs
+import operator
+import os
+import tempfile
+import textwrap
+import time
+
+from mako.template import Template
+
+from .. import Messages, blocks
+from ..Constants import TOP_BLOCK_FILE_MODE
+from .FlowGraphProxy import FlowGraphProxy
+from ..utils import expr_utils
+
+DATA_DIR = os.path.dirname(__file__)
+FLOW_GRAPH_TEMPLATE = os.path.join(DATA_DIR, 'flow_graph.py.mako')
+flow_graph_template = Template(filename=FLOW_GRAPH_TEMPLATE)
+
+
+class TopBlockGenerator(object):
+
+ def __init__(self, flow_graph, file_path):
+ """
+ Initialize the top block generator object.
+
+ Args:
+ flow_graph: the flow graph object
+ file_path: the path to write the file to
+ """
+
+ self._flow_graph = FlowGraphProxy(flow_graph)
+ self._generate_options = self._flow_graph.get_option('generate_options')
+
+ self._mode = TOP_BLOCK_FILE_MODE
+ dirname = os.path.dirname(file_path)
+ # Handle the case where the directory is read-only
+ # In this case, use the system's temp directory
+ if not os.access(dirname, os.W_OK):
+ dirname = tempfile.gettempdir()
+ filename = self._flow_graph.get_option('id') + '.py'
+ self.file_path = os.path.join(dirname, filename)
+ self._dirname = dirname
+
+ def _warnings(self):
+ throttling_blocks = [b for b in self._flow_graph.get_enabled_blocks()
+ if b.flags.throttle]
+ if not throttling_blocks and not self._generate_options.startswith('hb'):
+ Messages.send_warning("This flow graph may not have flow control: "
+ "no audio or RF hardware blocks found. "
+ "Add a Misc->Throttle block to your flow "
+ "graph to avoid CPU congestion.")
+ if len(throttling_blocks) > 1:
+ keys = set([b.key for b in throttling_blocks])
+ if len(keys) > 1 and 'blocks_throttle' in keys:
+ Messages.send_warning("This flow graph contains a throttle "
+ "block and another rate limiting block, "
+ "e.g. a hardware source or sink. "
+ "This is usually undesired. Consider "
+ "removing the throttle block.")
+
+ deprecated_block_keys = {b.name for b in self._flow_graph.get_enabled_blocks() if b.flags.deprecated}
+ for key in deprecated_block_keys:
+ Messages.send_warning("The block {!r} is deprecated.".format(key))
+
+ def write(self):
+ """generate output and write it to files"""
+ self._warnings()
+
+ for filename, data in self._build_python_code_from_template():
+ with codecs.open(filename, 'w', encoding='utf-8') as fp:
+ fp.write(data)
+ if filename == self.file_path:
+ try:
+ os.chmod(filename, self._mode)
+ except:
+ pass
+
+ def _build_python_code_from_template(self):
+ """
+ Convert the flow graph to python code.
+
+ Returns:
+ a string of python code
+ """
+ output = []
+
+ fg = self._flow_graph
+ title = fg.get_option('title') or fg.get_option('id').replace('_', ' ').title()
+ variables = fg.get_variables()
+ parameters = fg.get_parameters()
+ monitors = fg.get_monitors()
+
+ for block in fg.iter_enabled_blocks():
+ key = block.key
+ file_path = os.path.join(self._dirname, block.name + '.py')
+ if key == 'epy_block':
+ src = block.params['_source_code'].get_value()
+ output.append((file_path, src))
+ elif key == 'epy_module':
+ src = block.params['source_code'].get_value()
+ output.append((file_path, src))
+
+ namespace = {
+ 'flow_graph': fg,
+ 'variables': variables,
+ 'parameters': parameters,
+ 'monitors': monitors,
+ 'generate_options': self._generate_options,
+ 'generated_time': time.ctime(),
+ }
+ flow_graph_code = flow_graph_template.render(
+ title=title,
+ imports=self._imports(),
+ blocks=self._blocks(),
+ callbacks=self._callbacks(),
+ connections=self._connections(),
+ **namespace
+ )
+ # strip trailing white-space
+ flow_graph_code = "\n".join(line.rstrip() for line in flow_graph_code.split("\n"))
+ output.append((self.file_path, flow_graph_code))
+
+ return output
+
+ def _imports(self):
+ fg = self._flow_graph
+ imports = fg.imports()
+ seen = set()
+ output = []
+
+ need_path_hack = any(imp.endswith("# grc-generated hier_block") for imp in imports)
+ if need_path_hack:
+ output.insert(0, textwrap.dedent("""\
+ import os
+ import sys
+ sys.path.append(os.environ.get('GRC_HIER_PATH', os.path.expanduser('~/.grc_gnuradio')))
+ """))
+ seen.add('import os')
+ seen.add('import sys')
+
+ if fg.get_option('qt_qss_theme'):
+ imports.append('import os')
+ imports.append('import sys')
+
+ if fg.get_option('thread_safe_setters'):
+ imports.append('import threading')
+
+ def is_duplicate(l):
+ if l.startswith('import') or l.startswith('from') and l in seen:
+ return True
+ seen.add(line)
+ return False
+
+ for import_ in sorted(imports):
+ lines = import_.strip().split('\n')
+ if not lines[0]:
+ continue
+ for line in lines:
+ line = line.rstrip()
+ if not is_duplicate(line):
+ output.append(line)
+
+ return output
+
+ def _blocks(self):
+ fg = self._flow_graph
+ parameters = fg.get_parameters()
+
+ # List of blocks not including variables and imports and parameters and disabled
+ def _get_block_sort_text(block):
+ code = block.templates.render('make').replace(block.name, ' ')
+ try:
+ code += block.params['gui_hint'].get_value() # Newer gui markup w/ qtgui
+ except:
+ pass
+ return code
+
+ 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')
+ ]
+
+ blocks = expr_utils.sort_objects(blocks, operator.attrgetter('name'), _get_block_sort_text)
+ blocks_make = []
+ for block in blocks:
+ make = block.templates.render('make')
+ if not block.is_variable:
+ make = 'self.' + block.name + ' = ' + make
+ if make:
+ blocks_make.append((block, make))
+ return blocks_make
+
+ def _callbacks(self):
+ fg = self._flow_graph
+ variables = fg.get_variables()
+ parameters = fg.get_parameters()
+
+ # List of variable names
+ var_ids = [var.name for var in parameters + variables]
+ replace_dict = dict((var_id, 'self.' + var_id) for var_id in var_ids)
+ callbacks_all = []
+ for block in fg.iter_enabled_blocks():
+ callbacks_all.extend(expr_utils.expr_replace(cb, replace_dict) for cb in block.get_callbacks())
+
+ # Map var id to callbacks
+ def uses_var_id():
+ used = expr_utils.get_variable_dependencies(callback, [var_id])
+ return used and 'self.' + var_id in callback # callback might contain var_id itself
+
+ callbacks = {}
+ for var_id in var_ids:
+ callbacks[var_id] = [callback for callback in callbacks_all if uses_var_id()]
+
+ return callbacks
+
+ def _connections(self):
+ fg = self._flow_graph
+ templates = {key: Template(text)
+ for key, text in fg.parent_platform.connection_templates.items()}
+
+ def make_port_sig(port):
+ if port.parent.key in ('pad_source', 'pad_sink'):
+ block = 'self'
+ key = fg.get_pad_port_global_key(port)
+ else:
+ block = 'self.' + port.parent_block.name
+ key = port.key
+
+ if not key.isdigit():
+ key = repr(key)
+
+ return '({block}, {key})'.format(block=block, key=key)
+
+ connections = fg.get_enabled_connections()
+
+ # Get the virtual blocks and resolve their connections
+ connection_factory = fg.parent_platform.Connection
+ virtual = [c for c in connections if isinstance(c.source_block, blocks.VirtualSource)]
+ for connection in virtual:
+ sink = connection.sink_port
+ for source in connection.source_port.resolve_virtual_source():
+ resolved = connection_factory(fg.orignal_flowgraph, source, sink)
+ connections.append(resolved)
+ # Remove the virtual connection
+ connections.remove(connection)
+
+ # Bypassing blocks: Need to find all the enabled connections for the block using
+ # the *connections* object rather than get_connections(). Create new connections
+ # that bypass the selected block and remove the existing ones. This allows adjacent
+ # bypassed blocks to see the newly created connections to downstream blocks,
+ # allowing them to correctly construct bypass connections.
+ bypassed_blocks = fg.get_bypassed_blocks()
+ for block in bypassed_blocks:
+ # Get the upstream connection (off of the sink ports)
+ # Use *connections* not get_connections()
+ source_connection = [c for c in connections if c.sink_port == block.sinks[0]]
+ # The source connection should never have more than one element.
+ assert (len(source_connection) == 1)
+
+ # Get the source of the connection.
+ source_port = source_connection[0].source_port
+
+ # Loop through all the downstream connections
+ for sink in (c for c in connections if c.source_port == block.sources[0]):
+ if not sink.enabled:
+ # Ignore disabled connections
+ continue
+ connection = connection_factory(fg.orignal_flowgraph, source_port, sink.sink_port)
+ connections.append(connection)
+ # Remove this sink connection
+ connections.remove(sink)
+ # Remove the source connection
+ connections.remove(source_connection[0])
+
+ # List of connections where each endpoint is enabled (sorted by domains, block names)
+ def by_domain_and_blocks(c):
+ return c.type, c.source_block.name, c.sink_block.name
+
+ rendered = []
+ for con in sorted(connections, key=by_domain_and_blocks):
+ template = templates[con.type]
+ code = template.render(make_port_sig=make_port_sig, source=con.source_port, sink=con.sink_port)
+ rendered.append(code)
+
+ return rendered
diff --git a/grc/core/io/__init__.py b/grc/core/io/__init__.py
new file mode 100644
index 0000000000..f77f1a6704
--- /dev/null
+++ b/grc/core/io/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2017 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
diff --git a/grc/core/io/yaml.py b/grc/core/io/yaml.py
new file mode 100644
index 0000000000..29b4cb81d6
--- /dev/null
+++ b/grc/core/io/yaml.py
@@ -0,0 +1,91 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from collections import OrderedDict
+
+import six
+import yaml
+
+
+class GRCDumper(yaml.SafeDumper):
+ @classmethod
+ def add(cls, data_type):
+ def decorator(func):
+ cls.add_representer(data_type, func)
+ return func
+ return decorator
+
+ def represent_ordered_mapping(self, data):
+ value = []
+ node = yaml.MappingNode(u'tag:yaml.org,2002:map', value, flow_style=False)
+
+ if self.alias_key is not None:
+ self.represented_objects[self.alias_key] = node
+
+ for item_key, item_value in six.iteritems(data):
+ node_key = self.represent_data(item_key)
+ node_value = self.represent_data(item_value)
+ value.append((node_key, node_value))
+
+ return node
+
+ def represent_ordered_mapping_flowing(self, data):
+ node = self.represent_ordered_mapping(data)
+ node.flow_style = True
+ return node
+
+ def represent_list_flowing(self, data):
+ node = self.represent_list(data)
+ node.flow_style = True
+ return node
+
+ def represent_ml_string(self, data):
+ node = self.represent_str(data)
+ node.style = '|'
+ return node
+
+
+class OrderedDictFlowing(OrderedDict):
+ pass
+
+
+class ListFlowing(list):
+ pass
+
+
+class MultiLineString(str):
+ pass
+
+
+GRCDumper.add_representer(OrderedDict, GRCDumper.represent_ordered_mapping)
+GRCDumper.add_representer(OrderedDictFlowing, GRCDumper.represent_ordered_mapping_flowing)
+GRCDumper.add_representer(ListFlowing, GRCDumper.represent_list_flowing)
+GRCDumper.add_representer(tuple, GRCDumper.represent_list)
+GRCDumper.add_representer(MultiLineString, GRCDumper.represent_ml_string)
+GRCDumper.add_representer(yaml.nodes.ScalarNode, lambda r, n: n)
+
+
+def dump(data, stream=None, **kwargs):
+ config = dict(stream=stream, default_flow_style=False, indent=4, Dumper=GRCDumper)
+ config.update(kwargs)
+ return yaml.dump_all([data], **config)
+
+
+safe_load = yaml.safe_load
+__with_libyaml__ = yaml.__with_libyaml__
diff --git a/grc/core/platform.py b/grc/core/platform.py
new file mode 100644
index 0000000000..02203942f9
--- /dev/null
+++ b/grc/core/platform.py
@@ -0,0 +1,430 @@
+# Copyright 2008-2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import, print_function
+
+from codecs import open
+from collections import namedtuple
+import glob
+import os
+import logging
+from itertools import chain
+
+import six
+from six.moves import range
+
+from . import (
+ Messages, Constants,
+ blocks, ports, errors, utils, schema_checker
+)
+
+from .Config import Config
+from .base import Element
+from .io import yaml
+from .generator import Generator
+from .FlowGraph import FlowGraph
+from .Connection import Connection
+from .Param import Param
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO)
+
+
+class Platform(Element):
+
+ def __init__(self, *args, **kwargs):
+ """ Make a platform for GNU Radio """
+ Element.__init__(self, parent=None)
+
+ self.config = self.Config(*args, **kwargs)
+ self.block_docstrings = {}
+ self.block_docstrings_loaded_callback = lambda: None # dummy to be replaced by BlockTreeWindow
+
+ self._docstring_extractor = utils.extract_docs.SubprocessLoader(
+ callback_query_result=self._save_docstring_extraction_result,
+ callback_finished=lambda: self.block_docstrings_loaded_callback()
+ )
+
+ self.blocks = self.block_classes
+ self.domains = {}
+ self.connection_templates = {}
+
+ self._block_categories = {}
+ self._auto_hier_block_generate_chain = set()
+
+ if not yaml.__with_libyaml__:
+ logger.warning("Slow YAML loading (libyaml not available)")
+
+ def __str__(self):
+ return 'Platform - {}'.format(self.config.name)
+
+ @staticmethod
+ def find_file_in_paths(filename, paths, cwd):
+ """Checks the provided paths relative to cwd for a certain filename"""
+ if not os.path.isdir(cwd):
+ cwd = os.path.dirname(cwd)
+ if isinstance(paths, str):
+ paths = (p for p in paths.split(':') if p)
+
+ for path in paths:
+ path = os.path.expanduser(path)
+ if not os.path.isabs(path):
+ path = os.path.normpath(os.path.join(cwd, path))
+ file_path = os.path.join(path, filename)
+ if os.path.exists(os.path.normpath(file_path)):
+ return file_path
+
+ def load_and_generate_flow_graph(self, file_path, out_path=None, hier_only=False):
+ """Loads a flow graph from file and generates it"""
+ Messages.set_indent(len(self._auto_hier_block_generate_chain))
+ Messages.send('>>> Loading: {}\n'.format(file_path))
+ if file_path in self._auto_hier_block_generate_chain:
+ Messages.send(' >>> Warning: cyclic hier_block dependency\n')
+ return None, None
+ self._auto_hier_block_generate_chain.add(file_path)
+ try:
+ flow_graph = self.make_flow_graph()
+ flow_graph.grc_file_path = file_path
+ # Other, nested hier_blocks might be auto-loaded here
+ flow_graph.import_data(self.parse_flow_graph(file_path))
+ flow_graph.rewrite()
+ flow_graph.validate()
+ if not flow_graph.is_valid():
+ raise Exception('Flowgraph invalid')
+ if hier_only and not flow_graph.get_option('generate_options').startswith('hb'):
+ raise Exception('Not a hier block')
+ except Exception as e:
+ Messages.send('>>> Load Error: {}: {}\n'.format(file_path, str(e)))
+ return None, None
+ finally:
+ self._auto_hier_block_generate_chain.discard(file_path)
+ Messages.set_indent(len(self._auto_hier_block_generate_chain))
+
+ try:
+ generator = self.Generator(flow_graph, out_path or file_path)
+ Messages.send('>>> Generating: {}\n'.format(generator.file_path))
+ generator.write()
+ except Exception as e:
+ Messages.send('>>> Generate Error: {}: {}\n'.format(file_path, str(e)))
+ return None, None
+
+ if flow_graph.get_option('generate_options').startswith('hb'):
+ # self.load_block_xml(generator.file_path_xml)
+ # TODO: implement yml output for hier blocks
+ pass
+ return flow_graph, generator.file_path
+
+ def build_library(self, path=None):
+ """load the blocks and block tree from the search paths
+
+ path: a list of paths/files to search in or load (defaults to config)
+ """
+ self._docstring_extractor.start()
+
+ # Reset
+ self.blocks.clear()
+ self.domains.clear()
+ self.connection_templates.clear()
+ self._block_categories.clear()
+
+ # FIXME: remove this as soon as converter is stable
+ from ..converter import Converter
+ converter = Converter(self.config.block_paths, self.config.yml_block_cache)
+ converter.run()
+ logging.info('XML converter done.')
+
+ for file_path in self._iter_files_in_block_path(path):
+ try:
+ data = converter.cache[file_path]
+ except KeyError:
+ with open(file_path, encoding='utf-8') as fp:
+ data = yaml.safe_load(fp)
+
+ if file_path.endswith('.block.yml'):
+ loader = self.load_block_description
+ scheme = schema_checker.BLOCK_SCHEME
+ elif file_path.endswith('.domain.yml'):
+ loader = self.load_domain_description
+ scheme = schema_checker.DOMAIN_SCHEME
+ elif file_path.endswith('.tree.yml'):
+ loader = self.load_category_tree_description
+ scheme = None
+ else:
+ continue
+
+ try:
+ checker = schema_checker.Validator(scheme)
+ passed = checker.run(data)
+ for msg in checker.messages:
+ logger.warning('{:<40s} {}'.format(os.path.basename(file_path), msg))
+ if not passed:
+ logger.info('YAML schema check failed for: ' + file_path)
+
+ loader(data, file_path)
+ except Exception as error:
+ logger.exception('Error while loading %s', file_path)
+ logger.exception(error)
+ raise
+
+ for key, block in six.iteritems(self.blocks):
+ category = self._block_categories.get(key, block.category)
+ if not category:
+ continue
+ root = category[0]
+ if root.startswith('[') and root.endswith(']'):
+ category[0] = root[1:-1]
+ else:
+ category.insert(0, Constants.DEFAULT_BLOCK_MODULE_NAME)
+ block.category = category
+
+ self._docstring_extractor.finish()
+ # self._docstring_extractor.wait()
+
+ def _iter_files_in_block_path(self, path=None, ext='yml'):
+ """Iterator for block descriptions and category trees"""
+ for entry in (path or self.config.block_paths):
+ if os.path.isfile(entry):
+ yield entry
+ elif os.path.isdir(entry):
+ pattern = os.path.join(entry, '**.' + ext)
+ yield_from = glob.iglob(pattern)
+ for file_path in yield_from:
+ yield file_path
+ else:
+ logger.debug('Ignoring invalid path entry %r', entry)
+
+ def _save_docstring_extraction_result(self, block_id, docstrings):
+ docs = {}
+ for match, docstring in six.iteritems(docstrings):
+ if not docstring or match.endswith('_sptr'):
+ continue
+ docs[match] = docstring.replace('\n\n', '\n').strip()
+ try:
+ self.blocks[block_id].documentation.update(docs)
+ except KeyError:
+ pass # in tests platform might be gone...
+
+ ##############################################
+ # Description File Loaders
+ ##############################################
+ # region loaders
+ def load_block_description(self, data, file_path):
+ log = logger.getChild('block_loader')
+
+ # don't load future block format versions
+ file_format = data['file_format']
+ if file_format < 1 or file_format > Constants.BLOCK_DESCRIPTION_FILE_FORMAT_VERSION:
+ log.error('Unknown format version %d in %s', file_format, file_path)
+ return
+
+ block_id = data.pop('id').rstrip('_')
+
+ if block_id in self.block_classes_build_in:
+ log.warning('Not overwriting build-in block %s with %s', block_id, file_path)
+ return
+ if block_id in self.blocks:
+ log.warning('Block with id "%s" overwritten by %s', block_id, file_path)
+
+ try:
+ block_cls = self.blocks[block_id] = self.new_block_class(block_id, **data)
+ except errors.BlockLoadError as error:
+ log.error('Unable to load block %s', block_id)
+ log.exception(error)
+ return
+
+ self._docstring_extractor.query(
+ block_id, block_cls.templates['imports'], block_cls.templates['make'],
+ )
+
+ def load_domain_description(self, data, file_path):
+ log = logger.getChild('domain_loader')
+ domain_id = data['id']
+ if domain_id in self.domains: # test against repeated keys
+ log.debug('Domain "{}" already exists. Ignoring: %s', file_path)
+ return
+
+ color = data.get('color', '')
+ if color.startswith('#'):
+ try:
+ tuple(int(color[o:o + 2], 16) / 255.0 for o in range(1, 6, 2))
+ except ValueError:
+ log.warning('Cannot parse color code "%s" in %s', color, file_path)
+ return
+
+ self.domains[domain_id] = self.Domain(
+ name=data.get('label', domain_id),
+ multi_in=data.get('multiple_connections_per_input', True),
+ multi_out=data.get('multiple_connections_per_output', False),
+ color=color
+ )
+ for connection in data.get('templates', []):
+ try:
+ source_id, sink_id = connection.get('type', [])
+ except ValueError:
+ log.warn('Invalid connection template.')
+ continue
+ connection_id = str(source_id), str(sink_id)
+ self.connection_templates[connection_id] = connection.get('connect', '')
+
+ def load_category_tree_description(self, data, file_path):
+ """Parse category tree file and add it to list"""
+ log = logger.getChild('tree_loader')
+ log.debug('Loading %s', file_path)
+ path = []
+
+ def load_category(name, elements):
+ if not isinstance(name, str):
+ log.debug('invalid name %r', name)
+ return
+ if isinstance(elements, list):
+ pass
+ elif isinstance(elements, str):
+ elements = [elements]
+ else:
+ log.debug('Ignoring elements of %s', name)
+ return
+ path.append(name)
+ for element in elements:
+ if isinstance(element, str):
+ block_id = element
+ self._block_categories[block_id] = list(path)
+ elif isinstance(element, dict):
+ load_category(*next(six.iteritems(element)))
+ else:
+ log.debug('Ignoring some elements of %s', name)
+ path.pop()
+
+ try:
+ module_name, categories = next(six.iteritems(data))
+ except (AttributeError, StopIteration):
+ log.warning('no valid data found')
+ else:
+ load_category(module_name, categories)
+
+ ##############################################
+ # Access
+ ##############################################
+ def parse_flow_graph(self, filename):
+ """
+ Parse a saved flow graph file.
+ Ensure that the file exists, and passes the dtd check.
+
+ Args:
+ filename: the flow graph file
+
+ Returns:
+ nested data
+ @throws exception if the validation fails
+ """
+ filename = filename or self.config.default_flow_graph
+ with open(filename, encoding='utf-8') as fp:
+ is_xml = '<flow_graph>' in fp.read(100)
+ fp.seek(0)
+ # todo: try
+ if not is_xml:
+ data = yaml.safe_load(fp)
+ validator = schema_checker.Validator(schema_checker.FLOW_GRAPH_SCHEME)
+ validator.run(data)
+ else:
+ Messages.send('>>> Converting from XML\n')
+ from ..converter.flow_graph import from_xml
+ data = from_xml(fp)
+
+ return data
+
+ def save_flow_graph(self, filename, flow_graph):
+ data = flow_graph.export_data()
+
+ try:
+ data['connections'] = [yaml.ListFlowing(i) for i in data['connections']]
+ except KeyError:
+ pass
+
+ try:
+ for d in chain([data['options']], data['blocks']):
+ d['states']['coordinate'] = yaml.ListFlowing(d['states']['coordinate'])
+ for param_id, value in list(d['parameters'].items()):
+ if value == '':
+ d['parameters'].pop(param_id)
+ except KeyError:
+ pass
+
+ out = yaml.dump(data, indent=2)
+
+ replace = [
+ ('blocks:', '\nblocks:'),
+ ('connections:', '\nconnections:'),
+ ('metadata:', '\nmetadata:'),
+ ]
+ for r in replace:
+ out = out.replace(*r)
+
+ with open(filename, 'w', encoding='utf-8') as fp:
+ fp.write(out)
+
+ def get_generate_options(self):
+ for param in self.block_classes['options'].parameters_data:
+ if param.get('id') == 'generate_options':
+ break
+ else:
+ return []
+ generate_mode_default = param.get('default')
+ return [(value, name, value == generate_mode_default)
+ for value, name in zip(param['options'], param['option_labels'])]
+
+ ##############################################
+ # Factories
+ ##############################################
+ Config = Config
+ Domain = namedtuple('Domain', 'name multi_in multi_out color')
+ Generator = Generator
+ FlowGraph = FlowGraph
+ Connection = Connection
+
+ block_classes_build_in = blocks.build_ins
+ block_classes = utils.backports.ChainMap({}, block_classes_build_in) # separates build-in from loaded blocks)
+
+ port_classes = {
+ None: ports.Port, # default
+ 'clone': ports.PortClone, # clone of ports with multiplicity > 1
+ }
+ param_classes = {
+ None: Param, # default
+ }
+
+ def make_flow_graph(self, from_filename=None):
+ fg = self.FlowGraph(parent=self)
+ if from_filename:
+ data = self.parse_flow_graph(from_filename)
+ fg.grc_file_path = from_filename
+ fg.import_data(data)
+ return fg
+
+ def new_block_class(self, block_id, **data):
+ return blocks.build(block_id, **data)
+
+ def make_block(self, parent, block_id, **kwargs):
+ cls = self.block_classes[block_id]
+ return cls(parent, **kwargs)
+
+ def make_param(self, parent, **kwargs):
+ cls = self.param_classes[kwargs.pop('cls_key', None)]
+ return cls(parent, **kwargs)
+
+ def make_port(self, parent, **kwargs):
+ cls = self.port_classes[kwargs.pop('cls_key', None)]
+ return cls(parent, **kwargs)
diff --git a/grc/core/block_tree.dtd b/grc/core/ports/__init__.py
index 9e23576477..375b5d63e3 100644
--- a/grc/core/block_tree.dtd
+++ b/grc/core/ports/__init__.py
@@ -1,5 +1,5 @@
-<!--
-Copyright 2008 Free Software Foundation, Inc.
+"""
+Copyright 2008-2015 Free Software Foundation, Inc.
This file is part of GNU Radio
GNU Radio Companion is free software; you can redistribute it and/or
@@ -15,12 +15,9 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
--->
-<!--
- block_tree.dtd
- Josh Blum
- The document type definition for a block tree category listing.
- -->
-<!ELEMENT cat (name, cat*, block*)>
-<!ELEMENT name (#PCDATA)>
-<!ELEMENT block (#PCDATA)>
+"""
+
+from __future__ import absolute_import
+
+from .port import Port
+from .clone import PortClone
diff --git a/grc/core/ports/_virtual_connections.py b/grc/core/ports/_virtual_connections.py
new file mode 100644
index 0000000000..45f4a247fd
--- /dev/null
+++ b/grc/core/ports/_virtual_connections.py
@@ -0,0 +1,126 @@
+# Copyright 2008-2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from itertools import chain
+
+from .. import blocks
+
+
+class LoopError(Exception):
+ pass
+
+
+def upstream_ports(port):
+ if port.is_sink:
+ return _sources_from_virtual_sink_port(port)
+ else:
+ return _sources_from_virtual_source_port(port)
+
+
+def _sources_from_virtual_sink_port(sink_port, _traversed=None):
+ """
+ Resolve the source port that is connected to the given virtual sink port.
+ Use the get source from virtual source to recursively resolve subsequent ports.
+ """
+ source_ports_per_virtual_connection = (
+ # there can be multiple ports per virtual connection
+ _sources_from_virtual_source_port(c.source_port, _traversed) # type: list
+ for c in sink_port.connections(enabled=True)
+ )
+ return list(chain(*source_ports_per_virtual_connection)) # concatenate generated lists of ports
+
+
+def _sources_from_virtual_source_port(source_port, _traversed=None):
+ """
+ Recursively resolve source ports over the virtual connections.
+ Keep track of traversed sources to avoid recursive loops.
+ """
+ _traversed = set(_traversed or []) # a new set!
+ if source_port in _traversed:
+ raise LoopError('Loop found when resolving port type')
+ _traversed.add(source_port)
+
+ block = source_port.parent_block
+ flow_graph = source_port.parent_flowgraph
+
+ if not isinstance(block, blocks.VirtualSource):
+ return [source_port] # nothing to resolve, we're done
+
+ stream_id = block.params['stream_id'].value
+
+ # currently the validation does not allow multiple virtual sinks and one virtual source
+ # but in the future it may...
+ connected_virtual_sink_blocks = (
+ b for b in flow_graph.iter_enabled_blocks()
+ if isinstance(b, blocks.VirtualSink) and b.params['stream_id'].value == stream_id
+ )
+ source_ports_per_virtual_connection = (
+ _sources_from_virtual_sink_port(b.sinks[0], _traversed) # type: list
+ for b in connected_virtual_sink_blocks
+ )
+ return list(chain(*source_ports_per_virtual_connection)) # concatenate generated lists of ports
+
+
+def downstream_ports(port):
+ if port.is_source:
+ return _sinks_from_virtual_source_port(port)
+ else:
+ return _sinks_from_virtual_sink_port(port)
+
+
+def _sinks_from_virtual_source_port(source_port, _traversed=None):
+ """
+ Resolve the sink port that is connected to the given virtual source port.
+ Use the get sink from virtual sink to recursively resolve subsequent ports.
+ """
+ sink_ports_per_virtual_connection = (
+ # there can be multiple ports per virtual connection
+ _sinks_from_virtual_sink_port(c.sink_port, _traversed) # type: list
+ for c in source_port.connections(enabled=True)
+ )
+ return list(chain(*sink_ports_per_virtual_connection)) # concatenate generated lists of ports
+
+
+def _sinks_from_virtual_sink_port(sink_port, _traversed=None):
+ """
+ Recursively resolve sink ports over the virtual connections.
+ Keep track of traversed sinks to avoid recursive loops.
+ """
+ _traversed = set(_traversed or []) # a new set!
+ if sink_port in _traversed:
+ raise LoopError('Loop found when resolving port type')
+ _traversed.add(sink_port)
+
+ block = sink_port.parent_block
+ flow_graph = sink_port.parent_flowgraph
+
+ if not isinstance(block, blocks.VirtualSink):
+ return [sink_port]
+
+ stream_id = block.params['stream_id'].value
+
+ connected_virtual_source_blocks = (
+ b for b in flow_graph.iter_enabled_blocks()
+ if isinstance(b, blocks.VirtualSource) and b.params['stream_id'].value == stream_id
+ )
+ sink_ports_per_virtual_connection = (
+ _sinks_from_virtual_source_port(b.sources[0], _traversed) # type: list
+ for b in connected_virtual_source_blocks
+ )
+ return list(chain(*sink_ports_per_virtual_connection)) # concatenate generated lists of ports
diff --git a/grc/core/ports/clone.py b/grc/core/ports/clone.py
new file mode 100644
index 0000000000..4e1320f81d
--- /dev/null
+++ b/grc/core/ports/clone.py
@@ -0,0 +1,38 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from .port import Port, Element
+
+
+class PortClone(Port):
+
+ def __init__(self, parent, direction, master, name, key):
+ Element.__init__(self, parent)
+ self.master_port = master
+
+ self.name = name
+ self.key = key
+ self.multiplicity = 1
+
+ def __getattr__(self, item):
+ return getattr(self.master_port, item)
+
+ def add_clone(self):
+ raise NotImplementedError()
+
+ def remove_clone(self, port):
+ raise NotImplementedError()
diff --git a/grc/core/ports/port.py b/grc/core/ports/port.py
new file mode 100644
index 0000000000..139aae0ccc
--- /dev/null
+++ b/grc/core/ports/port.py
@@ -0,0 +1,207 @@
+# Copyright 2008-2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from . import _virtual_connections
+
+from .. import Constants
+from ..base import Element
+from ..utils.descriptors import (
+ EvaluatedFlag, EvaluatedEnum, EvaluatedPInt,
+ setup_names, lazy_property
+)
+
+
+@setup_names
+class Port(Element):
+
+ is_port = True
+
+ dtype = EvaluatedEnum(list(Constants.TYPE_TO_SIZEOF.keys()), default='')
+ vlen = EvaluatedPInt()
+ multiplicity = EvaluatedPInt()
+ hidden = EvaluatedFlag()
+ optional = EvaluatedFlag()
+
+ def __init__(self, parent, direction, id, label='', domain=Constants.DEFAULT_DOMAIN, dtype='',
+ vlen='', multiplicity=1, optional=False, hide='', **_):
+ """Make a new port from nested data."""
+ Element.__init__(self, parent)
+
+ self._dir = direction
+ self.key = id
+ if not label:
+ label = id if not id.isdigit() else {'sink': 'in', 'source': 'out'}[direction] + id
+ self.name = self._base_name = label
+
+ self.domain = domain
+ self.dtype = dtype
+ self.vlen = vlen
+
+ if domain == Constants.GR_MESSAGE_DOMAIN: # ToDo: message port class
+ self.key = self.name
+ self.dtype = 'message'
+
+ self.multiplicity = multiplicity
+ self.optional = optional
+ self.hidden = hide
+ # end of args ########################################################
+ self.clones = [] # References to cloned ports (for nports > 1)
+
+ def __str__(self):
+ if self.is_source:
+ return 'Source - {}({})'.format(self.name, self.key)
+ if self.is_sink:
+ return 'Sink - {}({})'.format(self.name, self.key)
+
+ def __repr__(self):
+ return '{!r}.{}[{}]'.format(self.parent, 'sinks' if self.is_sink else 'sources', self.key)
+
+ @property
+ def item_size(self):
+ return Constants.TYPE_TO_SIZEOF[self.dtype] * self.vlen
+
+ @lazy_property
+ def is_sink(self):
+ return self._dir == 'sink'
+
+ @lazy_property
+ def is_source(self):
+ return self._dir == 'source'
+
+ @property
+ def inherit_type(self):
+ """always empty for e.g. virtual blocks, may eval to empty for 'Wildcard'"""
+ return not self.dtype
+
+ def validate(self):
+ Element.validate(self)
+ platform = self.parent_platform
+
+ num_connections = len(list(self.connections(enabled=True)))
+ need_connection = not self.optional and not self.hidden
+ if need_connection and num_connections == 0:
+ self.add_error_message('Port is not connected.')
+
+ if self.dtype not in Constants.TYPE_TO_SIZEOF.keys():
+ self.add_error_message('Type "{}" is not a possible type.'.format(self.dtype))
+
+ try:
+ domain = platform.domains[self.domain]
+ if self.is_sink and not domain.multi_in and num_connections > 1:
+ self.add_error_message('Domain "{}" can have only one upstream block'
+ ''.format(self.domain))
+ if self.is_source and not domain.multi_out and num_connections > 1:
+ self.add_error_message('Domain "{}" can have only one downstream block'
+ ''.format(self.domain))
+ except KeyError:
+ self.add_error_message('Domain key "{}" is not registered.'.format(self.domain))
+
+ def rewrite(self):
+ del self.vlen
+ del self.multiplicity
+ del self.hidden
+ del self.optional
+ del self.dtype
+
+ if self.inherit_type:
+ self.resolve_empty_type()
+
+ Element.rewrite(self)
+
+ # Update domain if was deduced from (dynamic) port type
+ if self.domain == Constants.GR_STREAM_DOMAIN and self.dtype == "message":
+ self.domain = Constants.GR_MESSAGE_DOMAIN
+ self.key = self.name
+ if self.domain == Constants.GR_MESSAGE_DOMAIN and self.dtype != "message":
+ self.domain = Constants.GR_STREAM_DOMAIN
+ self.key = '0' # Is rectified in rewrite()
+
+ def resolve_virtual_source(self):
+ """Only used by Generator after validation is passed"""
+ return _virtual_connections.upstream_ports(self)
+
+ def resolve_empty_type(self):
+ def find_port(finder):
+ try:
+ return next((p for p in finder(self) if not p.inherit_type), None)
+ except _virtual_connections.LoopError as error:
+ self.add_error_message(str(error))
+ except (StopIteration, Exception):
+ pass
+
+ try:
+ port = find_port(_virtual_connections.upstream_ports) or \
+ find_port(_virtual_connections.downstream_ports)
+ self.set_evaluated('dtype', port.dtype) # we don't want to override the template
+ self.set_evaluated('vlen', port.vlen) # we don't want to override the template
+ self.domain = port.domain
+ except AttributeError:
+ self.domain = Constants.DEFAULT_DOMAIN
+
+ def add_clone(self):
+ """
+ Create a clone of this (master) port and store a reference in self._clones.
+
+ The new port name (and key for message ports) will have index 1... appended.
+ If this is the first clone, this (master) port will get a 0 appended to its name (and key)
+
+ Returns:
+ the cloned port
+ """
+ # Add index to master port name if there are no clones yet
+ if not self.clones:
+ self.name = self._base_name + '0'
+ # Also update key for none stream ports
+ if not self.key.isdigit():
+ self.key = self.name
+
+ name = self._base_name + str(len(self.clones) + 1)
+ # Dummy value 99999 will be fixed later
+ key = '99999' if self.key.isdigit() else name
+
+ # Clone
+ port_factory = self.parent_platform.make_port
+ port = port_factory(self.parent, direction=self._dir,
+ name=name, key=key,
+ master=self, cls_key='clone')
+
+ self.clones.append(port)
+ return port
+
+ def remove_clone(self, port):
+ """
+ Remove a cloned port (from the list of clones only)
+ Remove the index 0 of the master port name (and key9 if there are no more clones left
+ """
+ self.clones.remove(port)
+ # Remove index from master port name if there are no more clones
+ if not self.clones:
+ self.name = self._base_name
+ # Also update key for none stream ports
+ if not self.key.isdigit():
+ self.key = self.name
+
+ def connections(self, enabled=None):
+ """Iterator over all connections to/from this port
+
+ enabled: None for all, True for enabled only, False for disabled only
+ """
+ for con in self.parent_flowgraph.connections:
+ if self in con and (enabled is None or enabled == con.enabled):
+ yield con
diff --git a/grc/core/schema_checker/__init__.py b/grc/core/schema_checker/__init__.py
new file mode 100644
index 0000000000..e92500ed4a
--- /dev/null
+++ b/grc/core/schema_checker/__init__.py
@@ -0,0 +1,5 @@
+from .validator import Validator
+
+from .block import BLOCK_SCHEME
+from .domain import DOMAIN_SCHEME
+from .flow_graph import FLOW_GRAPH_SCHEME
diff --git a/grc/core/schema_checker/block.py b/grc/core/schema_checker/block.py
new file mode 100644
index 0000000000..db8830fddf
--- /dev/null
+++ b/grc/core/schema_checker/block.py
@@ -0,0 +1,57 @@
+from .utils import Spec, expand, str_
+
+PARAM_SCHEME = expand(
+ base_key=str_, # todo: rename/remove
+
+ id=str_,
+ label=str_,
+ category=str_,
+
+ dtype=str_,
+ default=object,
+
+ options=list,
+ option_labels=list,
+ option_attributes=Spec(types=dict, required=False, item_scheme=(str_, list)),
+
+ hide=str_,
+)
+PORT_SCHEME = expand(
+ label=str_,
+ domain=str_,
+
+ id=str_,
+ dtype=str_,
+ vlen=(int, str_),
+
+ multiplicity=(int, str_),
+ optional=(bool, int, str_),
+ hide=(bool, str_),
+)
+TEMPLATES_SCHEME = expand(
+ imports=str_,
+ var_make=str_,
+ make=str_,
+ callbacks=list,
+)
+BLOCK_SCHEME = expand(
+ id=Spec(types=str_, required=True, item_scheme=None),
+ label=str_,
+ category=(list, str_),
+ flags=(list, str_),
+
+ parameters=Spec(types=list, required=False, item_scheme=PARAM_SCHEME),
+ inputs=Spec(types=list, required=False, item_scheme=PORT_SCHEME),
+ outputs=Spec(types=list, required=False, item_scheme=PORT_SCHEME),
+
+ checks=(list, str_),
+ value=str_,
+
+ templates=Spec(types=dict, required=False, item_scheme=TEMPLATES_SCHEME),
+
+ documentation=str_,
+
+ file_format=Spec(types=int, required=True, item_scheme=None),
+
+ block_wrapper_path=str_, # todo: rename/remove
+)
diff --git a/grc/core/schema_checker/domain.py b/grc/core/schema_checker/domain.py
new file mode 100644
index 0000000000..86c29ed3c6
--- /dev/null
+++ b/grc/core/schema_checker/domain.py
@@ -0,0 +1,16 @@
+from .utils import Spec, expand, str_
+
+DOMAIN_CONNECTION = expand(
+ type=Spec(types=list, required=True, item_scheme=None),
+ connect=str_,
+)
+
+DOMAIN_SCHEME = expand(
+ id=Spec(types=str_, required=True, item_scheme=None),
+ label=str_,
+ color=str_,
+ multiple_connections_per_input=bool,
+ multiple_connections_per_output=bool,
+
+ templates=Spec(types=list, required=False, item_scheme=DOMAIN_CONNECTION)
+) \ No newline at end of file
diff --git a/grc/core/schema_checker/flow_graph.py b/grc/core/schema_checker/flow_graph.py
new file mode 100644
index 0000000000..746fbf4aa7
--- /dev/null
+++ b/grc/core/schema_checker/flow_graph.py
@@ -0,0 +1,23 @@
+from .utils import Spec, expand, str_
+
+OPTIONS_SCHEME = expand(
+ parameters=Spec(types=dict, required=False, item_scheme=(str_, str_)),
+ states=Spec(types=dict, required=False, item_scheme=(str_, str_)),
+)
+
+BLOCK_SCHEME = expand(
+ name=str_,
+ id=str_,
+ **OPTIONS_SCHEME
+)
+
+FLOW_GRAPH_SCHEME = expand(
+ options=Spec(types=dict, required=False, item_scheme=OPTIONS_SCHEME),
+ blocks=Spec(types=dict, required=False, item_scheme=BLOCK_SCHEME),
+ connections=list,
+
+ metadata=Spec(types=dict, required=True, item_scheme=expand(
+ file_format=Spec(types=int, required=True, item_scheme=None),
+ ))
+
+)
diff --git a/grc/core/schema_checker/utils.py b/grc/core/schema_checker/utils.py
new file mode 100644
index 0000000000..a9cf4c0175
--- /dev/null
+++ b/grc/core/schema_checker/utils.py
@@ -0,0 +1,27 @@
+import collections
+
+import six
+
+Spec = collections.namedtuple('Spec', 'types required item_scheme')
+
+
+def expand(**kwargs):
+ def expand_spec(spec):
+ if not isinstance(spec, Spec):
+ types_ = spec if isinstance(spec, tuple) else (spec,)
+ spec = Spec(types=types_, required=False, item_scheme=None)
+ elif not isinstance(spec.types, tuple):
+ spec = Spec(types=(spec.types,), required=spec.required,
+ item_scheme=spec.item_scheme)
+ return spec
+ return {key: expand_spec(value) for key, value in kwargs.items()}
+
+
+str_ = six.string_types
+
+
+class Message(collections.namedtuple('Message', 'path type message')):
+ fmt = '{path}: {type}: {message}'
+
+ def __str__(self):
+ return self.fmt.format(**self._asdict())
diff --git a/grc/core/schema_checker/validator.py b/grc/core/schema_checker/validator.py
new file mode 100644
index 0000000000..ab4d43bc67
--- /dev/null
+++ b/grc/core/schema_checker/validator.py
@@ -0,0 +1,102 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import print_function
+
+import six
+
+from .utils import Message, Spec
+
+
+class Validator(object):
+
+ def __init__(self, scheme=None):
+ self._path = []
+ self.scheme = scheme
+ self.messages = []
+ self.passed = False
+
+ def run(self, data):
+ if not self.scheme:
+ return True
+ self._reset()
+ self._path.append('block')
+ self._check(data, self.scheme)
+ self._path.pop()
+ return self.passed
+
+ def _reset(self):
+ del self.messages[:]
+ del self._path[:]
+ self.passed = True
+
+ def _check(self, data, scheme):
+ if not data or not isinstance(data, dict):
+ self._error('Empty data or not a dict')
+ return
+ if isinstance(scheme, dict):
+ self._check_dict(data, scheme)
+ else:
+ self._check_var_key_dict(data, *scheme)
+
+ def _check_var_key_dict(self, data, key_type, value_scheme):
+ for key, value in six.iteritems(data):
+ if not isinstance(key, key_type):
+ self._error('Key type {!r} for {!r} not in valid types'.format(
+ type(value).__name__, key))
+ if isinstance(value_scheme, Spec):
+ self._check_dict(value, value_scheme)
+ elif not isinstance(value, value_scheme):
+ self._error('Value type {!r} for {!r} not in valid types'.format(
+ type(value).__name__, key))
+
+ def _check_dict(self, data, scheme):
+ for key, (types_, required, item_scheme) in six.iteritems(scheme):
+ try:
+ value = data[key]
+ except KeyError:
+ if required:
+ self._error('Missing required entry {!r}'.format(key))
+ continue
+
+ self._check_value(value, types_, item_scheme, label=key)
+
+ for key in set(data).difference(scheme):
+ self._warn('Ignoring extra key {!r}'.format(key))
+
+ def _check_list(self, data, scheme, label):
+ for i, item in enumerate(data):
+ self._path.append('{}[{}]'.format(label, i))
+ self._check(item, scheme)
+ self._path.pop()
+
+ def _check_value(self, value, types_, item_scheme, label):
+ if not isinstance(value, types_):
+ self._error('Value type {!r} for {!r} not in valid types'.format(
+ type(value).__name__, label))
+ if item_scheme:
+ if isinstance(value, list):
+ self._check_list(value, item_scheme, label)
+ elif isinstance(value, dict):
+ self._check(value, item_scheme)
+
+ def _error(self, msg):
+ self.messages.append(Message('.'.join(self._path), 'error', msg))
+ self.passed = False
+
+ def _warn(self, msg):
+ self.messages.append(Message('.'.join(self._path), 'warn', msg))
diff --git a/grc/core/utils/__init__.py b/grc/core/utils/__init__.py
index d095179a10..2d12e280b5 100644
--- a/grc/core/utils/__init__.py
+++ b/grc/core/utils/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2008-2015 Free Software Foundation, Inc.
+# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# GNU Radio Companion is free software; you can redistribute it and/or
@@ -17,8 +17,4 @@
from __future__ import absolute_import
-from . import expr_utils
-from . import epy_block_io
-from . import extract_docs
-
-from ._complexity import calculate_flowgraph_complexity
+from . import epy_block_io, expr_utils, extract_docs, flow_graph_complexity
diff --git a/grc/core/utils/backports/__init__.py b/grc/core/utils/backports/__init__.py
new file mode 100644
index 0000000000..a24ee3ae01
--- /dev/null
+++ b/grc/core/utils/backports/__init__.py
@@ -0,0 +1,25 @@
+# Copyright 2016 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+
+from __future__ import absolute_import
+
+try:
+ from collections import ChainMap
+except ImportError:
+ from .chainmap import ChainMap
diff --git a/grc/core/utils/backports/chainmap.py b/grc/core/utils/backports/chainmap.py
new file mode 100644
index 0000000000..1f4f4a96fb
--- /dev/null
+++ b/grc/core/utils/backports/chainmap.py
@@ -0,0 +1,106 @@
+# from https://hg.python.org/cpython/file/default/Lib/collections/__init__.py
+
+from collections import MutableMapping
+
+
+class ChainMap(MutableMapping):
+ """ A ChainMap groups multiple dicts (or other mappings) together
+ to create a single, updateable view.
+
+ The underlying mappings are stored in a list. That list is public and can
+ be accessed or updated using the *maps* attribute. There is no other
+ state.
+
+ Lookups search the underlying mappings successively until a key is found.
+ In contrast, writes, updates, and deletions only operate on the first
+ mapping.
+
+ """
+
+ def __init__(self, *maps):
+ """Initialize a ChainMap by setting *maps* to the given mappings.
+ If no mappings are provided, a single empty dictionary is used.
+
+ """
+ self.maps = list(maps) or [{}] # always at least one map
+
+ def __missing__(self, key):
+ raise KeyError(key)
+
+ def __getitem__(self, key):
+ for mapping in self.maps:
+ try:
+ return mapping[key] # can't use 'key in mapping' with defaultdict
+ except KeyError:
+ pass
+ return self.__missing__(key) # support subclasses that define __missing__
+
+ def get(self, key, default=None):
+ return self[key] if key in self else default
+
+ def __len__(self):
+ return len(set().union(*self.maps)) # reuses stored hash values if possible
+
+ def __iter__(self):
+ return iter(set().union(*self.maps))
+
+ def __contains__(self, key):
+ return any(key in m for m in self.maps)
+
+ def __bool__(self):
+ return any(self.maps)
+
+ def __repr__(self):
+ return '{0.__class__.__name__}({1})'.format(
+ self, ', '.join(map(repr, self.maps)))
+
+ @classmethod
+ def fromkeys(cls, iterable, *args):
+ """Create a ChainMap with a single dict created from the iterable."""
+ return cls(dict.fromkeys(iterable, *args))
+
+ def copy(self):
+ """New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]"""
+ return self.__class__(self.maps[0].copy(), *self.maps[1:])
+
+ __copy__ = copy
+
+ def new_child(self, m=None): # like Django's Context.push()
+ """New ChainMap with a new map followed by all previous maps.
+ If no map is provided, an empty dict is used.
+ """
+ if m is None:
+ m = {}
+ return self.__class__(m, *self.maps)
+
+ @property
+ def parents(self): # like Django's Context.pop()
+ """New ChainMap from maps[1:]."""
+ return self.__class__(*self.maps[1:])
+
+ def __setitem__(self, key, value):
+ self.maps[0][key] = value
+
+ def __delitem__(self, key):
+ try:
+ del self.maps[0][key]
+ except KeyError:
+ raise KeyError('Key not found in the first mapping: {!r}'.format(key))
+
+ def popitem(self):
+ """Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty."""
+ try:
+ return self.maps[0].popitem()
+ except KeyError:
+ raise KeyError('No keys found in the first mapping.')
+
+ def pop(self, key, *args):
+ """Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0]."""
+ try:
+ return self.maps[0].pop(key, *args)
+ except KeyError:
+ raise KeyError('Key not found in the first mapping: {!r}'.format(key))
+
+ def clear(self):
+ """Clear maps[0], leaving maps[1:] intact."""
+ self.maps[0].clear()
diff --git a/grc/core/utils/shlex.py b/grc/core/utils/backports/shlex.py
index 6b620fa396..6b620fa396 100644
--- a/grc/core/utils/shlex.py
+++ b/grc/core/utils/backports/shlex.py
diff --git a/grc/core/utils/descriptors/__init__.py b/grc/core/utils/descriptors/__init__.py
new file mode 100644
index 0000000000..80c5259230
--- /dev/null
+++ b/grc/core/utils/descriptors/__init__.py
@@ -0,0 +1,26 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from ._lazy import lazy_property, nop_write
+
+from .evaluated import (
+ Evaluated,
+ EvaluatedEnum,
+ EvaluatedPInt,
+ EvaluatedFlag,
+ setup_names,
+)
diff --git a/grc/core/utils/descriptors/_lazy.py b/grc/core/utils/descriptors/_lazy.py
new file mode 100644
index 0000000000..a0cb126932
--- /dev/null
+++ b/grc/core/utils/descriptors/_lazy.py
@@ -0,0 +1,39 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+import functools
+
+
+class lazy_property(object):
+
+ def __init__(self, func):
+ self.func = func
+ functools.update_wrapper(self, func)
+
+ def __get__(self, instance, owner):
+ if instance is None:
+ return self
+ value = self.func(instance)
+ setattr(instance, self.func.__name__, value)
+ return value
+
+
+def nop_write(prop):
+ """Make this a property with a nop setter"""
+ def nop(self, value):
+ pass
+ return prop.setter(nop)
diff --git a/grc/core/utils/descriptors/evaluated.py b/grc/core/utils/descriptors/evaluated.py
new file mode 100644
index 0000000000..313cee5b96
--- /dev/null
+++ b/grc/core/utils/descriptors/evaluated.py
@@ -0,0 +1,112 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+
+class Evaluated(object):
+ def __init__(self, expected_type, default, name=None):
+ self.expected_type = expected_type
+ self.default = default
+
+ self.name = name or 'evaled_property_{}'.format(id(self))
+ self.eval_function = self.default_eval_func
+
+ @property
+ def name_raw(self):
+ return '_' + self.name
+
+ def default_eval_func(self, instance):
+ raw = getattr(instance, self.name_raw)
+ try:
+ value = instance.parent_block.evaluate(raw)
+ except Exception as error:
+ if raw:
+ instance.add_error_message("Failed to eval '{}': {}".format(raw, error))
+ return self.default
+
+ if not isinstance(value, self.expected_type):
+ instance.add_error_message("Can not cast evaluated value '{}' to type {}"
+ "".format(value, self.expected_type))
+ return self.default
+ # print(instance, self.name, raw, value)
+ return value
+
+ def __call__(self, func):
+ self.name = func.__name__
+ self.eval_function = func
+ return self
+
+ def __get__(self, instance, owner):
+ if instance is None:
+ return self
+ attribs = instance.__dict__
+ try:
+ value = attribs[self.name]
+ except KeyError:
+ value = attribs[self.name] = self.eval_function(instance)
+ return value
+
+ def __set__(self, instance, value):
+ attribs = instance.__dict__
+ value = value or self.default
+ if isinstance(value, str) and value.startswith('${') and value.endswith('}'):
+ attribs[self.name_raw] = value[2:-1].strip()
+ else:
+ attribs[self.name] = type(self.default)(value)
+
+ def __delete__(self, instance):
+ attribs = instance.__dict__
+ if self.name_raw in attribs:
+ attribs.pop(self.name, None)
+
+
+class EvaluatedEnum(Evaluated):
+ def __init__(self, allowed_values, default=None, name=None):
+ self.allowed_values = allowed_values if isinstance(allowed_values, (list, tuple)) else \
+ allowed_values.split()
+ default = default if default is not None else self.allowed_values[0]
+ super(EvaluatedEnum, self).__init__(str, default, name)
+
+ def default_eval_func(self, instance):
+ value = super(EvaluatedEnum, self).default_eval_func(instance)
+ if value not in self.allowed_values:
+ instance.add_error_message("Value '{}' not in allowed values".format(value))
+ return self.default
+ return value
+
+
+class EvaluatedPInt(Evaluated):
+ def __init__(self, name=None):
+ super(EvaluatedPInt, self).__init__(int, 1, name)
+
+ def default_eval_func(self, instance):
+ value = super(EvaluatedPInt, self).default_eval_func(instance)
+ if value < 1:
+ # todo: log
+ return self.default
+ return value
+
+
+class EvaluatedFlag(Evaluated):
+ def __init__(self, name=None):
+ super(EvaluatedFlag, self).__init__((bool, int), False, name)
+
+
+def setup_names(cls):
+ for name, attrib in cls.__dict__.items():
+ if isinstance(attrib, Evaluated):
+ attrib.name = name
+ return cls
diff --git a/grc/core/utils/expr_utils.py b/grc/core/utils/expr_utils.py
index cc03e9cb1c..427585e93c 100644
--- a/grc/core/utils/expr_utils.py
+++ b/grc/core/utils/expr_utils.py
@@ -23,17 +23,105 @@ import string
import six
+
+def expr_replace(expr, replace_dict):
+ """
+ Search for vars in the expression and add the prepend.
+
+ Args:
+ expr: an expression string
+ replace_dict: a dict of find:replace
+
+ Returns:
+ a new expression with the prepend
+ """
+ expr_splits = _expr_split(expr, var_chars=VAR_CHARS + '.')
+ for i, es in enumerate(expr_splits):
+ if es in list(replace_dict.keys()):
+ expr_splits[i] = replace_dict[es]
+ return ''.join(expr_splits)
+
+
+def get_variable_dependencies(expr, vars):
+ """
+ Return a set of variables used in this expression.
+
+ Args:
+ expr: an expression string
+ vars: a list of variable names
+
+ Returns:
+ a subset of vars used in the expression
+ """
+ expr_toks = _expr_split(expr)
+ return set(v for v in vars if v in expr_toks)
+
+
+def sort_objects(objects, get_id, get_expr):
+ """
+ Sort a list of objects according to their expressions.
+
+ Args:
+ objects: the list of objects to sort
+ get_id: the function to extract an id from the object
+ get_expr: the function to extract an expression from the object
+
+ Returns:
+ a list of sorted objects
+ """
+ id2obj = {get_id(obj): obj for obj in objects}
+ # Map obj id to expression code
+ id2expr = {get_id(obj): get_expr(obj) for obj in objects}
+ # Sort according to dependency
+ sorted_ids = _sort_variables(id2expr)
+ # Return list of sorted objects
+ return [id2obj[id] for id in sorted_ids]
+
+
+import ast
+
+
+def dependencies(expr, names=None):
+ node = ast.parse(expr, mode='eval')
+ used_ids = frozenset([n.id for n in ast.walk(node) if isinstance(n, ast.Name)])
+ return used_ids & names if names else used_ids
+
+
+def sort_objects2(objects, id_getter, expr_getter, check_circular=True):
+ known_ids = {id_getter(obj) for obj in objects}
+
+ def dependent_ids(obj):
+ deps = dependencies(expr_getter(obj))
+ return [id_ if id_ in deps else None for id_ in known_ids]
+
+ objects = sorted(objects, key=dependent_ids)
+
+ if check_circular: # walk var defines step by step
+ defined_ids = set() # variables defined so far
+ for obj in objects:
+ deps = dependencies(expr_getter(obj), known_ids)
+ if not defined_ids.issuperset(deps): # can't have an undefined dep
+ raise RuntimeError(obj, deps, defined_ids)
+ defined_ids.add(id_getter(obj)) # define this one
+
+ return objects
+
+
+
+
VAR_CHARS = string.ascii_letters + string.digits + '_'
-class graph(object):
+class _graph(object):
"""
Simple graph structure held in a dictionary.
"""
- def __init__(self): self._graph = dict()
+ def __init__(self):
+ self._graph = dict()
- def __str__(self): return str(self._graph)
+ def __str__(self):
+ return str(self._graph)
def add_node(self, node_key):
if node_key in self._graph:
@@ -61,7 +149,7 @@ class graph(object):
return self._graph[node_key]
-def expr_split(expr, var_chars=VAR_CHARS):
+def _expr_split(expr, var_chars=VAR_CHARS):
"""
Split up an expression by non alphanumeric characters, including underscore.
Leave strings in-tact.
@@ -93,40 +181,7 @@ def expr_split(expr, var_chars=VAR_CHARS):
return [t for t in toks if t]
-def expr_replace(expr, replace_dict):
- """
- Search for vars in the expression and add the prepend.
-
- Args:
- expr: an expression string
- replace_dict: a dict of find:replace
-
- Returns:
- a new expression with the prepend
- """
- expr_splits = expr_split(expr, var_chars=VAR_CHARS + '.')
- for i, es in enumerate(expr_splits):
- if es in list(replace_dict.keys()):
- expr_splits[i] = replace_dict[es]
- return ''.join(expr_splits)
-
-
-def get_variable_dependencies(expr, vars):
- """
- Return a set of variables used in this expression.
-
- Args:
- expr: an expression string
- vars: a list of variable names
-
- Returns:
- a subset of vars used in the expression
- """
- expr_toks = expr_split(expr)
- return set(v for v in vars if v in expr_toks)
-
-
-def get_graph(exprs):
+def _get_graph(exprs):
"""
Get a graph representing the variable dependencies
@@ -138,7 +193,7 @@ def get_graph(exprs):
"""
vars = list(exprs.keys())
# Get dependencies for each expression, load into graph
- var_graph = graph()
+ var_graph = _graph()
for var in vars:
var_graph.add_node(var)
for var, expr in six.iteritems(exprs):
@@ -148,7 +203,7 @@ def get_graph(exprs):
return var_graph
-def sort_variables(exprs):
+def _sort_variables(exprs):
"""
Get a list of variables in order of dependencies.
@@ -159,7 +214,7 @@ def sort_variables(exprs):
a list of variable names
@throws Exception circular dependencies
"""
- var_graph = get_graph(exprs)
+ var_graph = _get_graph(exprs)
sorted_vars = list()
# Determine dependency order
while var_graph.get_nodes():
@@ -173,24 +228,3 @@ def sort_variables(exprs):
for var in indep_vars:
var_graph.remove_node(var)
return reversed(sorted_vars)
-
-
-def sort_objects(objects, get_id, get_expr):
- """
- Sort a list of objects according to their expressions.
-
- Args:
- objects: the list of objects to sort
- get_id: the function to extract an id from the object
- get_expr: the function to extract an expression from the object
-
- Returns:
- a list of sorted objects
- """
- id2obj = dict([(get_id(obj), obj) for obj in objects])
- # Map obj id to expression code
- id2expr = dict([(get_id(obj), get_expr(obj)) for obj in objects])
- # Sort according to dependency
- sorted_ids = sort_variables(id2expr)
- # Return list of sorted objects
- return [id2obj[id] for id in sorted_ids]
diff --git a/grc/core/utils/extract_docs.py b/grc/core/utils/extract_docs.py
index cff8a81099..7688f98de5 100644
--- a/grc/core/utils/extract_docs.py
+++ b/grc/core/utils/extract_docs.py
@@ -98,8 +98,7 @@ def docstring_from_make(key, imports, make):
if '$' in blk_cls:
raise ValueError('Not an identifier')
ns = dict()
- for _import in imports:
- exec(_import.strip(), ns)
+ exec(imports.strip(), ns)
blk = eval(blk_cls, ns)
doc_strings = {key: blk.__doc__}
@@ -166,7 +165,8 @@ class SubprocessLoader(object):
else:
break # normal termination, return
finally:
- self._worker.terminate()
+ if self._worker:
+ self._worker.terminate()
else:
print("Warning: docstring loader crashed too often", file=sys.stderr)
self._thread = None
@@ -277,7 +277,7 @@ elif __name__ == '__main__':
print(key)
for match, doc in six.iteritems(docs):
print('-->', match)
- print(doc.strip())
+ print(str(doc).strip())
print()
print()
diff --git a/grc/core/utils/_complexity.py b/grc/core/utils/flow_graph_complexity.py
index c0f3ae9de4..d06f04ab5f 100644
--- a/grc/core/utils/_complexity.py
+++ b/grc/core/utils/flow_graph_complexity.py
@@ -1,5 +1,5 @@
-def calculate_flowgraph_complexity(flowgraph):
+def calculate(flowgraph):
""" Determines the complexity of a flowgraph """
dbal = 0
for block in flowgraph.blocks:
@@ -8,8 +8,8 @@ def calculate_flowgraph_complexity(flowgraph):
continue
# Don't worry about optional sinks?
- sink_list = [c for c in block.sinks if not c.get_optional()]
- source_list = [c for c in block.sources if not c.get_optional()]
+ sink_list = [c for c in block.sinks if not c.optional]
+ source_list = [c for c in block.sources if not c.optional]
sinks = float(len(sink_list))
sources = float(len(source_list))
base = max(min(sinks, sources), 1)
@@ -22,8 +22,8 @@ def calculate_flowgraph_complexity(flowgraph):
multi = 1
# Connection ratio multiplier
- sink_multi = max(float(sum(len(c.get_connections()) for c in sink_list) / max(sinks, 1.0)), 1.0)
- source_multi = max(float(sum(len(c.get_connections()) for c in source_list) / max(sources, 1.0)), 1.0)
+ sink_multi = max(float(sum(len(c.connections()) for c in sink_list) / max(sinks, 1.0)), 1.0)
+ source_multi = max(float(sum(len(c.connections()) for c in source_list) / max(sources, 1.0)), 1.0)
dbal += base * multi * sink_multi * source_multi
blocks = float(len(flowgraph.blocks))