diff options
Diffstat (limited to 'grc/core')
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)) |