diff options
Diffstat (limited to 'grc/core')
65 files changed, 4826 insertions, 4250 deletions
diff --git a/grc/core/Block.py b/grc/core/Block.py deleted file mode 100644 index e80552a0a8..0000000000 --- a/grc/core/Block.py +++ /dev/null @@ -1,852 +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 -""" - -import collections -import itertools - -from Cheetah.Template import Template - -from .utils import epy_block_io, odict -from . Constants import ( - BLOCK_FLAG_NEED_QT_GUI, - ADVANCED_PARAM_TAB, DEFAULT_PARAM_TAB, - BLOCK_FLAG_THROTTLE, BLOCK_FLAG_DISABLE_BYPASS, - BLOCK_FLAG_DEPRECATED, - BLOCK_ENABLED, BLOCK_BYPASSED, BLOCK_DISABLED -) -from . Element import Element - - -def _get_keys(lst): - return [elem.get_key() for elem in lst] - - -def _get_elem(lst, key): - try: - return lst[_get_keys(lst).index(key)] - except ValueError: - raise ValueError('Key "{}" not found in {}.'.format(key, _get_keys(lst))) - - -class Block(Element): - - is_block = True - - def __init__(self, flow_graph, n): - """ - Make a new block from nested data. - - Args: - flow: graph the parent element - n: the nested odict - - Returns: - block a new block - """ - # Grab the data - self._doc = (n.find('doc') or '').strip('\n').replace('\\\n', '') - self._imports = map(lambda i: i.strip(), n.findall('import')) - self._make = n.find('make') - self._var_make = n.find('var_make') - self._checks = n.findall('check') - self._callbacks = n.findall('callback') - self._bus_structure_source = n.find('bus_structure_source') or '' - self._bus_structure_sink = n.find('bus_structure_sink') or '' - self.port_counters = [itertools.count(), itertools.count()] - - # Build the block - Element.__init__(self, flow_graph) - - # Grab the data - params = n.findall('param') - sources = n.findall('source') - sinks = n.findall('sink') - self._name = n.find('name') - self._key = n.find('key') - category = (n.find('category') or '').split('/') - self.category = [cat.strip() for cat in category if cat.strip()] - self._flags = n.find('flags') or '' - # Backwards compatibility - if n.find('throttle') and BLOCK_FLAG_THROTTLE not in self._flags: - self._flags += BLOCK_FLAG_THROTTLE - self._grc_source = n.find('grc_source') or '' - self._block_wrapper_path = n.find('block_wrapper_path') - self._bussify_sink = n.find('bus_sink') - self._bussify_source = n.find('bus_source') - self._var_value = n.find('var_value') or '$value' - - # Get list of param tabs - n_tabs = n.find('param_tab_order') or None - self._param_tab_labels = n_tabs.findall('tab') if n_tabs is not None else [DEFAULT_PARAM_TAB] - - # Create the param objects - self._params = list() - - # Add the id param - self.get_params().append(self.get_parent().get_parent().Param( - block=self, - n=odict({ - 'name': 'ID', - 'key': 'id', - 'type': 'id', - }) - )) - self.get_params().append(self.get_parent().get_parent().Param( - block=self, - n=odict({ - 'name': 'Enabled', - 'key': '_enabled', - 'type': 'raw', - 'value': 'True', - 'hide': 'all', - }) - )) - for param in itertools.imap(lambda n: self.get_parent().get_parent().Param(block=self, n=n), params): - key = param.get_key() - # Test against repeated keys - if key in self.get_param_keys(): - raise Exception('Key "{}" already exists in params'.format(key)) - # Store the param - self.get_params().append(param) - # Create the source objects - self._sources = list() - for source in map(lambda n: self.get_parent().get_parent().Port(block=self, n=n, dir='source'), sources): - key = source.get_key() - # Test against repeated keys - if key in self.get_source_keys(): - raise Exception('Key "{}" already exists in sources'.format(key)) - # Store the port - self.get_sources().append(source) - self.back_ofthe_bus(self.get_sources()) - # Create the sink objects - self._sinks = list() - for sink in map(lambda n: self.get_parent().get_parent().Port(block=self, n=n, dir='sink'), sinks): - key = sink.get_key() - # Test against repeated keys - if key in self.get_sink_keys(): - raise Exception('Key "{}" already exists in sinks'.format(key)) - # Store the port - self.get_sinks().append(sink) - self.back_ofthe_bus(self.get_sinks()) - self.current_bus_structure = {'source': '', 'sink': ''} - - # 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 - - if not (self.is_virtual_or_pad or self.is_variable or self._key == 'options'): - self.get_params().append(self.get_parent().get_parent().Param( - block=self, - n=odict({'name': 'Block Alias', - 'key': 'alias', - 'type': 'string', - 'hide': 'part', - 'tab': ADVANCED_PARAM_TAB - }) - )) - - if (len(sources) or len(sinks)) and not self.is_virtual_or_pad: - self.get_params().append(self.get_parent().get_parent().Param( - block=self, - n=odict({'name': 'Core Affinity', - 'key': 'affinity', - 'type': 'int_vector', - 'hide': 'part', - 'tab': ADVANCED_PARAM_TAB - }) - )) - if len(sources) and not self.is_virtual_or_pad: - self.get_params().append(self.get_parent().get_parent().Param( - block=self, - n=odict({'name': 'Min Output Buffer', - 'key': 'minoutbuf', - 'type': 'int', - 'hide': 'part', - 'value': '0', - 'tab': ADVANCED_PARAM_TAB - }) - )) - self.get_params().append(self.get_parent().get_parent().Param( - block=self, - n=odict({'name': 'Max Output Buffer', - 'key': 'maxoutbuf', - 'type': 'int', - 'hide': 'part', - 'value': '0', - 'tab': ADVANCED_PARAM_TAB - }) - )) - - self.get_params().append(self.get_parent().get_parent().Param( - block=self, - n=odict({'name': 'Comment', - 'key': 'comment', - 'type': '_multiline', - 'hide': 'part', - 'value': '', - 'tab': ADVANCED_PARAM_TAB - }) - )) - - self._epy_source_hash = -1 # for epy blocks - self._epy_reload_error = None - - if self._bussify_sink: - self.bussify({'name': 'bus', 'type': 'bus'}, 'sink') - if self._bussify_source: - self.bussify({'name': 'bus', 'type': 'bus'}, 'source') - - def get_bus_structure(self, direction): - if direction == 'source': - bus_structure = self._bus_structure_source - else: - bus_structure = self._bus_structure_sink - - bus_structure = self.resolve_dependencies(bus_structure) - - if not bus_structure: - return '' # TODO: Don't like empty strings. should change this to None eventually - - try: - clean_bus_structure = self.get_parent().evaluate(bus_structure) - return clean_bus_structure - except: - return '' - - def validate(self): - """ - Validate this block. - Call the base class validate. - Evaluate the checks: each check must evaluate to True. - """ - Element.validate(self) - # Evaluate the checks - for check in self._checks: - check_res = self.resolve_dependencies(check) - try: - if not self.get_parent().evaluate(check_res): - self.add_error_message('Check "{}" failed.'.format(check)) - except: - self.add_error_message('Check "{}" did not evaluate.'.format(check)) - - # For 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.get_parent().evaluate(value) - except Exception as err: - self.add_error_message('Value "{}" cannot be evaluated:\n{}'.format(value, err)) - - # check if this is a GUI block and matches the selected generate option - current_generate_option = self.get_parent().get_option('generate_options') - - def check_generate_mode(label, flag, valid_options): - block_requires_mode = ( - flag in self.get_flags() or - self.get_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')) - if self._epy_reload_error: - self.get_param('_source_code').add_error_message(str(self._epy_reload_error)) - - def rewrite(self): - """ - Add and remove ports to adjust for the nports. - """ - Element.rewrite(self) - # Check and run any custom rewrite function for this block - getattr(self, 'rewrite_' + self._key, lambda: None)() - - # Adjust nports, disconnect hidden ports - for ports in (self.get_sources(), self.get_sinks()): - for i, master_port in enumerate(ports): - nports = master_port.get_nports() or 1 - num_ports = 1 + len(master_port.get_clones()) - if master_port.get_hide(): - for connection in master_port.get_connections(): - self.get_parent().remove_element(connection) - if not nports and num_ports == 1: # Not a master port and no left-over clones - continue - # Remove excess cloned ports - for port in master_port.get_clones()[nports-1:]: - # Remove excess connections - for connection in port.get_connections(): - self.get_parent().remove_element(connection) - master_port.remove_clone(port) - ports.remove(port) - # Add more cloned ports - for j in range(num_ports, nports): - port = master_port.add_clone() - ports.insert(ports.index(master_port) + j, port) - - self.back_ofthe_bus(ports) - # Renumber non-message/message ports - domain_specific_port_index = collections.defaultdict(int) - for port in filter(lambda p: p.get_key().isdigit(), ports): - domain = port.get_domain() - port._key = str(domain_specific_port_index[domain]) - domain_specific_port_index[domain] += 1 - - def port_controller_modify(self, direction): - """ - Change the port controller. - - Args: - direction: +1 or -1 - - Returns: - true for change - """ - changed = False - # Concat the nports string from the private nports settings of all ports - nports_str = ' '.join([port._nports for port in self.get_ports()]) - # Modify all params whose keys appear in the nports string - for param in self.get_params(): - if param.is_enum() or param.get_key() not in nports_str: - continue - # Try to increment the port controller by direction - try: - value = param.get_evaluated() - value = value + direction - if 0 < value: - param.set_value(value) - changed = True - except: - pass - return changed - - def get_doc(self): - platform = self.get_parent().get_parent() - documentation = platform.block_docstrings.get(self._key, {}) - from_xml = self._doc.strip() - if from_xml: - documentation[''] = from_xml - return documentation - - 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 filter(lambda i: i, sum(map(lambda i: self.resolve_dependencies(i).split('\n'), self._imports), [])) - - 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 map(make_callback, self._callbacks) - - def is_virtual_sink(self): - return self.get_key() == 'virtual_sink' - - def is_virtual_source(self): - return self.get_key() == 'virtual_source' - - ########################################################################### - # Custom rewrite functions - ########################################################################### - - def rewrite_epy_block(self): - flowgraph = self.get_parent() - platform = flowgraph.get_parent() - param_blk = self.get_param('_io_cache') - param_src = self.get_param('_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 = 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 = 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] - - params = {} - for param in list(self._params): - if hasattr(param, '__epy_param__'): - params[param.get_key()] = param - self._params.remove(param) - - for key, value in blk_io.params: - try: - param = params[key] - param.set_default(value) - except KeyError: # need to make a new param - name = key.replace('_', ' ').title() - n = odict(dict(name=name, key=key, type='raw', value=value)) - param = platform.Param(block=self, n=n) - setattr(param, '__epy_param__', True) - self._params.append(param) - - def update_ports(label, ports, port_specs, direction): - 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.get_key() == key) - ) - if reuse_port: - ports_to_remove.remove(port_current) - port, port_current = port_current, next(iter_ports, None) - else: - n = odict(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 = platform.Port(block=self, n=n, dir=direction) - 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(): - flowgraph.remove_element(connection) - - update_ports('in', self.get_sinks(), blk_io.sinks, 'sink') - update_ports('out', self.get_sources(), blk_io.sources, 'source') - self.rewrite() - - def back_ofthe_bus(self, portlist): - portlist.sort(key=lambda p: p._type == 'bus') - - def filter_bus_port(self, ports): - buslist = [p for p in ports if p._type == 'bus'] - return buslist or ports - - # Main functions to get and set the block state - # Also kept get_enabled and set_enabled to keep compatibility - def get_state(self): - """ - Gets the block's current state. - - Returns: - ENABLED - 0 - BYPASSED - 1 - DISABLED - 2 - """ - try: - return int(eval(self.get_param('_enabled').get_value())) - except: - return BLOCK_ENABLED - - def set_state(self, state): - """ - Sets the state for the block. - - Args: - ENABLED - 0 - BYPASSED - 1 - DISABLED - 2 - """ - if state in [BLOCK_ENABLED, BLOCK_BYPASSED, BLOCK_DISABLED]: - self.get_param('_enabled').set_value(str(state)) - else: - self.get_param('_enabled').set_value(str(BLOCK_ENABLED)) - - # Enable/Disable Aliases - def get_enabled(self): - """ - Get the enabled state of the block. - - Returns: - true for enabled - """ - return not (self.get_state() == BLOCK_DISABLED) - - def set_enabled(self, enabled): - """ - Set the enabled state of the block. - - Args: - enabled: true for enabled - - Returns: - True if block changed state - """ - old_state = self.get_state() - new_state = BLOCK_ENABLED if enabled else BLOCK_DISABLED - self.set_state(new_state) - return old_state != new_state - - # Block bypassing - def get_bypassed(self): - """ - Check if the block is bypassed - """ - return self.get_state() == BLOCK_BYPASSED - - def set_bypassed(self): - """ - Bypass the block - - Returns: - True if block chagnes state - """ - if self.get_state() != BLOCK_BYPASSED and self.can_bypass(): - self.set_state(BLOCK_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.get_sources()) != 1 or len(self.get_sinks()) != 1: - return False - if not (self.get_sources()[0].get_type() == self.get_sinks()[0].get_type()): - return False - if self.bypass_disabled(): - return False - return True - - def __str__(self): - return 'Block - {} - {}({})'.format(self.get_id(), self.get_name(), self.get_key()) - - def get_id(self): - return self.get_param('id').get_value() - - def get_name(self): - return self._name - - def get_key(self): - return self._key - - def get_ports(self): - return self.get_sources() + self.get_sinks() - - def get_ports_gui(self): - return self.filter_bus_port(self.get_sources()) + self.filter_bus_port(self.get_sinks()) - - def get_children(self): - return self.get_ports() + self.get_params() - - def get_children_gui(self): - return self.get_ports_gui() + self.get_params() - - def get_block_wrapper_path(self): - return self._block_wrapper_path - - def get_comment(self): - return self.get_param('comment').get_value() - - def get_flags(self): - return self._flags - - def throtteling(self): - return BLOCK_FLAG_THROTTLE in self._flags - - def bypass_disabled(self): - return BLOCK_FLAG_DISABLE_BYPASS in self._flags - - @property - def is_deprecated(self): - return BLOCK_FLAG_DEPRECATED in self._flags - - ############################################## - # Access Params - ############################################## - def get_param_tab_labels(self): - return self._param_tab_labels - - def get_param_keys(self): - return _get_keys(self._params) - - def get_param(self, key): - return _get_elem(self._params, key) - - def get_params(self): - return self._params - - def has_param(self, key): - try: - _get_elem(self._params, key) - return True - except: - return False - - ############################################## - # Access Sinks - ############################################## - def get_sink_keys(self): - return _get_keys(self._sinks) - - def get_sink(self, key): - return _get_elem(self._sinks, key) - - def get_sinks(self): - return self._sinks - - def get_sinks_gui(self): - return self.filter_bus_port(self.get_sinks()) - - ############################################## - # Access Sources - ############################################## - def get_source_keys(self): - return _get_keys(self._sources) - - def get_source(self, key): - return _get_elem(self._sources, key) - - def get_sources(self): - return self._sources - - def get_sources_gui(self): - return self.filter_bus_port(self.get_sources()) - - def get_connections(self): - return sum([port.get_connections() for port in self.get_ports()], []) - - def resolve_dependencies(self, tmpl): - """ - Resolve a parameter dependency with cheetah templates. - - Args: - tmpl: the string with dependencies - - Returns: - the resolved value - """ - tmpl = str(tmpl) - if '$' not in tmpl: - return tmpl - n = dict((param.get_key(), param.template_arg) - for param in self.get_params()) # TODO: cache that - try: - return str(Template(tmpl, n)) - except Exception as err: - return "Template error: {}\n {}".format(tmpl, err) - - ############################################## - # Controller Modify - ############################################## - def type_controller_modify(self, direction): - """ - Change the type controller. - - Args: - direction: +1 or -1 - - Returns: - true for change - """ - changed = False - type_param = None - for param in filter(lambda p: p.is_enum(), self.get_params()): - children = self.get_ports() + self.get_params() - # Priority to the type controller - if param.get_key() in ' '.join(map(lambda p: p._type, children)): type_param = param - # Use param if type param is unset - if not type_param: - type_param = param - if type_param: - # Try to increment the enum by direction - try: - keys = type_param.get_option_keys() - old_index = keys.index(type_param.get_value()) - new_index = (old_index + direction + len(keys)) % len(keys) - type_param.set_value(keys[new_index]) - changed = True - except: - pass - return changed - - def form_bus_structure(self, direc): - if direc == 'source': - get_p = self.get_sources - get_p_gui = self.get_sources_gui - bus_structure = self.get_bus_structure('source') - else: - get_p = self.get_sinks - get_p_gui = self.get_sinks_gui - bus_structure = self.get_bus_structure('sink') - - struct = [range(len(get_p()))] - if True in map(lambda a: isinstance(a.get_nports(), int), get_p()): - structlet = [] - last = 0 - for j in [i.get_nports() for i in get_p() if isinstance(i.get_nports(), int)]: - structlet.extend(map(lambda a: a+last, range(j))) - last = structlet[-1] + 1 - struct = [structlet] - if bus_structure: - - struct = bus_structure - - self.current_bus_structure[direc] = struct - return struct - - def bussify(self, n, direc): - if direc == 'source': - get_p = self.get_sources - get_p_gui = self.get_sources_gui - bus_structure = self.get_bus_structure('source') - else: - get_p = self.get_sinks - get_p_gui = self.get_sinks_gui - bus_structure = self.get_bus_structure('sink') - - for elt in get_p(): - for connect in elt.get_connections(): - self.get_parent().remove_element(connect) - - if ('bus' not in map(lambda a: a.get_type(), get_p())) and len(get_p()) > 0: - struct = self.form_bus_structure(direc) - self.current_bus_structure[direc] = struct - if get_p()[0].get_nports(): - n['nports'] = str(1) - - for i in range(len(struct)): - n['key'] = str(len(get_p())) - n = odict(n) - port = self.get_parent().get_parent().Port(block=self, n=n, dir=direc) - get_p().append(port) - elif 'bus' in map(lambda a: a.get_type(), get_p()): - for elt in get_p_gui(): - get_p().remove(elt) - self.current_bus_structure[direc] = '' - - ############################################## - # Import/Export Methods - ############################################## - def export_data(self): - """ - Export this block's params to nested data. - - Returns: - a nested data odict - """ - n = odict() - n['key'] = self.get_key() - n['param'] = map(lambda p: p.export_data(), sorted(self.get_params(), key=str)) - if 'bus' in map(lambda a: a.get_type(), self.get_sinks()): - n['bus_sink'] = str(1) - if 'bus' in map(lambda a: a.get_type(), self.get_sources()): - n['bus_source'] = str(1) - return n - - def get_hash(self): - return hash(tuple(map(hash, self.get_params()))) - - 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 - """ - my_hash = 0 - while self.get_hash() != my_hash: - params_n = n.findall('param') - for param_n in params_n: - key = param_n.find('key') - value = param_n.find('value') - # The key must exist in this block's params - if key in self.get_param_keys(): - self.get_param(key).set_value(value) - # Store hash and call rewrite - my_hash = self.get_hash() - self.rewrite() - bussinks = n.findall('bus_sink') - if len(bussinks) > 0 and not self._bussify_sink: - self.bussify({'name': 'bus', 'type': 'bus'}, 'sink') - elif len(bussinks) > 0: - self.bussify({'name': 'bus', 'type': 'bus'}, 'sink') - self.bussify({'name': 'bus', 'type': 'bus'}, 'sink') - bussrcs = n.findall('bus_source') - if len(bussrcs) > 0 and not self._bussify_source: - self.bussify({'name': 'bus', 'type': 'bus'}, 'source') - elif len(bussrcs) > 0: - self.bussify({'name': 'bus', 'type': 'bus'}, 'source') - self.bussify({'name': 'bus', 'type': 'bus'}, 'source') diff --git a/grc/core/CMakeLists.txt b/grc/core/CMakeLists.txt deleted file mode 100644 index f340127873..0000000000 --- a/grc/core/CMakeLists.txt +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2011 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. - -file(GLOB py_files "*.py") - -GR_PYTHON_INSTALL( - FILES ${py_files} - DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/core -) - -file(GLOB dtd_files "*.dtd") - -install( - FILES ${dtd_files} default_flow_graph.grc - DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/core -) - -add_subdirectory(generator) -add_subdirectory(utils) diff --git a/grc/core/Config.py b/grc/core/Config.py index 744ad06ba9..4accb74c63 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 @@ -17,6 +16,8 @@ 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 os from os.path import expanduser, normpath, expandvars, exists @@ -24,16 +25,14 @@ from . import Constants class Config(object): - - key = 'grc' name = 'GNU Radio Companion (no gui)' license = __doc__.strip() website = 'http://gnuradio.org' hier_block_lib_dir = os.environ.get('GRC_HIER_PATH', Constants.DEFAULT_HIER_BLOCK_LIB_DIR) - def __init__(self, prefs_file, version, version_parts=None, name=None): - self.prefs = prefs_file + def __init__(self, version, version_parts=None, name=None, prefs=None): + self._gr_prefs = prefs if prefs else DummyPrefs() self.version = version self.version_parts = version_parts or version[1:].split('-', 1)[0].split('.')[:3] if name: @@ -46,8 +45,8 @@ class Config(object): paths_sources = ( self.hier_block_lib_dir, os.environ.get('GRC_BLOCKS_PATH', ''), - self.prefs.get_string('grc', 'local_blocks_path', ''), - self.prefs.get_string('grc', 'global_blocks_path', ''), + self._gr_prefs.get_string('grc', 'local_blocks_path', ''), + self._gr_prefs.get_string('grc', 'global_blocks_path', ''), ) collected_paths = sum((paths.split(path_list_sep) @@ -62,7 +61,22 @@ class Config(object): def default_flow_graph(self): user_default = ( os.environ.get('GRC_DEFAULT_FLOW_GRAPH') or - self.prefs.get_string('grc', 'default_flow_graph', '') or + self._gr_prefs.get_string('grc', 'default_flow_graph', '') or os.path.join(self.hier_block_lib_dir, 'default_flow_graph.grc') ) return user_default if exists(user_default) else Constants.DEFAULT_FLOW_GRAPH + + +class DummyPrefs(object): + + def get_string(self, category, item, default): + return str(default) + + def set_string(self, category, item, value): + pass + + def get_long(self, category, item, default): + return int(default) + + def save(self): + pass diff --git a/grc/core/Connection.py b/grc/core/Connection.py index c028d89ddc..01baaaf8fc 100644 --- a/grc/core/Connection.py +++ b/grc/core/Connection.py @@ -17,128 +17,95 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -from . import Constants -from .Element import Element -from .utils import odict +from __future__ import absolute_import + +from .base import Element +from .utils.descriptors import lazy_property class Connection(Element): is_connection = True - def __init__(self, flow_graph, 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: a new connection """ - Element.__init__(self, flow_graph) - source = sink = None - # Separate the source and sink - for port in (porta, portb): - if port.is_source: - source = port - else: - sink = port - if not source: + Element.__init__(self, parent) + + if not source.is_source: + source, sink = sink, source + if not source.is_source: raise ValueError('Connection could not isolate source') - if not sink: + if not sink.is_sink: raise ValueError('Connection could not isolate sink') - busses = len(filter(lambda a: a.get_type() == 'bus', [source, sink])) % 2 - if not busses == 0: - raise ValueError('busses must get with busses') - - if not len(source.get_associated_ports()) == len(sink.get_associated_ports()): - raise ValueError('port connections must have same cardinality') - # Ensure that this connection (source -> sink) is unique - for connection in flow_graph.connections: - if connection.get_source() is source and connection.get_sink() is sink: - raise LookupError('This connection between source and sink is not unique.') - self._source = source - self._sink = sink - if source.get_type() == 'bus': - - sources = source.get_associated_ports() - sinks = sink.get_associated_ports() - - for i in range(len(sources)): - try: - flow_graph.connect(sources[i], sinks[i]) - except: - pass + + self.source_port = source + self.sink_port = sink def __str__(self): return 'Connection (\n\t{}\n\t\t{}\n\t{}\n\t\t{}\n)'.format( - self.get_source().get_parent(), - self.get_source(), - self.get_sink().get_parent(), - self.get_sink(), + self.source_block, self.source_port, self.sink_block, self.sink_port, ) - def is_bus(self): - return self.get_source().get_type() == self.get_sink().get_type() == 'bus' + 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 - def validate(self): - """ - Validate the connections. - The ports must match in io size. - """ - """ - Validate the connections. - The ports must match in type. - """ - Element.validate(self) - platform = self.get_parent().get_parent() - source_domain = self.get_source().get_domain() - sink_domain = self.get_sink().get_domain() - 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.get_source().get_enabled_connections()) > 1 - ) - too_many_other_sources = ( - not platform.domains.get(sink_domain, []).get('multiple_sources', False) and - len(self.get_sink().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.get_source().get_type()] * self.get_source().get_vlen() - sink_size = Constants.TYPE_TO_SIZEOF[self.get_sink().get_type()] * self.get_sink().get_vlen() - if source_size != sink_size: - self.add_error_message('Source IO size "{}" does not match sink IO size "{}".'.format(source_size, sink_size)) + 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): + return self.source_port.parent_block + + @lazy_property + def sink_block(self): + return self.sink_port.parent_block - def get_enabled(self): + @lazy_property + def type(self): + return self.source_port.domain, self.sink_port.domain + + @property + def enabled(self): """ Get the enabled state of this connection. Returns: true if source and sink blocks are enabled """ - return self.get_source().get_parent().get_enabled() and \ - self.get_sink().get_parent().get_enabled() + return self.source_block.enabled and self.sink_block.enabled - ############################# - # Access Ports - ############################# - def get_sink(self): - return self._sink + def validate(self): + """ + Validate the connections. + The ports must match in io size. + """ + Element.validate(self) + platform = self.parent_platform - def get_source(self): - return self._source + if self.type not in platform.connection_templates: + self.add_error_message('No connection known between domains "{}" and "{}"' + ''.format(*self.type)) + + 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)) ############################################## # Import/Export Methods @@ -150,9 +117,7 @@ class Connection(Element): Returns: a nested data odict """ - n = odict() - n['source_block_id'] = self.get_source().get_parent().get_id() - n['sink_block_id'] = self.get_sink().get_parent().get_id() - n['source_key'] = self.get_source().get_key() - n['sink_key'] = self.get_sink().get_key() - return n + 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 425415468b..127e0e05f9 100644 --- a/grc/core/Constants.py +++ b/grc/core/Constants.py @@ -17,19 +17,24 @@ 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 os -import numpy +import numbers import stat +import numpy + + # 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') +CACHE_FILE = os.path.expanduser('~/.cache/grc_gnuradio/cache.json') + +BLOCK_DESCRIPTION_FILE_FORMAT_VERSION = 1 # File format versions: # 0: undefined / legacy # 1: non-numeric message port keys (label is used instead) @@ -41,35 +46,35 @@ 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' - -# Block States -BLOCK_DISABLED = 0 -BLOCK_ENABLED = 1 -BLOCK_BYPASSED = 2 - # 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 HIER_BLOCK_FILE_MODE = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH +PARAM_TYPE_NAMES = { + 'raw', 'enum', + 'complex', 'real', 'float', 'int', + 'complex_vector', 'real_vector', 'float_vector', 'int_vector', + 'hex', 'string', 'bool', + 'file_open', 'file_save', '_multiline', '_multiline_python_external', + 'id', 'stream_id', + 'gui_hint', + 'import', +} + +PARAM_TYPE_MAP = { + 'complex': numbers.Complex, + 'float': numbers.Real, + 'real': numbers.Real, + 'int': numbers.Integral, +} + # Define types, native python + numpy VECTOR_TYPES = (tuple, list, set, numpy.ndarray) -COMPLEX_TYPES = [complex, numpy.complex, numpy.complex64, numpy.complex128] -REAL_TYPES = [float, numpy.float, numpy.float32, numpy.float64] -INT_TYPES = [int, long, numpy.int, numpy.int8, numpy.int16, numpy.int32, numpy.uint64, - numpy.uint, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64] -# Cast to tuple for isinstance, concat subtypes -COMPLEX_TYPES = tuple(COMPLEX_TYPES + REAL_TYPES + INT_TYPES) -REAL_TYPES = tuple(REAL_TYPES + INT_TYPES) -INT_TYPES = tuple(INT_TYPES) # Updating colors. Using the standard color palette from: # http://www.google.com/design/spec/style/color.html#color-color-palette @@ -95,54 +100,32 @@ GRC_COLOR_GREY = '#BDBDBD' GRC_COLOR_WHITE = '#FFFFFF' CORE_TYPES = ( # name, key, sizeof, color - ('Complex Float 64', 'fc64', 16, GRC_COLOR_BROWN), - ('Complex Float 32', 'fc32', 8, GRC_COLOR_BLUE), - ('Complex Integer 64', 'sc64', 16, GRC_COLOR_LIGHT_GREEN), - ('Complex Integer 32', 'sc32', 8, GRC_COLOR_GREEN), - ('Complex Integer 16', 'sc16', 4, GRC_COLOR_AMBER), - ('Complex Integer 8', 'sc8', 2, GRC_COLOR_PURPLE), - ('Float 64', 'f64', 8, GRC_COLOR_CYAN), - ('Float 32', 'f32', 4, GRC_COLOR_ORANGE), - ('Integer 64', 's64', 8, GRC_COLOR_LIME), - ('Integer 32', 's32', 4, GRC_COLOR_TEAL), - ('Integer 16', 's16', 2, GRC_COLOR_YELLOW), - ('Integer 8', 's8', 1, GRC_COLOR_PURPLE_A400), - ('Bits (unpacked byte)', 'bit', 1, GRC_COLOR_PURPLE_A100), - ('Async Message', 'message', 0, GRC_COLOR_GREY), - ('Bus Connection', 'bus', 0, GRC_COLOR_WHITE), - ('Wildcard', '', 0, GRC_COLOR_WHITE), + ('Complex Float 64', 'fc64', 16, GRC_COLOR_BROWN), + ('Complex Float 32', 'fc32', 8, GRC_COLOR_BLUE), + ('Complex Integer 64', 'sc64', 16, GRC_COLOR_LIGHT_GREEN), + ('Complex Integer 32', 'sc32', 8, GRC_COLOR_GREEN), + ('Complex Integer 16', 'sc16', 4, GRC_COLOR_AMBER), + ('Complex Integer 8', 'sc8', 2, GRC_COLOR_PURPLE), + ('Float 64', 'f64', 8, GRC_COLOR_CYAN), + ('Float 32', 'f32', 4, GRC_COLOR_ORANGE), + ('Integer 64', 's64', 8, GRC_COLOR_LIME), + ('Integer 32', 's32', 4, GRC_COLOR_TEAL), + ('Integer 16', 's16', 2, GRC_COLOR_YELLOW), + ('Integer 8', 's8', 1, GRC_COLOR_PURPLE_A400), + ('Bits (unpacked byte)', 'bit', 1, GRC_COLOR_PURPLE_A100), + ('Async Message', 'message', 0, GRC_COLOR_GREY), + ('Bus Connection', 'bus', 0, GRC_COLOR_WHITE), + ('Wildcard', '', 0, GRC_COLOR_WHITE), ) ALIAS_TYPES = { 'complex': (8, GRC_COLOR_BLUE), - 'float': (4, GRC_COLOR_ORANGE), - 'int': (4, GRC_COLOR_TEAL), - 'short': (2, GRC_COLOR_YELLOW), - 'byte': (1, GRC_COLOR_PURPLE_A400), - 'bits': (1, GRC_COLOR_PURPLE_A100), + 'float': (4, GRC_COLOR_ORANGE), + 'int': (4, GRC_COLOR_TEAL), + 'short': (2, GRC_COLOR_YELLOW), + 'byte': (1, GRC_COLOR_PURPLE_A400), + 'bits': (1, GRC_COLOR_PURPLE_A100), } -TYPE_TO_COLOR = dict() -TYPE_TO_SIZEOF = dict() - -for name, key, sizeof, color in CORE_TYPES: - TYPE_TO_COLOR[key] = color - TYPE_TO_SIZEOF[key] = sizeof - -for key, (sizeof, color) in ALIAS_TYPES.iteritems(): - TYPE_TO_COLOR[key] = color - TYPE_TO_SIZEOF[key] = sizeof - -# Coloring -COMPLEX_COLOR_SPEC = '#3399FF' -FLOAT_COLOR_SPEC = '#FF8C69' -INT_COLOR_SPEC = '#00FF99' -SHORT_COLOR_SPEC = '#FFFF66' -BYTE_COLOR_SPEC = '#FF66FF' -COMPLEX_VECTOR_COLOR_SPEC = '#3399AA' -FLOAT_VECTOR_COLOR_SPEC = '#CC8C69' -INT_VECTOR_COLOR_SPEC = '#00CC99' -SHORT_VECTOR_COLOR_SPEC = '#CCCC33' -BYTE_VECTOR_COLOR_SPEC = '#CC66CC' -ID_COLOR_SPEC = '#DDDDDD' -WILDCARD_COLOR_SPEC = '#FFFFFF' +TYPE_TO_SIZEOF = {key: sizeof for name, key, sizeof, color in CORE_TYPES} +TYPE_TO_SIZEOF.update((key, sizeof) for key, (sizeof, _) in ALIAS_TYPES.items()) diff --git a/grc/core/Element.py b/grc/core/Element.py deleted file mode 100644 index 67c36e12b4..0000000000 --- a/grc/core/Element.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -Copyright 2008, 2009, 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 -""" - - -class Element(object): - - def __init__(self, parent=None): - self._parent = parent - self._error_messages = list() - - ################################################## - # Element Validation API - ################################################## - def validate(self): - """ - Validate this element and call validate on all children. - Call this base method before adding error messages in the subclass. - """ - del self._error_messages[:] - for child in self.get_children(): - child.validate() - - def is_valid(self): - """ - Is this element valid? - - Returns: - true when the element is enabled and has no error messages or is bypassed - """ - return (not self.get_error_messages() or not self.get_enabled()) or self.get_bypassed() - - def add_error_message(self, msg): - """ - Add an error message to the list of errors. - - Args: - msg: the error message string - """ - self._error_messages.append(msg) - - def get_error_messages(self): - """ - Get the list of error messages from this element and all of its children. - Do not include the error messages from disabled or bypassed children. - Cleverly indent the children error messages for printing purposes. - - Returns: - a list of error message strings - """ - error_messages = list(self._error_messages) # Make a copy - for child in filter(lambda c: c.get_enabled() and not c.get_bypassed(), self.get_children()): - for msg in child.get_error_messages(): - error_messages.append("{}:\n\t{}".format(child, msg.replace("\n", "\n\t"))) - return error_messages - - def rewrite(self): - """ - Rewrite this element and call rewrite on all children. - Call this base method before rewriting the element. - """ - for child in self.get_children(): - child.rewrite() - - def get_enabled(self): - return True - - def get_bypassed(self): - return False - - ############################################## - # Tree-like API - ############################################## - def get_parent(self): - return self._parent - - def get_children(self): - return list() - - ############################################## - # 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 diff --git a/grc/core/FlowGraph.py b/grc/core/FlowGraph.py index ecae11cf1a..8c59ec0bea 100644 --- a/grc/core/FlowGraph.py +++ b/grc/core/FlowGraph.py @@ -15,69 +15,57 @@ # 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 collections import imp -from itertools import ifilter, chain -from operator import methodcaller, attrgetter -import re +import itertools import sys -import time +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 odict, 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): is_flow_graph = True - def __init__(self, platform): + def __init__(self, parent): """ Make a flow graph from the arguments. Args: - platform: a platforms with blocks and contrcutors + parent: a platforms with blocks and element factories Returns: the flow graph object """ - Element.__init__(self, platform) - self._elements = [] - self._timestamp = time.ctime() + Element.__init__(self, parent) + self._options_block = self.parent_platform.make_block(self, 'options') - self.platform = platform # todo: make this a lazy prop - self.blocks = [] - self.connections = [] + self.blocks = [self._options_block] + self.connections = set() self._eval_cache = {} self.namespace = {} self.grc_file_path = '' - self._options_block = self.new_block('options') def __str__(self): return 'FlowGraph - {}({})'.format(self.get_option('title'), self.get_option('id')) - ############################################## - # TODO: Move these to new generator package - ############################################## - 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): """ @@ -87,8 +75,8 @@ class FlowGraph(Element): Returns: a sorted list of variable blocks in order of dependency (indep -> dep) """ - variables = filter(attrgetter('is_variable'), self.iter_enabled_blocks()) - return expr_utils.sort_objects(variables, methodcaller('get_id'), methodcaller('get_var_make')) + variables = [block for block in self.iter_enabled_blocks() if block.is_variable] + return expr_utils.sort_objects(variables, attrgetter('name'), methodcaller('get_var_make')) def get_parameters(self): """ @@ -97,54 +85,27 @@ class FlowGraph(Element): Returns: a list of parameterized variables """ - parameters = filter(lambda b: _parameter_matcher.match(b.get_key()), self.iter_enabled_blocks()) + 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 = filter(lambda b: _monitors_searcher.search(b.get_key()), - self.iter_enabled_blocks()) + 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.get_key() == 'epy_module': - yield block.get_id(), block.get_param('source_code').get_value() - - def get_bussink(self): - bussink = filter(lambda b: _bussink_searcher.search(b.get_key()), self.get_enabled_blocks()) - - for i in bussink: - for j in i.get_params(): - if j.get_name() == 'On/Off' and j.get_value() == 'on': - return True - return False - - def get_bussrc(self): - bussrc = filter(lambda b: _bussrc_searcher.search(b.get_key()), self.get_enabled_blocks()) - - for i in bussrc: - for j in i.get_params(): - if j.get_name() == 'On/Off' and j.get_value() == 'on': - return True - return False - - def get_bus_structure_sink(self): - bussink = filter(lambda b: _bus_struct_sink_searcher.search(b.get_key()), self.get_enabled_blocks()) - return bussink - - def get_bus_structure_src(self): - bussrc = filter(lambda b: _bus_struct_src_searcher.search(b.get_key()), self.get_enabled_blocks()) - return bussrc + if block.key == 'epy_module': + yield block.name, block.params[1].get_value() def iter_enabled_blocks(self): """ Get an iterator of all blocks that are enabled and not bypassed. """ - return ifilter(methodcaller('get_enabled'), self.blocks) + return (block for block in self.blocks if block.enabled) def get_enabled_blocks(self): """ @@ -162,7 +123,7 @@ class FlowGraph(Element): Returns: a list of blocks """ - return filter(methodcaller('get_bypassed'), self.blocks) + return [block for block in self.blocks if block.get_bypassed()] def get_enabled_connections(self): """ @@ -171,7 +132,7 @@ class FlowGraph(Element): Returns: a list of connections """ - return filter(methodcaller('get_enabled'), self.connections) + return [connection for connection in self.connections if connection.enabled] def get_option(self, key): """ @@ -184,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') @@ -199,73 +160,59 @@ 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): - """ - Get a list of all the elements. - Always ensure that the options block is in the list (only once). + elements = list(self.blocks) + elements.extend(self.connections) + return elements - Returns: - the element list - """ - options_block_count = self.blocks.count(self._options_block) - if not options_block_count: - self.blocks.append(self._options_block) - for i in range(options_block_count-1): - self.blocks.remove(self._options_block) - - return self.blocks + self.connections - - get_children = get_elements + def children(self): + return itertools.chain(self.iter_enabled_blocks(), self.connections) def rewrite(self): """ Flag the namespace to be renewed. """ - self.renew_namespace() - for child in chain(self.blocks, self.connections): - child.rewrite() - - self.bus_ports_rewrite() + Element.rewrite(self) def renew_namespace(self): namespace = {} # Load imports - for expr in self.get_imports(): + for expr in self.imports(): try: - exec expr in namespace + exec(expr, namespace) except: pass for id, expr in self.get_python_modules(): try: module = imp.new_module(id) - exec expr in module.__dict__ + exec(expr, module.__dict__) namespace[id] = module except: pass # 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 @@ -273,39 +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): + 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 block_id == 'options': + return self._options_block try: - block = self.platform.get_new_block(self, key) + block = self.parent_platform.make_block(self, block_id, **kwargs) self.blocks.append(block) except KeyError: block = None @@ -323,12 +268,17 @@ class FlowGraph(Element): Returns: the new connection """ - - connection = self.platform.Connection( - flow_graph=self, porta=porta, portb=portb) - self.connections.append(connection) + connection = self.parent_platform.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. @@ -336,22 +286,18 @@ class FlowGraph(Element): If the element is a block, remove its connections. If the element is a connection, just remove the connection. """ + if element is self._options_block: + return + if element.is_port: - # Found a port, set to parent signal block - element = element.get_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(): - map(self.remove_element, port.get_connections()) + self.disconnect(*element.ports()) self.blocks.remove(element) elif element in self.connections: - if element.is_bus(): - cons_list = [] - for i in map(lambda a: a.get_connections(), element.get_source().get_associated_ports()): - cons_list.extend(i) - map(self.remove_element, cons_list) self.connections.remove(element) ############################################## @@ -365,173 +311,107 @@ class FlowGraph(Element): Returns: a nested data odict """ - # sort blocks and connections for nicer diffs - blocks = sorted(self.blocks, key=lambda b: ( - b.get_key() != 'options', # options to the front - not b.get_key().startswith('variable'), # then vars - str(b) - )) - connections = sorted(self.connections, key=str) - n = odict() - n['timestamp'] = self._timestamp - n['block'] = [b.export_data() for b in blocks] - n['connection'] = [c.export_data() for c in connections] - instructions = odict({ - 'created': '.'.join(self.get_parent().config.version_parts), - 'format': FLOW_GRAPH_FILE_FORMAT_VERSION, - }) - return odict({'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 flowgraph of all previous blocks and connections. + 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.find('_instructions') or {} - 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.find('flow_graph') or odict() # use blank data if none provided - self._timestamp = fg_n.find('timestamp') or time.ctime() + file_format = data['metadata']['file_format'] # build the blocks - self._options_block = self.new_block('options') - for block_n in fg_n.findall('block'): - key = block_n.find('key') - block = self._options_block if key == 'options' else 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.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.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_block') - # Ugly ugly ugly - _initialize_dummy_block(block, block_n) - print('Block key "%s" not found' % key) - - block.import_data(block_n) + self._options_block.import_data(name='', **data.get('options', {})) + self.blocks.append(self._options_block) + + 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 # build the connections def verify_and_get_port(key, block, dir): - ports = block.get_sinks() if dir == 'sink' else block.get_sources() + ports = block.sinks if dir == 'sink' else block.sources for port in ports: - if key == port.get_key(): + if key == port.key or key + '0' == port.key: break - if not key.isdigit() and port.get_type() == '' and key == port.get_name(): + if not key.isdigit() and port.dtype == '' and key == port.name: break else: if block.is_dummy_block: - port = _dummy_block_add_port(block, key, dir) + port = block.add_missing_port(key, dir) else: raise LookupError('%s key %r not in %s block keys' % (dir, key, dir)) return port - errors = False - for connection_n in fg_n.findall('connection'): - # get the block ids and port keys - source_block_id = connection_n.find('source_block_id') - sink_block_id = connection_n.find('sink_block_id') - source_key = connection_n.find('source_key') - sink_key = connection_n.find('sink_key') - try: - source_block = self.get_block(source_block_id) - sink_block = self.get_block(sink_block_id) + had_connect_errors = False + _blocks = {block.name: block for block in self.blocks} + + try: + # TODO: Add better error handling if no connections exist in the flowgraph file. + for src_blk_id, src_port_id, snk_blk_id, snk_port_id in data.get('connections', []): + 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: - 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 - self.rewrite() # global rewrite - return errors + except (KeyError, LookupError) as e: + Messages.send_error_load( + 'Connection between {}({}) and {}({}) could not be made.\n\t{}'.format( + src_blk_id, src_port_id, snk_blk_id, snk_port_id, e)) + had_connect_errors = True - ############################################## - # Needs to go - ############################################## - def bus_ports_rewrite(self): - # todo: move to block.rewrite() - for block in self.blocks: - for direc in ['source', 'sink']: - if direc == 'source': - get_p = block.get_sources - get_p_gui = block.get_sources_gui - bus_structure = block.form_bus_structure('source') - else: - get_p = block.get_sinks - get_p_gui = block.get_sinks_gui - bus_structure = block.form_bus_structure('sink') - - if 'bus' in map(lambda a: a.get_type(), get_p_gui()): - if len(get_p_gui()) > len(bus_structure): - times = range(len(bus_structure), len(get_p_gui())) - for i in times: - for connect in get_p_gui()[-1].get_connections(): - block.get_parent().remove_element(connect) - get_p().remove(get_p_gui()[-1]) - elif len(get_p_gui()) < len(bus_structure): - n = {'name': 'bus', 'type': 'bus'} - if True in map( - lambda a: isinstance(a.get_nports(), int), - get_p()): - n['nports'] = str(1) - - times = range(len(get_p_gui()), len(bus_structure)) - - for i in times: - n['key'] = str(len(get_p())) - n = odict(n) - port = block.get_parent().get_parent().Port( - block=block, n=n, dir=direc) - get_p().append(port) - - if 'bus' in map(lambda a: a.get_type(), - block.get_sources_gui()): - for i in range(len(block.get_sources_gui())): - if len(block.get_sources_gui()[ - i].get_connections()) > 0: - source = block.get_sources_gui()[i] - sink = [] - - for j in range(len(source.get_connections())): - sink.append( - source.get_connections()[j].get_sink()) - for elt in source.get_connections(): - self.remove_element(elt) - for j in sink: - self.connect(source, j) + self.rewrite() # global rewrite + return had_connect_errors def _update_old_message_port_keys(source_key, sink_key, source_block, sink_block): @@ -548,55 +428,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) - source_port = source_block.get_sources()[int(source_key)] - sink_port = sink_block.get_sinks()[int(sink_key)] - if source_port.get_type() == "message" and sink_port.get_type() == "message": - source_key, sink_key = source_port.get_key(), sink_port.get_key() + # 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.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.find('source_key').isdigit() and - connection_n.find('sink_key').isdigit() - ) for connection_n in n.find('flow_graph').findall('connection')) - if has_non_numeric_message_keys: - return 1 - except: - pass - return 0 - - -def _initialize_dummy_block(block, block_n): - """ - This is so ugly... dummy-fy a block - Modify block object to get the behaviour for a missing block - """ - - block._key = block_n.find('key') - block.is_dummy_block = lambda: True - block.is_valid = lambda: False - block.get_enabled = lambda: False - for param_n in block_n.findall('param'): - if param_n['key'] not in block.get_param_keys(): - new_param_n = odict({'key': param_n['key'], 'name': param_n['key'], 'type': 'string'}) - params = block.get_parent().get_parent().Param(block=block, n=new_param_n) - block.get_params().append(params) - - -def _dummy_block_add_port(block, key, dir): - """ This is so ugly... Add a port to a dummy-field block """ - port_n = odict({'name': '?', 'key': key, 'type': ''}) - port = block.get_parent().get_parent().Port(block=block, n=port_n, dir=dir) - if port.is_source: - block.get_sources().append(port) - else: - block.get_sinks().append(port) - return port diff --git a/grc/core/Messages.py b/grc/core/Messages.py index 8daa12c33f..f546c3b62e 100644 --- a/grc/core/Messages.py +++ b/grc/core/Messages.py @@ -16,9 +16,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +from __future__ import absolute_import + import traceback import sys -import os # A list of functions that can receive a message. MESSENGERS_LIST = list() @@ -124,8 +125,8 @@ def send_fail_save(file_path): send('>>> Error: Cannot save: %s\n' % file_path) -def send_fail_connection(): - send('>>> Error: Cannot create connection.\n') +def send_fail_connection(msg=''): + send('>>> Error: Cannot create connection.\n' + ('\t{}\n'.format(msg) if msg else '')) def send_fail_load_preferences(prefs_file_path): diff --git a/grc/core/Param.py b/grc/core/Param.py deleted file mode 100644 index 2077925879..0000000000 --- a/grc/core/Param.py +++ /dev/null @@ -1,691 +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 -""" - -import ast -import weakref -import re - -from . import Constants -from .Constants import VECTOR_TYPES, COMPLEX_TYPES, REAL_TYPES, INT_TYPES -from .Element import Element -from .utils import odict - -# Blacklist certain ids, its not complete, but should help -import __builtin__ - - -ID_BLACKLIST = ['self', 'options', 'gr', 'math', 'firdes'] + dir(__builtin__) -try: - from gnuradio import gr - ID_BLACKLIST.extend(attr for attr in dir(gr.top_block()) if not attr.startswith('_')) -except ImportError: - pass - -_check_id_matcher = re.compile('^[a-z|A-Z]\w*$') -_show_id_matcher = re.compile('^(variable\w*|parameter|options|notebook)$') - - -def _get_keys(lst): - return [elem.get_key() for elem in lst] - - -def _get_elem(lst, key): - try: - return lst[_get_keys(lst).index(key)] - except ValueError: - raise ValueError('Key "{}" not found in {}.'.format(key, _get_keys(lst))) - - -def num_to_str(num): - """ Display logic for numbers """ - def eng_notation(value, fmt='g'): - """Convert a number to a string in engineering notation. E.g., 5e-9 -> 5n""" - template = '{:' + fmt + '}{}' - magnitude = abs(value) - for exp, symbol in zip(range(9, -15-1, -3), 'GMk munpf'): - factor = 10 ** exp - if magnitude >= factor: - return template.format(value / factor, symbol.strip()) - return template.format(value, '') - - if isinstance(num, COMPLEX_TYPES): - num = complex(num) # Cast to python complex - if num == 0: - return '0' - output = eng_notation(num.real) if num.real else '' - output += eng_notation(num.imag, '+g' if output else 'g') + 'j' if num.imag else '' - return output - else: - return str(num) - - -class Option(Element): - - def __init__(self, param, n): - Element.__init__(self, param) - self._name = n.find('name') - self._key = n.find('key') - self._opts = dict() - opts = n.findall('opt') - # Test against opts when non enum - if not self.get_parent().is_enum() and opts: - raise Exception('Options for non-enum types cannot have sub-options') - # Extract opts - for opt in opts: - # Separate the key:value - try: - key, value = opt.split(':') - except: - raise Exception('Error separating "{}" into key:value'.format(opt)) - # Test against repeated keys - if key in self._opts: - raise Exception('Key "{}" already exists in option'.format(key)) - # Store the option - self._opts[key] = value - - def __str__(self): - return 'Option {}({})'.format(self.get_name(), self.get_key()) - - def get_name(self): - return self._name - - def get_key(self): - return self._key - - ############################################## - # Access Opts - ############################################## - def get_opt_keys(self): - return self._opts.keys() - - def get_opt(self, key): - return self._opts[key] - - def get_opts(self): - return self._opts.values() - - -class TemplateArg(object): - """ - A cheetah template argument created from a param. - The str of this class evaluates to the param's to code method. - The use of this class as a dictionary (enum only) will reveal the enum opts. - The __call__ or () method can return the param evaluated to a raw python data type. - """ - - def __init__(self, param): - self._param = weakref.proxy(param) - - def __getitem__(self, item): - return str(self._param.get_opt(item)) if self._param.is_enum() else NotImplemented - - def __getattr__(self, item): - if not self._param.is_enum(): - raise AttributeError() - try: - return str(self._param.get_opt(item)) - except KeyError: - raise AttributeError() - - def __str__(self): - return str(self._param.to_code()) - - def __call__(self): - return self._param.get_evaluated() - - -class Param(Element): - - is_param = True - - def __init__(self, block, n): - """ - Make a new param from nested data. - - Args: - block: the parent element - n: the nested odict - """ - # If the base key is a valid param key, copy its data and overlay this params data - base_key = n.find('base_key') - if base_key and base_key in block.get_param_keys(): - n_expanded = block.get_param(base_key)._n.copy() - n_expanded.update(n) - n = n_expanded - # Save odict in case this param will be base for another - self._n = n - # Parse the data - self._name = n.find('name') - self._key = n.find('key') - value = n.find('value') or '' - self._type = n.find('type') or 'raw' - self._hide = n.find('hide') or '' - self._tab_label = n.find('tab') or block.get_param_tab_labels()[0] - if self._tab_label not in block.get_param_tab_labels(): - block.get_param_tab_labels().append(self._tab_label) - # Build the param - Element.__init__(self, block) - # Create the Option objects from the n data - self._options = list() - self._evaluated = None - for option in map(lambda o: Option(param=self, n=o), n.findall('option')): - key = option.get_key() - # Test against repeated keys - if key in self.get_option_keys(): - raise Exception('Key "{}" already exists in options'.format(key)) - # Store the option - self.get_options().append(option) - # Test the enum options - if self.is_enum(): - # Test against options with identical keys - if len(set(self.get_option_keys())) != len(self.get_options()): - raise Exception('Options keys "{}" are not unique.'.format(self.get_option_keys())) - # Test against inconsistent keys in options - opt_keys = self.get_options()[0].get_opt_keys() - for option in self.get_options(): - if set(opt_keys) != set(option.get_opt_keys()): - raise Exception('Opt keys "{}" are not identical across all options.'.format(opt_keys)) - # If a value is specified, it must be in the options keys - if value or value in self.get_option_keys(): - self._value = value - else: - self._value = self.get_option_keys()[0] - if self.get_value() not in self.get_option_keys(): - raise Exception('The value "{}" is not in the possible values of "{}".'.format(self.get_value(), self.get_option_keys())) - else: - self._value = value or '' - self._default = value - self._init = False - self._hostage_cells = list() - self.template_arg = TemplateArg(self) - - def get_types(self): - return ( - 'raw', 'enum', - 'complex', 'real', 'float', 'int', - 'complex_vector', 'real_vector', 'float_vector', 'int_vector', - 'hex', 'string', 'bool', - 'file_open', 'file_save', '_multiline', '_multiline_python_external', - 'id', 'stream_id', - 'gui_hint', - 'import', - ) - - def __repr__(self): - """ - Get the repr (nice string format) for this param. - - Returns: - the string representation - """ - ################################################## - # Truncate helper method - ################################################## - def _truncate(string, style=0): - max_len = max(27 - len(self.get_name()), 3) - if len(string) > max_len: - if style < 0: # Front truncate - string = '...' + string[3-max_len:] - elif style == 0: # Center truncate - string = string[:max_len/2 - 3] + '...' + string[-max_len/2:] - elif style > 0: # Rear truncate - string = string[:max_len-3] + '...' - return string - - ################################################## - # Simple conditions - ################################################## - if not self.is_valid(): - return _truncate(self.get_value()) - if self.get_value() in self.get_option_keys(): - return self.get_option(self.get_value()).get_name() - - ################################################## - # Split up formatting by type - ################################################## - # Default center truncate - truncate = 0 - e = self.get_evaluated() - t = self.get_type() - if isinstance(e, bool): - return str(e) - elif isinstance(e, COMPLEX_TYPES): - dt_str = num_to_str(e) - elif isinstance(e, VECTOR_TYPES): - # Vector types - if len(e) > 8: - # Large vectors use code - dt_str = self.get_value() - truncate = 1 - else: - # Small vectors use eval - dt_str = ', '.join(map(num_to_str, e)) - elif t in ('file_open', 'file_save'): - dt_str = self.get_value() - truncate = -1 - else: - # Other types - dt_str = str(e) - - # Done - return _truncate(dt_str, truncate) - - def __repr2__(self): - """ - Get the repr (nice string format) for this param. - - Returns: - the string representation - """ - if self.is_enum(): - return self.get_option(self.get_value()).get_name() - return self.get_value() - - def __str__(self): - return 'Param - {}({})'.format(self.get_name(), self.get_key()) - - def get_color(self): - """ - Get the color that represents this param's type. - - Returns: - a hex color code. - """ - try: - return { - # Number types - 'complex': Constants.COMPLEX_COLOR_SPEC, - 'real': Constants.FLOAT_COLOR_SPEC, - 'float': Constants.FLOAT_COLOR_SPEC, - 'int': Constants.INT_COLOR_SPEC, - # Vector types - 'complex_vector': Constants.COMPLEX_VECTOR_COLOR_SPEC, - 'real_vector': Constants.FLOAT_VECTOR_COLOR_SPEC, - 'float_vector': Constants.FLOAT_VECTOR_COLOR_SPEC, - 'int_vector': Constants.INT_VECTOR_COLOR_SPEC, - # Special - 'bool': Constants.INT_COLOR_SPEC, - 'hex': Constants.INT_COLOR_SPEC, - 'string': Constants.BYTE_VECTOR_COLOR_SPEC, - 'id': Constants.ID_COLOR_SPEC, - 'stream_id': Constants.ID_COLOR_SPEC, - 'raw': Constants.WILDCARD_COLOR_SPEC, - }[self.get_type()] - except: - return '#FFFFFF' - - 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. - - Returns: - hide the hide property string - """ - hide = self.get_parent().resolve_dependencies(self._hide).strip() - if hide: - return hide - # Hide ID in non variable blocks - if self.get_key() == 'id' and not _show_id_matcher.match(self.get_parent().get_key()): - return 'part' - # Hide port controllers for type and nports - if self.get_key() in ' '.join(map(lambda p: ' '.join([p._type, p._nports]), - self.get_parent().get_ports())): - return 'part' - # Hide port controllers for vlen, when == 1 - if self.get_key() in ' '.join(map( - lambda p: p._vlen, self.get_parent().get_ports()) - ): - try: - if int(self.get_evaluated()) == 1: - return 'part' - except: - pass - return hide - - 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 self.get_types(): - self.add_error_message('Type "{}" is not a possible type.'.format(self.get_type())) - - self._evaluated = None - try: - self._evaluated = self.evaluate() - except Exception, e: - self.add_error_message(str(e)) - - def get_evaluated(self): - return self._evaluated - - def evaluate(self): - """ - Evaluate the value. - - Returns: - evaluated type - """ - self._init = True - self._lisitify_flag = False - self._stringify_flag = False - self._hostage_cells = list() - t = self.get_type() - v = self.get_value() - - ######################### - # Enum Type - ######################### - if self.is_enum(): - return v - - ######################### - # Numeric Types - ######################### - elif t in ('raw', 'complex', 'real', 'float', 'int', 'hex', 'bool'): - # Raise exception if python cannot evaluate this value - try: - e = self.get_parent().get_parent().evaluate(v) - except Exception, e: - raise Exception('Value "{}" cannot be evaluated:\n{}'.format(v, e)) - # Raise an exception if the data is invalid - if t == 'raw': - return e - elif t == 'complex': - if not isinstance(e, 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, REAL_TYPES): - raise Exception('Expression "{}" is invalid for type float.'.format(str(e))) - return e - elif t == 'int': - if not isinstance(e, 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 - else: - raise TypeError('Type "{}" not handled'.format(t)) - ######################### - # 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 - try: - e = self.get_parent().get_parent().evaluate(v) - except Exception, e: - raise Exception('Value "{}" cannot be evaluated:\n{}'.format(v, e)) - # Raise an exception if the data is invalid - if t == 'complex_vector': - if not isinstance(e, VECTOR_TYPES): - self._lisitify_flag = True - e = [e] - if not all([isinstance(ei, 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, VECTOR_TYPES): - self._lisitify_flag = True - e = [e] - if not all([isinstance(ei, 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, VECTOR_TYPES): - self._lisitify_flag = True - e = [e] - if not all([isinstance(ei, INT_TYPES) for ei in e]): - raise Exception('Expression "{}" is invalid for type integer vector.'.format(str(e))) - return e - ######################### - # String Types - ######################### - elif t 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.get_parent().get_parent().evaluate(v) - if not isinstance(e, str): - raise Exception() - except: - self._stringify_flag = True - e = str(v) - if t == '_multiline_python_external': - ast.parse(e) # Raises SyntaxError - return e - ######################### - # 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 - - ######################### - # 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.get_parent().is_virtual_sink(), - self.get_all_params(t), - )] - # Check that the virtual sink's stream id is unique - if self.get_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.get_parent().is_virtual_source(): - if v not in ids: - raise Exception('Stream ID "{}" is not found.'.format(v)) - return v - - ######################### - # GUI Position/Hint - ######################### - elif t == 'gui_hint': - if ':' in v: - tab, pos = v.split(':') - elif '@' in v: - tab, pos = v, '' - else: - tab, pos = '', v - - if '@' in tab: - tab, index = tab.split('@') - else: - index = '?' - - # TODO: Problem with this code. Produces bad tabs - widget_str = ({ - (True, True): 'self.%(tab)s_grid_layout_%(index)s.addWidget(%(widget)s, %(pos)s)', - (True, False): 'self.%(tab)s_layout_%(index)s.addWidget(%(widget)s)', - (False, True): 'self.top_grid_layout.addWidget(%(widget)s, %(pos)s)', - (False, False): 'self.top_layout.addWidget(%(widget)s)', - }[bool(tab), bool(pos)]) % {'tab': tab, 'index': index, 'widget': '%s', 'pos': pos} - - # FIXME: Move replace(...) into the make template of the qtgui blocks - # Return a string here - class GuiHint(object): - def __init__(self, ws): - self._ws = ws - - def __call__(self, w): - return (self._ws.replace('addWidget', 'addLayout') if 'layout' in w else self._ws) % w - - def __str__(self): - return self._ws - return GuiHint(widget_str) - ######################### - # Import Type - ######################### - elif t == 'import': - # New namespace - n = dict() - try: - exec v in n - except ImportError: - raise Exception('Import "{}" failed.'.format(v)) - except Exception: - raise Exception('Bad import syntax: "{}".'.format(v)) - return filter(lambda k: str(k) != '__builtins__', n.keys()) - - ######################### - else: - raise TypeError('Type "{}" not handled'.format(t)) - - def to_code(self): - """ - Convert the value to code. - For string and list types, check the init flag, call evaluate(). - This ensures that evaluate() was called to set the xxxify_flags. - - Returns: - a string representing the code - """ - v = self.get_value() - t = self.get_type() - # String types - if t in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'): - if not self._init: - self.evaluate() - return repr(v) if self._stringify_flag else v - - # Vector types - elif t in ('complex_vector', 'real_vector', 'float_vector', 'int_vector'): - if not self._init: - self.evaluate() - if self._lisitify_flag: - return '(%s, )' % v - else: - 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 - """ - return sum([filter(lambda p: ((p.get_type() == type) and ((key is None) or (p.get_key() == key))), block.get_params()) for block in self.get_parent().get_parent().get_enabled_blocks()], []) - - def is_enum(self): - return self._type == 'enum' - - def get_value(self): - value = self._value - if self.is_enum() and value not in self.get_option_keys(): - value = self.get_option_keys()[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.get_parent().resolve_dependencies(self._type) - - def get_tab_label(self): - return self._tab_label - - def get_name(self): - return self.get_parent().resolve_dependencies(self._name).strip() - - def get_key(self): - return self._key - - ############################################## - # Access Options - ############################################## - def get_option_keys(self): - return _get_keys(self.get_options()) - - def get_option(self, key): - return _get_elem(self.get_options(), key) - - def get_options(self): - return self._options - - ############################################## - # Access Opts - ############################################## - def get_opt_keys(self): - return self.get_option(self.get_value()).get_opt_keys() - - def get_opt(self, key): - return self.get_option(self.get_value()).get_opt(key) - - def get_opts(self): - return self.get_option(self.get_value()).get_opts() - - ############################################## - # Import/Export Methods - ############################################## - def export_data(self): - """ - Export this param's key/value. - - Returns: - a nested data odict - """ - n = odict() - n['key'] = self.get_key() - n['value'] = self.get_value() - return n diff --git a/grc/core/ParseXML.py b/grc/core/ParseXML.py index c9f6541ee7..430ba5b474 100644 --- a/grc/core/ParseXML.py +++ b/grc/core/ParseXML.py @@ -17,9 +17,13 @@ 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 lxml import etree -from .utils import odict +import six +from six.moves import map + xml_failures = {} etree.set_default_parser(etree.XMLParser(remove_comments=True)) @@ -75,17 +79,35 @@ def from_file(xml_file): the nested data with grc version information """ xml = etree.parse(xml_file) - nested_data = _from_file(xml.getroot()) + + tag, nested_data = _from_file(xml.getroot()) + nested_data = {tag: nested_data, '_instructions': {}} # Get the embedded instructions and build a dictionary item - nested_data['_instructions'] = {} xml_instructions = xml.xpath('/processing-instruction()') - for inst in filter(lambda i: i.target == 'grc', xml_instructions): - nested_data['_instructions'] = odict(inst.attrib) + for inst in xml_instructions: + if inst.target != 'grc': + continue + nested_data['_instructions'] = dict(inst.attrib) return nested_data -def _from_file(xml): +WANT_A_LIST = { + '/block': 'import callback param check sink source'.split(), + '/block/param_tab_order': 'tab'.split(), + '/block/param': 'option'.split(), + '/block/param/option': 'opt'.split(), + '/flow_graph': 'block connection'.split(), + '/flow_graph/block': 'param'.split(), + '/cat': 'cat block'.split(), + '/cat/cat': 'cat block'.split(), + '/cat/cat/cat': 'cat block'.split(), + '/cat/cat/cat/cat': 'cat block'.split(), + '/domain': 'connection'.split(), +} + + +def _from_file(xml, parent_tag=''): """ Recursively parse the xml tree into nested data format. @@ -96,21 +118,24 @@ def _from_file(xml): the nested data """ tag = xml.tag + tag_path = parent_tag + '/' + tag + if not len(xml): - return odict({tag: xml.text or ''}) # store empty tags (text is None) as empty string - nested_data = odict() + return tag, xml.text or '' # store empty tags (text is None) as empty string + + nested_data = {} for elem in xml: - key, value = _from_file(elem).items()[0] - if key in nested_data: - nested_data[key].append(value) + key, value = _from_file(elem, tag_path) + + if key in WANT_A_LIST.get(tag_path, []): + try: + nested_data[key].append(value) + except KeyError: + nested_data[key] = [value] else: - nested_data[key] = [value] - # Delistify if the length of values is 1 - for key, values in nested_data.iteritems(): - if len(values) == 1: - nested_data[key] = values[0] + nested_data[key] = value - return odict({tag: nested_data}) + return tag, nested_data def to_file(nested_data, xml_file): @@ -127,11 +152,11 @@ def to_file(nested_data, xml_file): if instructions: xml_data += etree.tostring(etree.ProcessingInstruction( 'grc', ' '.join( - "{0}='{1}'".format(*item) for item in instructions.iteritems()) + "{0}='{1}'".format(*item) for item in six.iteritems(instructions)) ), xml_declaration=True, pretty_print=True, encoding='utf-8') xml_data += etree.tostring(_to_file(nested_data)[0], pretty_print=True, encoding='utf-8') - with open(xml_file, 'w') as fp: + with open(xml_file, 'wb') as fp: fp.write(xml_data) @@ -146,14 +171,14 @@ def _to_file(nested_data): the xml tree filled with child nodes """ nodes = list() - for key, values in nested_data.iteritems(): + for key, values in six.iteritems(nested_data): # Listify the values if not a list if not isinstance(values, (list, set, tuple)): values = [values] for value in values: node = etree.Element(key) - if isinstance(value, (str, unicode)): - node.text = unicode(value) + if isinstance(value, (str, six.text_type)): + node.text = six.text_type(value) else: node.extend(_to_file(value)) nodes.append(node) diff --git a/grc/core/Platform.py b/grc/core/Platform.py deleted file mode 100644 index 20702b900f..0000000000 --- a/grc/core/Platform.py +++ /dev/null @@ -1,309 +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 -""" - -import os -import sys - -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 .Block import Block -from .Port import Port -from .Param import Param - -from .utils import odict, extract_docs, hide_bokeh_gui_options_if_not_installed - - -class Platform(Element): - - Config = Config - Generator = Generator - FlowGraph = FlowGraph - Connection = Connection - Block = Block - Port = Port - Param = Param - - is_platform = True - - def __init__(self, *args, **kwargs): - """ Make a platform for GNU Radio """ - Element.__init__(self) - - 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() - ) - - # Create a dummy flow graph for the blocks - self._flow_graph = Element(self) - self._flow_graph.connections = [] - - self.blocks = odict() - self._blocks_n = odict() - self._block_categories = {} - self.domains = {} - self.connection_templates = {} - - self._auto_hier_block_generate_chain = set() - - self.build_block_library() - - def __str__(self): - return 'Platform - {}({})'.format(self.config.key, 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.get_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: - print >> sys.stderr, 'Warning: XML parsing failed:\n\t%r\n\tIgnoring: %s' % (e, xml_file) - - # Add blocks to block tree - for key, block in self.blocks.iteritems(): - 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() - - hide_bokeh_gui_options_if_not_installed(self.blocks['options']) - - 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(filter(lambda f: f.endswith('.xml'), filenames)): - 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).find('block') - n['block_wrapper_path'] = xml_file # inject block wrapper path - # Get block instance and add it to the list of blocks - block = self.Block(self._flow_graph, n) - key = block.get_key() - if key in self.blocks: - print >> sys.stderr, 'Warning: Block with key "{}" already exists.\n\tIgnoring: {}'.format(key, xml_file) - else: # Store the block - self.blocks[key] = block - self._blocks_n[key] = n - - self._docstring_extractor.query( - block.get_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.find('name').strip()) - for block_key in cat_n.findall('block'): - if block_key not in self._block_categories: - self._block_categories[block_key] = list(path) - for sub_cat_n in cat_n.findall('cat'): - load_category(sub_cat_n) - path.pop() - - load_category(xml.find('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).find('domain') - - key = n.find('key') - if not key: - print >> sys.stderr, 'Warning: Domain with empty key.\n\tIgnoring: {0}'.format(xml_file) - return - if key in self.domains: # test against repeated keys - print >> sys.stderr, 'Warning: Domain with key "{}" already exists.\n\tIgnoring: {}'.format(key, xml_file) - 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.find('color') or '' - try: - import gtk # ugly but handy - gtk.gdk.color_parse(color) - except (ValueError, ImportError): - if color: # no color is okay, default set in GUI - print >> sys.stderr, 'Warning: Can\'t parse color code "{}" for domain "{}" '.format(color, key) - color = None - - self.domains[key] = dict( - name=n.find('name') or key, - multiple_sinks=to_bool(n.find('multiple_sinks'), True), - multiple_sources=to_bool(n.find('multiple_sources'), False), - color=color - ) - for connection_n in n.findall('connection'): - key = (connection_n.find('source_domain'), connection_n.find('sink_domain')) - if not all(key): - print >> sys.stderr, 'Warning: Empty domain key(s) in connection template.\n\t{}'.format(xml_file) - elif key in self.connection_templates: - print >> sys.stderr, 'Warning: Connection template "{}" already exists.\n\t{}'.format(key, xml_file) - else: - self.connection_templates[key] = connection_n.find('make') or '' - - def _save_docstring_extraction_result(self, key, docstrings): - docs = {} - for match, docstring in docstrings.iteritems(): - 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_new_flow_graph(self): - return self.FlowGraph(platform=self) - - def get_blocks(self): - return self.blocks.values() - - def get_new_block(self, flow_graph, key): - return self.Block(flow_graph, n=self._blocks_n[key]) - - def get_colors(self): - return [(name, color) for name, key, sizeof, color in Constants.CORE_TYPES] diff --git a/grc/core/Port.py b/grc/core/Port.py deleted file mode 100644 index 8549656c9b..0000000000 --- a/grc/core/Port.py +++ /dev/null @@ -1,414 +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 itertools import chain - -from .Constants import DEFAULT_DOMAIN, GR_STREAM_DOMAIN, GR_MESSAGE_DOMAIN -from .Element import Element - -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.get_source(), _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.get_parent() - flow_graph = block.get_parent() - - 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.get_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.get_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.get_sink(), _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.get_parent() - flow_graph = block.get_parent() - - 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.get_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.get_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 - - def __init__(self, block, n, dir): - """ - 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'] = GR_MESSAGE_DOMAIN - if 'domain' not in n: - n['domain'] = DEFAULT_DOMAIN - elif n['domain'] == GR_MESSAGE_DOMAIN: - n['key'] = n['name'] - n['type'] = 'message' # For port color - if not n.find('key'): - n['key'] = str(next(block.port_counters[dir == 'source'])) - - # Build the port - Element.__init__(self, block) - # Grab the data - self._name = n['name'] - self._key = n['key'] - self._type = n['type'] or '' - self._domain = n['domain'] - self._hide = n.find('hide') or '' - self._dir = dir - self._hide_evaluated = False # Updated on rewrite() - - self._nports = n.find('nports') or '' - self._vlen = n.find('vlen') or '' - self._optional = n.find('optional') or '' - 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.get_name(), self.get_key()) - if self.is_sink: - return 'Sink - {}({})'.format(self.get_name(), self.get_key()) - - def get_types(self): - return Constants.TYPE_TO_SIZEOF.keys() - - def is_type_empty(self): - return not self._n['type'] or not self.get_parent().resolve_dependencies(self._n['type']) - - def validate(self): - if self.get_type() not in self.get_types(): - self.add_error_message('Type "{}" is not a possible type.'.format(self.get_type())) - platform = self.get_parent().get_parent().get_parent() - if self.get_domain() not in platform.domains: - self.add_error_message('Domain key "{}" is not registered.'.format(self.get_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.is_type_empty(): - self.resolve_empty_type() - - hide = self.get_parent().resolve_dependencies(self._hide).strip().lower() - self._hide_evaluated = False if hide in ('false', 'off', '0') else bool(hide) - optional = self.get_parent().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 == GR_STREAM_DOMAIN and type_ == "message": - self._domain = GR_MESSAGE_DOMAIN - self._key = self._name - if self._domain == GR_MESSAGE_DOMAIN and type_ != "message": - self._domain = 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.is_type_empty()), 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.get_parent().resolve_dependencies(self._vlen) - try: - return int(self.get_parent().get_parent().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 '' - - nports = self.get_parent().resolve_dependencies(self._nports) - try: - return max(1, int(self.get_parent().get_parent().evaluate(nports))) - except: - return 1 - - def get_optional(self): - return self._optional_evaluated - - def get_color(self): - """ - Get the color that represents this port's type. - Codes differ for ports where the vec length is 1 or greater than 1. - - Returns: - a hex color code. - """ - try: - color = Constants.TYPE_TO_COLOR[self.get_type()] - vlen = self.get_vlen() - if vlen == 1: - return color - color_val = int(color[1:], 16) - r = (color_val >> 16) & 0xff - g = (color_val >> 8) & 0xff - b = (color_val >> 0) & 0xff - dark = (0, 0, 30, 50, 70)[min(4, vlen)] - r = max(r-dark, 0) - g = max(g-dark, 0) - b = max(b-dark, 0) - # TODO: Change this to .format() - return '#%.2x%.2x%.2x' % (r, g, b) - except: - return '#FFFFFF' - - def get_clones(self): - """ - Get the clones of this master port (nports > 1) - - Returns: - a list of ports - """ - return self._clones - - 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 - - # Prepare a copy of the odict for the clone - n = self._n.copy() - # Remove nports from the key so the copy cannot be a duplicator - if 'nports' in n: - n.pop('nports') - n['name'] = self._n['name'] + str(len(self._clones) + 1) - # Dummy value 99999 will be fixed later - n['key'] = '99999' if self._key.isdigit() else n['name'] - - # Clone - port = self.__class__(self.get_parent(), n, self._dir) - 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 - - def get_name(self): - number = '' - if self.get_type() == 'bus': - busses = filter(lambda a: a._dir == self._dir, self.get_parent().get_ports_gui()) - number = str(busses.index(self)) + '#' + str(len(self.get_associated_ports())) - return self._name + number - - def get_key(self): - return self._key - - @property - def is_sink(self): - return self._dir == 'sink' - - @property - def is_source(self): - return self._dir == 'source' - - def get_type(self): - return self.get_parent().resolve_dependencies(self._type) - - def get_domain(self): - return self._domain - - 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.get_parent().get_parent().connections - connections = filter(lambda c: c.get_source() is self or c.get_sink() is self, connections) - return connections - - def get_enabled_connections(self): - """ - Get all enabled connections that use this port. - - Returns: - a list of connection objects - """ - return filter(lambda c: c.get_enabled(), self.get_connections()) - - def get_associated_ports(self): - if not self.get_type() == 'bus': - return [self] - else: - if self.is_source: - get_ports = self.get_parent().get_sources - bus_structure = self.get_parent().current_bus_structure['source'] - else: - get_ports = self.get_parent().get_sinks - bus_structure = self.get_parent().current_bus_structure['sink'] - - ports = [i for i in get_ports() if not i.get_type() == 'bus'] - if bus_structure: - busses = [i for i in get_ports() if i.get_type() == 'bus'] - bus_index = busses.index(self) - ports = filter(lambda a: ports.index(a) in bus_structure[bus_index], ports) - return ports diff --git a/grc/core/base.py b/grc/core/base.py new file mode 100644 index 0000000000..e5ff657d85 --- /dev/null +++ b/grc/core/base.py @@ -0,0 +1,164 @@ +# Copyright 2008, 2009, 2015, 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 weakref + +from .utils.descriptors import lazy_property + + +class Element(object): + + def __init__(self, parent=None): + self._parent = weakref.ref(parent) if parent else lambda: None + self._error_messages = [] + + ################################################## + # Element Validation API + ################################################## + def validate(self): + """ + Validate this element and call validate on all children. + Call this base method before adding error messages in the subclass. + """ + del self._error_messages[:] + + for child in self.children(): + child.validate() + + def is_valid(self): + """ + Is this element valid? + + Returns: + true when the element is enabled and has no error messages or is bypassed + """ + if not self.enabled or self.get_bypassed(): + return True + return not next(self.iter_error_messages(), False) + + def add_error_message(self, msg): + """ + Add an error message to the list of errors. + + Args: + msg: the error message string + """ + self._error_messages.append(msg) + + def get_error_messages(self): + """ + Get the list of error messages from this element and all of its children. + Do not include the error messages from disabled or bypassed children. + Cleverly indent the children error messages for printing purposes. + + Returns: + a list of error message strings + """ + return [msg if elem is self else "{}:\n\t{}".format(elem, msg.replace("\n", "\n\t")) + for elem, msg in self.iter_error_messages()] + + def iter_error_messages(self): + """ + Iterate over error messages. Yields tuples of (element, message) + """ + for msg in self._error_messages: + yield self, msg + for child in self.children(): + if not child.enabled or child.get_bypassed(): + continue + for element_msg in child.iter_error_messages(): + yield element_msg + + def rewrite(self): + """ + Rewrite this element and call rewrite on all children. + Call this base method before rewriting the element. + """ + for child in self.children(): + child.rewrite() + + @property + def enabled(self): + return True + + def get_bypassed(self): + return False + + ############################################## + # Tree-like API + ############################################## + @property + def parent(self): + return self._parent() + + def get_parent_by_type(self, cls): + parent = self.parent + if parent is None: + return None + elif isinstance(parent, cls): + return parent + else: + return parent.get_parent_by_type(cls) + + @lazy_property + def parent_platform(self): + from .platform import Platform + return self.get_parent_by_type(Platform) + + @lazy_property + def parent_flowgraph(self): + from .FlowGraph import FlowGraph + return self.get_parent_by_type(FlowGraph) + + @lazy_property + def parent_block(self): + from .blocks import Block + return self.get_parent_by_type(Block) + + def reset_parents_by_type(self): + """Reset all lazy properties""" + for name, obj in vars(Element): # explicitly only in Element, not subclasses + if isinstance(obj, lazy_property): + delattr(self, name) + + def children(self): + return + yield # empty generator + + ############################################## + # Type testing + ############################################## + 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 c81180a33e..4ca0d5d2bc 100644 --- a/grc/core/Element.pyi +++ b/grc/core/blocks/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2008, 2009, 2015, 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 @@ -15,40 +15,24 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -from . import Platform, FlowGraph, Block +from __future__ import absolute_import -def lazy_property(func): - return func +from ._flags import Flags +from ._templates import MakoTemplates +from .block import Block -class Element(object): +from ._build import build - def __init__(self, parent=None): - ... - @property - def parent(self): - ... +build_ins = {} - def get_parent_by_type(self, cls): - parent = self.parent - if parent is None: - return None - elif isinstance(parent, cls): - return parent - else: - return parent.get_parent_by_type(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 - ... +def register_build_in(cls): + cls.loaded_from = '(build-in)' + build_ins[cls.key] = cls + return cls +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..6db06040cf --- /dev/null +++ b/grc/core/blocks/_build.py @@ -0,0 +1,135 @@ +# 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 collections +import itertools +import re + +from ..Constants import ADVANCED_PARAM_TAB +from ..utils import to_list + +from .block import Block +from ._flags import Flags +from ._templates import MakoTemplates + + +def build(id, label='', category='', flags='', documentation='', + value=None, asserts=None, + parameters=None, inputs=None, outputs=None, templates=None, **kwargs): + block_id = id + + cls = type(str(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(to_list(flags)) + if re.match(r'options$|variable|virtual', block_id): + cls.flags.set(Flags.NOT_DSP, Flags.DISABLE_BYPASS) + + cls.documentation = {'': documentation.strip('\n\t ').replace('\\\n', '')} + + cls.asserts = [_single_mako_expr(a, block_id) for a in to_list(asserts)] + + cls.inputs_data = _build_ports(inputs, 'sink') if inputs else [] + cls.outputs_data = _build_ports(outputs, 'source') if outputs else [] + cls.parameters_data = _build_params(parameters or [], + bool(cls.inputs_data), bool(cls.outputs_data), cls.flags) + 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 _build_ports(ports_raw, direction): + ports = [] + port_ids = set() + stream_port_ids = itertools.count() + + for i, port_params in enumerate(ports_raw): + port = port_params.copy() + port['direction'] = direction + + port_id = port.setdefault('id', str(next(stream_port_ids))) + if port_id in port_ids: + raise Exception('Port id "{}" already exists in {}s'.format(port_id, direction)) + port_ids.add(port_id) + + ports.append(port) + return ports + + +def _build_params(params_raw, have_inputs, have_outputs, flags): + params = [] + + def add_param(**data): + params.append(data) + + add_param(id='id', name='ID', dtype='id', hide='part') + + if not flags.not_dsp: + add_param(id='alias', name='Block Alias', dtype='string', + hide='part', category=ADVANCED_PARAM_TAB) + + if have_outputs or have_inputs: + add_param(id='affinity', name='Core Affinity', dtype='int_vector', + hide='part', category=ADVANCED_PARAM_TAB) + + if have_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 params_raw: + 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 _single_mako_expr(value, block_id): + if not value: + return None + value = value.strip() + if not (value.startswith('${') and value.endswith('}')): + raise ValueError('{} is not a mako substitution in {}'.format(value, block_id)) + return value[2:-1].strip() diff --git a/grc/core/blocks/_flags.py b/grc/core/blocks/_flags.py new file mode 100644 index 0000000000..bbedd6a2d7 --- /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 + + +class Flags(object): + + THROTTLE = 'throttle' + DISABLE_BYPASS = 'disable_bypass' + NEED_QT_GUI = 'need_qt_gui' + DEPRECATED = 'deprecated' + NOT_DSP = 'not_dsp' + + def __init__(self, flags): + self.data = set(flags) + + def __getattr__(self, item): + return item in self + + def __contains__(self, item): + return item in self.data + + def set(self, *flags): + self.data.update(flags) 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..0cb3f61237 --- /dev/null +++ b/grc/core/blocks/block.py @@ -0,0 +1,359 @@ +""" +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 six +from six.moves import range + +from ._templates import MakoTemplates +from ._flags import Flags + +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 + asserts = [] + + templates = MakoTemplates() + parameters_data = [] + inputs_data = [] + outputs_data = [] + + extra_data = {} + loaded_from = '(unknown)' + + def __init__(self, parent): + """Make a new block from nested data.""" + super(Block, self).__init__(parent) + param_factory = self.parent_platform.make_param + port_factory = self.parent_platform.make_port + + self.params = collections.OrderedDict( + (data['id'], param_factory(parent=self, **data)) + for data in self.parameters_data + ) + if self.key == 'options' or self.is_variable: + self.params['id'].hide = 'part' + + self.sinks = [port_factory(parent=self, **params) for params in self.inputs_data] + self.sources = [port_factory(parent=self, **params) for params in self.outputs_data] + + self.active_sources = [] # on rewrite + self.active_sinks = [] # on rewrite + + self.states = {'state': True} + + # 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_asserts() + self._validate_generate_mode_compat() + self._validate_var_value() + + def _run_asserts(self): + """Evaluate the checks""" + for expr in self.asserts: + try: + if not self.evaluate(expr): + self.add_error_message('Assertion "{}" failed.'.format(expr)) + except: + self.add_error_message('Assertion "{}" did not evaluate.'.format(expr)) + + 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' or self.key == 'options') + )) + 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/cache.py b/grc/core/cache.py new file mode 100644 index 0000000000..f438d58bd9 --- /dev/null +++ b/grc/core/cache.py @@ -0,0 +1,99 @@ +# 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 + +from __future__ import absolute_import, print_function + +from io import open +import json +import logging +import os + +import six + +from .io import yaml + +logger = logging.getLogger(__name__) + + +class Cache(object): + + def __init__(self, filename): + self.cache_file = filename + self.cache = {} + self.need_cache_write = True + self._accessed_items = set() + try: + os.makedirs(os.path.dirname(filename)) + except OSError: + pass + try: + self._converter_mtime = os.path.getmtime(filename) + except OSError: + self._converter_mtime = -1 + + def load(self): + try: + logger.debug("Loading block cache from: {}".format(self.cache_file)) + with open(self.cache_file, encoding='utf-8') as cache_file: + self.cache = json.load(cache_file) + self.need_cache_write = False + except (IOError, ValueError): + self.need_cache_write = True + + def get_or_load(self, filename): + self._accessed_items.add(filename) + if os.path.getmtime(filename) <= self._converter_mtime: + try: + return self.cache[filename] + except KeyError: + pass + + with open(filename, encoding='utf-8') as fp: + data = yaml.safe_load(fp) + self.cache[filename] = data + self.need_cache_write = True + return data + + def save(self): + if not self.need_cache_write: + return + + logger.info('Saving %d entries to json cache', len(self.cache)) + with open(self.cache_file, 'w', encoding='utf8') as cache_file: + json.dump(self.cache, cache_file) + + def prune(self): + for filename in (set(self.cache) - self._accessed_items): + del self.cache[filename] + + def __enter__(self): + self.load() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.save() + + +def byteify(data): + if isinstance(data, dict): + return {byteify(key): byteify(value) for key, value in six.iteritems(data)} + elif isinstance(data, list): + return [byteify(element) for element in data] + elif isinstance(data, six.text_type) and six.PY2: + return data.encode('utf-8') + else: + return data diff --git a/grc/core/default_flow_graph.grc b/grc/core/default_flow_graph.grc index 059509d34b..d57ec75aea 100644 --- a/grc/core/default_flow_graph.grc +++ b/grc/core/default_flow_graph.grc @@ -1,43 +1,32 @@ -<?xml version="1.0"?> -<!-- ################################################### -##Default Flow Graph: -## include an options block and a variable for sample rate +# Default Flow Graph: +# Include an options block and a variable for sample rate ################################################### - --> -<flow_graph> - <block> - <key>options</key> - <param> - <key>id</key> - <value>top_block</value> - </param> - <param> - <key>_coordinate</key> - <value>(8, 8)</value> - </param> - <param> - <key>_rotation</key> - <value>0</value> - </param> - </block> - <block> - <key>variable</key> - <param> - <key>id</key> - <value>samp_rate</value> - </param> - <param> - <key>value</key> - <value>32000</value> - </param> - <param> - <key>_coordinate</key> - <value>(8, 160)</value> - </param> - <param> - <key>_rotation</key> - <value>0</value> - </param> - </block> -</flow_graph> + +options: + parameters: + id: 'top_block' + title: 'top_block' + states: + coordinate: + - 8 + - 8 + rotation: 0 + state: enabled + +blocks: +- name: samp_rate + id: variable + parameters: + comment: '' + value: '32000' + states: + coordinate: + - 184 + - 12 + rotation: 0 + state: enabled + +metadata: + file_format: 1 + grc_version: 3.8.0 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/CMakeLists.txt b/grc/core/generator/CMakeLists.txt deleted file mode 100644 index 492ad7c4ad..0000000000 --- a/grc/core/generator/CMakeLists.txt +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2011 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. - -file(GLOB py_files "*.py") - -GR_PYTHON_INSTALL( - FILES ${py_files} - DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/core/generator -) - -install(FILES - flow_graph.tmpl - DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/core/generator -) diff --git a/grc/core/generator/FlowGraphProxy.py b/grc/core/generator/FlowGraphProxy.py index 3723005576..f438fa0d39 100644 --- a/grc/core/generator/FlowGraphProxy.py +++ b/grc/core/generator/FlowGraphProxy.py @@ -16,13 +16,17 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -class FlowGraphProxy(object): +from __future__ import absolute_import +from six.moves import range + + +class FlowGraphProxy(object): # TODO: move this in a refactored Generator def __init__(self, fg): - self._fg = fg + self.orignal_flowgraph = fg def __getattr__(self, item): - return getattr(self._fg, item) + return getattr(self.orignal_flowgraph, item) def get_hier_block_stream_io(self, direction): """ @@ -34,7 +38,7 @@ class FlowGraphProxy(object): Returns: a list of dicts with: type, label, vlen, size, optional """ - return filter(lambda p: p['type'] != "message", self.get_hier_block_io(direction)) + return [p for p in self.get_hier_block_io(direction) if p['type'] != "message"] def get_hier_block_message_io(self, direction): """ @@ -46,7 +50,7 @@ class FlowGraphProxy(object): Returns: a list of dicts with: type, label, vlen, size, optional """ - return filter(lambda p: p['type'] == "message", self.get_hier_block_io(direction)) + return [p for p in self.get_hier_block_io(direction) if p['type'] == "message"] def get_hier_block_io(self, direction): """ @@ -62,16 +66,17 @@ class FlowGraphProxy(object): 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').get_opt('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 xrange(num_ports): + for i in range(num_ports): clone = master.copy() clone['label'] += str(i) ports.append(clone) @@ -86,8 +91,8 @@ class FlowGraphProxy(object): Returns: a list of pad source blocks in this flow graph """ - pads = filter(lambda b: b.get_key() == 'pad_source', self.get_enabled_blocks()) - return sorted(pads, lambda x, y: cmp(x.get_id(), y.get_id())) + pads = [b for b in self.get_enabled_blocks() if b.key == 'pad_source'] + return sorted(pads, key=lambda x: x.name) def get_pad_sinks(self): """ @@ -96,8 +101,8 @@ class FlowGraphProxy(object): Returns: a list of pad sink blocks in this flow graph """ - pads = filter(lambda b: b.get_key() == 'pad_sink', self.get_enabled_blocks()) - return sorted(pads, lambda x, y: cmp(x.get_id(), y.get_id())) + pads = [b for b in self.get_enabled_blocks() if b.key == 'pad_sink'] + return sorted(pads, key=lambda x: x.name) def get_pad_port_global_key(self, port): """ @@ -112,15 +117,46 @@ class FlowGraphProxy(object): 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" - if port.get_parent() == pad: + 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.get_key())) + 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()) - return -1
\ No newline at end of file + 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 dda226c6b2..62dc26b8a8 100644 --- a/grc/core/generator/Generator.py +++ b/grc/core/generator/Generator.py @@ -16,22 +16,18 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -import codecs +from __future__ import absolute_import + import os -import tempfile -from Cheetah.Template import Template +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, odict +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): @@ -59,339 +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 get_file_path(self): - return self.file_path - - def write(self): - """generate output and write it to files""" - # Do throttle warning - throttling_blocks = filter(lambda b: b.throtteling(), self._flow_graph.get_enabled_blocks()) - 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(map(lambda b: b.get_key(), 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( - filter(lambda b: b.get_enabled() and not b.get_bypassed(), fg.blocks), - lambda b: b.get_id(), _get_block_sort_text - ) - deprecated_block_keys = set(block.get_name() for block in blocks_all if block.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 = filter(lambda b: b not in (imports + parameters), blocks_all) - - for block in blocks: - key = block.get_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 virtual sink connections - def cf(c): - return not (c.is_bus() or c.get_sink().get_parent().is_virtual_sink()) - connections = filter(cf, fg.get_enabled_connections()) - - # Get the virtual blocks and resolve their connections - virtual = filter(lambda c: c.get_source().get_parent().is_virtual_source(), connections) - for connection in virtual: - sink = connection.get_sink() - for source in connection.get_source().resolve_virtual_source(): - resolved = fg.get_parent().Connection(flow_graph=fg, porta=source, portb=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 = filter(lambda c: c.get_sink() == block.get_sinks()[0], connections) - # 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].get_source() - - # Loop through all the downstream connections - for sink in filter(lambda c: c.get_source() == block.get_sources()[0], connections): - if not sink.get_enabled(): - # Ignore disabled connections - continue - sink_port = sink.get_sink() - connection = fg.get_parent().Connection(flow_graph=fg, porta=source_port, portb=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.get_source().get_domain(), c.get_sink().get_domain(), - c.get_source().get_parent().get_id(), c.get_sink().get_parent().get_id() - )) - - connection_templates = fg.get_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.get_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 get_file_path_xml(self): - return 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.get_file_path_xml()) - ParseXML.validate_dtd(self.get_file_path_xml(), BLOCK_DTD) - try: - os.chmod(self.get_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 map(lambda p: p.get_id(), parameters): - return "$"+name - return name - - # Build the nested data - block_n = odict() - 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 = odict() - 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 = odict() - 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 = n['block'] - - if not block_n['name'].upper().startswith('QT GUI'): - block_n['name'] = 'QT GUI ' + block_n['name'] - - block_n.insert_after('category', 'flags', BLOCK_FLAG_NEED_QT_GUI) - - gui_hint_param = odict() - 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 n diff --git a/grc/core/generator/__init__.py b/grc/core/generator/__init__.py index f44b94a85d..98f410c8d4 100644 --- a/grc/core/generator/__init__.py +++ b/grc/core/generator/__init__.py @@ -15,4 +15,5 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -from Generator import Generator +from __future__ import absolute_import +from .Generator import Generator diff --git a/grc/core/generator/flow_graph.py.mako b/grc/core/generator/flow_graph.py.mako new file mode 100644 index 0000000000..877c9eee9d --- /dev/null +++ b/grc/core/generator/flow_graph.py.mako @@ -0,0 +1,415 @@ +% if not generate_options.startswith('hb'): +<% +from sys import version_info +python_version = version_info.major +%>\ +% if python_version == 2: +#!/usr/bin/env python2 +% elif python_version == 3: +#!/usr/bin/env python3 +% endif +% 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 == 'bokeh_gui': + +class ${class_name}(gr.top_block): + def __init__(self, doc): + gr.top_block.__init__(self, "${title}") + self.doc = doc + self.plot_lst = [] + self.widget_lst = [] +% 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 + +########################################################## +## Create a layout entry if not manually done for BokehGUI +########################################################## +% if generate_options == 'bokeh_gui': + if self.widget_lst: + input_t = bokehgui.BokehLayout.widgetbox(self.widget_lst) + widgetbox = bokehgui.BokehLayout.WidgetLayout(input_t) + widgetbox.set_layout(*(${flow_graph.get_option('placement')})) + list_obj = [widgetbox] + self.plot_lst + else: + list_obj = self.plot_lst + layout_t = bokehgui.BokehLayout.create_layout(list_obj, "${flow_graph.get_option('sizing_mode')}") + self.doc.add_root(layout_t) +% endif + + % 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 == 'bokeh_gui': + serverProc, port = bokehgui.utils.create_server() + def killProc(signum, frame, tb): + tb.stop() + tb.wait() + serverProc.terminate() + serverProc.kill() + time.sleep(1) + try: + # Define the document instance + doc = curdoc() + % if flow_graph.get_option('author'): + doc.title = "${title} - ${flow_graph.get_option('author')}" + % else: + doc.title = "${title}" + % endif + session = push_session(doc, session_id="${flow_graph.get_option('id')}", + url = "http://localhost:" + port + "/bokehgui") + # Create Top Block instance + tb = top_block_cls(doc) + try: + tb.start() + signal.signal(signal.SIGTERM, functools.partial(killProc, tb=tb)) + session.loop_until_closed() + finally: + print("Exiting the simulation. Stopping Bokeh Server") + tb.stop() + tb.wait() + finally: + serverProc.terminate() + serverProc.kill() + % 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: + 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 3bcf54eee4..0000000000 --- a/grc/core/generator/flow_graph.tmpl +++ /dev/null @@ -1,475 +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 == 'bokeh_gui' - -class $(class_name)(gr.top_block): - def __init__(self, doc): - gr.top_block.__init__(self, "$title") - self.doc = doc - self.plot_lst = [] - self.widget_lst = [] -#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 $blk.has_param('alias') and $blk.get_param('alias').get_evaluated() - (self.$blk.get_id()).set_block_alias("$blk.get_param('alias').get_evaluated()") - #end if - #if $blk.has_param('affinity') and $blk.get_param('affinity').get_evaluated() - (self.$blk.get_id()).set_processor_affinity($blk.get_param('affinity').get_evaluated()) - #end if - #if (len($blk.get_sources())>0) and $blk.has_param('minoutbuf') and (int($blk.get_param('minoutbuf').get_evaluated()) > 0) - (self.$blk.get_id()).set_min_output_buffer($blk.get_param('minoutbuf').get_evaluated()) - #end if - #if (len($blk.get_sources())>0) and $blk.has_param('maxoutbuf') and (int($blk.get_param('maxoutbuf').get_evaluated()) > 0) - (self.$blk.get_id()).set_max_output_buffer($blk.get_param('maxoutbuf').get_evaluated()) - #end if - #end if -#end for - -########################################################## -## Create a layout entry if not manually done for BokehGUI -########################################################## -#if $generate_options == 'bokeh_gui' - if self.widget_lst: - input_t = bokehgui.BokehLayout.widgetbox(self.widget_lst) - widgetbox = bokehgui.BokehLayout.WidgetLayout(input_t) - widgetbox.set_layout(*($flow_graph.get_option('placement'))) - list_obj = [widgetbox] + self.plot_lst - else: - list_obj = self.plot_lst - layout_t = bokehgui.BokehLayout.create_layout(list_obj, "$flow_graph.get_option('sizing_mode')") - self.doc.add_root(layout_t) -#end if - -######################################################## -##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.get_parent().get_key() in ('pad_source', 'pad_sink') - #set block = 'self' - #set key = $flow_graph.get_pad_port_global_key($port) - #else - #set block = 'self.' + $port.get_parent().get_id() - #set key = $port.get_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.get_source() - #set global $sink = $con.get_sink() - #include source=$connection_templates[($source.get_domain(), $sink.get_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 $m.has_param('en'): - if $m.get_param('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 == 'bokeh_gui' - serverProc, port = bokehgui.utils.create_server() - def killProc(signum, frame, tb): - tb.stop() - tb.wait() - serverProc.terminate() - serverProc.kill() - time.sleep(1) - try: - \# Define the document instance - doc = curdoc() - #if $flow_graph.get_option('author') - doc.title = "$title - $flow_graph.get_option('author')" - #else - doc.title = "$title" - #end if - session = push_session(doc, session_id="$flow_graph.get_option('id')", - url = "http://localhost:" + port + "/bokehgui") - \# Create Top Block instance - tb = top_block_cls(doc) - try: - tb.start() - signal.signal(signal.SIGTERM, functools.partial(killProc, tb=tb)) - session.loop_until_closed() - finally: - print "Exiting the simulation. Stopping Bokeh Server" - tb.stop() - tb.wait() - finally: - serverProc.terminate() - serverProc.kill() - #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..31cd198c01 --- /dev/null +++ b/grc/core/generator/hier_block.py @@ -0,0 +1,193 @@ +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<% win = 'self.' + 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) diff --git a/grc/core/generator/top_block.py b/grc/core/generator/top_block.py new file mode 100644 index 0000000000..799ebb1076 --- /dev/null +++ b/grc/core/generator/top_block.py @@ -0,0 +1,284 @@ +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 + # Handle the case where the directory is read-only + # In this case, use the system's temp directory + if not os.access(file_path, os.W_OK): + file_path = tempfile.gettempdir() + filename = self._flow_graph.get_option('id') + '.py' + self.file_path = os.path.join(file_path, filename) + self._dirname = file_path + + 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(callback): + 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(callback)] + + 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/params/__init__.py b/grc/core/params/__init__.py new file mode 100644 index 0000000000..93663bdada --- /dev/null +++ b/grc/core/params/__init__.py @@ -0,0 +1,18 @@ +# 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 + +from .param import Param diff --git a/grc/core/params/dtypes.py b/grc/core/params/dtypes.py new file mode 100644 index 0000000000..f52868c080 --- /dev/null +++ b/grc/core/params/dtypes.py @@ -0,0 +1,103 @@ +# 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 + +import re + +from six.moves import builtins + +from .. import blocks +from .. import Constants + + +# 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, AttributeError): + pass + + +validators = {} + + +def validates(*dtypes): + def decorator(func): + for dtype in dtypes: + assert dtype in Constants.PARAM_TYPE_NAMES + validators[dtype] = func + return func + return decorator + + +class ValidateError(Exception): + """Raised by validate functions""" + + +@validates('id') +def validate_block_id(param): + value = param.value + # Can python use this as a variable? + if not re.match(r'^[a-z|A-Z]\w*$', value): + raise ValidateError('ID "{}" must begin with a letter and may contain letters, numbers, ' + 'and underscores.'.format(value)) + if value in ID_BLACKLIST: + raise ValidateError('ID "{}" is blacklisted.'.format(value)) + block_names = [block.name for block in param.parent_flowgraph.iter_enabled_blocks()] + # Id should only appear once, or zero times if block is disabled + if param.key == 'id' and block_names.count(value) > 1: + raise ValidateError('ID "{}" is not unique.'.format(value)) + elif value not in block_names: + raise ValidateError('ID "{}" does not exist.'.format(value)) + return value + + +@validates('stream_id') +def validate_stream_id(param): + value = param.value + stream_ids = [ + block.params['stream_id'].value + for block in param.parent_flowgraph.iter_enabled_blocks() + if isinstance(block, blocks.VirtualSink) + ] + # Check that the virtual sink's stream id is unique + if isinstance(param.parent_block, blocks.VirtualSink) and stream_ids.count(value) >= 2: + # Id should only appear once, or zero times if block is disabled + raise ValidateError('Stream ID "{}" is not unique.'.format(value)) + # Check that the virtual source's steam id is found + elif isinstance(param.parent_block, blocks.VirtualSource) and value not in stream_ids: + raise ValidateError('Stream ID "{}" is not found.'.format(value)) + + +@validates('complex', 'real', 'float', 'int') +def validate_scalar(param): + valid_types = Constants.PARAM_TYPE_MAP[param.dtype] + if not isinstance(param.get_evaluated(), valid_types): + raise ValidateError('Expression {!r} is invalid for type {!r}.'.format( + param.get_evaluated(), param.dtype)) + + +@validates('complex_vector', 'real_vector', 'float_vector', 'int_vector') +def validate_vector(param): + # todo: check vector types + + valid_types = Constants.PARAM_TYPE_MAP[param.dtype.split('_', 1)[0]] + if not all(isinstance(item, valid_types) for item in param.get_evaluated()): + raise ValidateError('Expression {!r} is invalid for type {!r}.'.format( + param.get_evaluated(), param.dtype)) diff --git a/grc/core/params/param.py b/grc/core/params/param.py new file mode 100644 index 0000000000..30a48bb434 --- /dev/null +++ b/grc/core/params/param.py @@ -0,0 +1,407 @@ +# 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 + +import ast +import collections +import textwrap + +import six +from six.moves import range + +from .. import Constants +from ..base import Element +from ..utils.descriptors import Evaluated, EvaluatedEnum, setup_names + +from . import dtypes +from .template_arg import TemplateArg + + +@setup_names +class Param(Element): + + is_param = True + + 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 = id + self.name = label.strip() or id.title() + self.category = category or Constants.DEFAULT_PARAM_TAB + + self.dtype = dtype + self.value = self.default = str(default) + + 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.hostage_cells = set() + self._init = False + + def _init_options(self, values, labels, attributes): + """parse option and option attributes""" + options = collections.OrderedDict() + options.attributes = collections.defaultdict(dict) + + 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: + 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 = 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 + + @property + def template_arg(self): + return TemplateArg(self) + + def __str__(self): + return 'Param - {}({})'.format(self.name, self.key) + + def __repr__(self): + return '{!r}.param[{}]'.format(self.parent, self.key) + + def is_enum(self): + return self.get_raw('dtype') == 'enum' + + 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: + self._evaluated = self.evaluate() + except Exception as e: + self.add_error_message(str(e)) + + rewriter = getattr(dtypes, 'rewrite_' + self.dtype, None) + if rewriter: + rewriter(self) + + 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)) + + validator = dtypes.validators.get(self.dtype, None) + if self._init and validator: + try: + validator(self) + except dtypes.ValidateError as e: + self.add_error_message(e.message) + + def get_evaluated(self): + return self._evaluated + + def evaluate(self): + """ + Evaluate the value. + + Returns: + evaluated type + """ + self._init = True + self._lisitify_flag = False + self._stringify_flag = False + dtype = self.dtype + expr = self.get_value() + + ######################### + # ID and Enum types (not evaled) + ######################### + if dtype in ('id', 'stream_id') or self.is_enum(): + return expr + + ######################### + # Numeric Types + ######################### + elif dtype in ('raw', 'complex', 'real', 'float', 'int', 'hex', 'bool'): + if expr: + try: + value = self.parent_flowgraph.evaluate(expr) + except Exception as e: + raise Exception('Value "{}" cannot be evaluated:\n{}'.format(expr, e)) + else: + value = 0 + if dtype == 'hex': + value = hex(value) + elif dtype == 'bool': + value = bool(value) + return value + + ######################### + # Numeric Vector Types + ######################### + elif dtype in ('complex_vector', 'real_vector', 'float_vector', 'int_vector'): + if not expr: + return [] # Turn a blank string into an empty list, so it will eval + try: + 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] + return value + ######################### + # String Types + ######################### + elif dtype in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'): + # Do not check if file/directory exists, that is a runtime issue + try: + value = self.parent_flowgraph.evaluate(expr) + if not isinstance(value, str): + raise Exception() + except: + self._stringify_flag = True + value = str(expr) + if dtype == '_multiline_python_external': + ast.parse(value) # Raises SyntaxError + return value + ######################### + # GUI Position/Hint + ######################### + elif dtype == 'gui_hint': + return self.parse_gui_hint(expr) if self.parent_block.state == 'enabled' else '' + ######################### + # Import Type + ######################### + elif dtype == 'import': + # New namespace + n = dict() + try: + exec(expr, n) + except ImportError: + raise Exception('Import "{}" failed.'.format(expr)) + except Exception: + 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(dtype)) + + def to_code(self): + """ + Convert the value to code. + For string and list types, check the init flag, call evaluate(). + This ensures that evaluate() was called to set the xxxify_flags. + + Returns: + a string representing the code + """ + self._init = True + value = self.get_value() + # String types + if self.dtype in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'): + if not self._init: + self.evaluate() + return repr(value) if self._stringify_flag else value + + # Vector types + elif self.dtype in ('complex_vector', 'real_vector', 'float_vector', 'int_vector'): + if not self._init: + self.evaluate() + return '[' + value + ']' if self._lisitify_flag else value + else: + return value + + def get_opt(self, item): + return self.options.attributes[self.get_value()][item] + + ############################################## + # GUI Hint + ############################################## + def parse_gui_hint(self, expr): + """ + Parse/validate gui hint value. + + Args: + expr: gui_hint string from a block's 'gui_hint' param + + Returns: + string of python code for positioning GUI elements in pyQT + """ + self.hostage_cells.clear() + + # Parsing + if ':' in expr: + tab, pos = expr.split(':') + elif ',' in expr: + tab, pos = '', expr + else: + tab, pos = expr, '' + + if '@' in tab: + tab, index = tab.split('@') + else: + index = '0' + index = int(index) + + # Validation + def parse_pos(): + e = self.parent_flowgraph.evaluate(pos) + + if not isinstance(e, (list, tuple)) or len(e) not in (2, 4) or not all(isinstance(ei, int) for ei in e): + raise Exception('Invalid GUI Hint entered: {e!r} (Must be a list of {{2,4}} non-negative integers).'.format(e=e)) + + if len(e) == 2: + row, col = e + row_span = col_span = 1 + else: + row, col, row_span, col_span = e + + if (row < 0) or (col < 0): + raise Exception('Invalid GUI Hint entered: {e!r} (non-negative integers only).'.format(e=e)) + + if (row_span < 1) or (col_span < 1): + raise Exception('Invalid GUI Hint entered: {e!r} (positive row/column span required).'.format(e=e)) + + return row, col, row_span, col_span + + def validate_tab(): + tabs = (block for block in self.parent_flowgraph.iter_enabled_blocks() + if block.key == 'qtgui_tab_widget' and block.name == tab) + tab_block = next(iter(tabs), None) + if not tab_block: + raise Exception('Invalid tab name entered: {tab} (Tab name not found).'.format(tab=tab)) + + tab_index_size = int(tab_block.params['num_tabs'].value) + if index >= tab_index_size: + raise Exception('Invalid tab index entered: {tab}@{index} (Index out of range).'.format( + tab=tab, index=index)) + + # Collision Detection + def collision_detection(row, col, row_span, col_span): + my_parent = '{tab}@{index}'.format(tab=tab, index=index) if tab else 'main' + # Calculate hostage cells + for r in range(row, row + row_span): + for c in range(col, col + col_span): + self.hostage_cells.add((my_parent, (r, c))) + + for other in self.get_all_params('gui_hint'): + if other is self: + continue + collision = next(iter(self.hostage_cells & other.hostage_cells), None) + if collision: + raise Exception('Block {block!r} is also using parent {parent!r}, cell {cell!r}.'.format( + block=other.parent_block.name, parent=collision[0], cell=collision[1] + )) + + # Code Generation + if tab: + validate_tab() + layout = '{tab}_grid_layout_{index}'.format(tab=tab, index=index) + else: + layout = 'top_grid_layout' + + widget = '%s' # to be fill-out in the mail template + + if pos: + row, col, row_span, col_span = parse_pos() + collision_detection(row, col, row_span, col_span) + + widget_str = textwrap.dedent(""" + self.{layout}.addWidget({widget}, {row}, {col}, {row_span}, {col_span}) + for r in range({row}, {row_end}): + self.{layout}.setRowStretch(r, 1) + for c in range({col}, {col_end}): + self.{layout}.setColumnStretch(c, 1) + """.strip('\n')).format( + layout=layout, widget=widget, + row=row, row_span=row_span, row_end=row+row_span, + col=col, col_span=col_span, col_end=col+col_span, + ) + + else: + widget_str = 'self.{layout}.addWidget({widget})'.format(layout=layout, widget=widget) + + return widget_str + + def get_all_params(self, dtype, key=None): + """ + Get all the params from the flowgraph that have the given type and + optionally a given key + + Args: + dtype: the specified type + key: the key to match against + + Returns: + a list of params + """ + params = [] + for block in self.parent_flowgraph.iter_enabled_blocks(): + params.extend( + param for param in block.params.values() + if param.dtype == dtype and (key is None or key == param.name) + ) + return params diff --git a/grc/core/params/template_arg.py b/grc/core/params/template_arg.py new file mode 100644 index 0000000000..5c8c610b4f --- /dev/null +++ b/grc/core/params/template_arg.py @@ -0,0 +1,50 @@ +# 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 + + +class TemplateArg(str): + """ + A cheetah template argument created from a param. + The str of this class evaluates to the param's to code method. + The use of this class as a dictionary (enum only) will reveal the enum opts. + The __call__ or () method can return the param evaluated to a raw python data type. + """ + + def __new__(cls, param): + value = param.to_code() + instance = str.__new__(cls, value) + setattr(instance, '_param', param) + return instance + + def __getitem__(self, item): + return str(self._param.get_opt(item)) if self._param.is_enum() else NotImplemented + + def __getattr__(self, item): + if not self._param.is_enum(): + raise AttributeError() + try: + return str(self._param.get_opt(item)) + except KeyError: + raise AttributeError() + + def __str__(self): + return str(self._param.to_code()) + + def __call__(self): + return self._param.get_evaluated() diff --git a/grc/core/platform.py b/grc/core/platform.py new file mode 100644 index 0000000000..6d02cb6441 --- /dev/null +++ b/grc/core/platform.py @@ -0,0 +1,421 @@ +# 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 os +import logging +from itertools import chain + +import six +from six.moves import range + +from . import ( + Messages, Constants, + blocks, params, ports, errors, utils, schema_checker +) + +from .Config import Config +from .cache import Cache +from .base import Element +from .io import yaml +from .generator import Generator +from .FlowGraph import FlowGraph +from .Connection import Connection + +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.') + + with Cache(Constants.CACHE_FILE) as cache: + for file_path in self._iter_files_in_block_path(path): + data = cache.get_or_load(file_path) + + 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() + utils.hide_bokeh_gui_options_if_not_installed(self.blocks['options']) + + 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): + for dirpath, dirnames, filenames in os.walk(entry): + for filename in sorted(filter(lambda f: f.endswith('.' + ext), filenames)): + yield os.path.join(dirpath, filename) + 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['id'] = data['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" loaded from\n %s\noverwritten by\n %s', + block_id, self.blocks[block_id].loaded_from, file_path) + + try: + block_cls = self.blocks[block_id] = self.new_block_class(**data) + block_cls.loaded_from = file_path + 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, six.string_types): + log.debug('Invalid name %r', name) + return + path.append(name) + for element in utils.to_list(elements): + if isinstance(element, six.string_types): + 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: params.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, **data): + return blocks.build(**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..d511e36887 --- /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=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), + + asserts=(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 2aed42d762..f2ac986fb4 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 @@ -15,9 +15,19 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -import expr_utils -import epy_block_io -import extract_docs +from __future__ import absolute_import + +import six + +from . import epy_block_io, expr_utils, extract_docs, flow_graph_complexity +from .hide_bokeh_gui_options_if_not_installed import hide_bokeh_gui_options_if_not_installed + + +def to_list(value): + if not value: + return [] + elif isinstance(value, six.string_types): + return [value] + else: + return list(value) -from odict import odict -from hide_bokeh_gui_options_if_not_installed import hide_bokeh_gui_options_if_not_installed diff --git a/grc/core/utils/CMakeLists.txt b/grc/core/utils/backports/__init__.py index 3ba65258a5..a24ee3ae01 100644 --- a/grc/core/utils/CMakeLists.txt +++ b/grc/core/utils/backports/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2015 Free Software Foundation, Inc. +# Copyright 2016 Free Software Foundation, Inc. # # This file is part of GNU Radio # @@ -17,9 +17,9 @@ # the Free Software Foundation, Inc., 51 Franklin Street, # Boston, MA 02110-1301, USA. -file(GLOB py_files "*.py") +from __future__ import absolute_import -GR_PYTHON_INSTALL( - FILES ${py_files} - DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/core/utils -) +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/complexity.py b/grc/core/utils/complexity.py deleted file mode 100644 index baa8040db4..0000000000 --- a/grc/core/utils/complexity.py +++ /dev/null @@ -1,49 +0,0 @@ - -def calculate_flowgraph_complexity(flowgraph): - """ Determines the complexity of a flowgraph """ - dbal = 0 - for block in flowgraph.blocks: - # Skip options block - if block.get_key() == 'options': - continue - - # Don't worry about optional sinks? - sink_list = filter(lambda c: not c.get_optional(), block.get_sinks()) - source_list = filter(lambda c: not c.get_optional(), block.get_sources()) - sinks = float(len(sink_list)) - sources = float(len(source_list)) - base = max(min(sinks, sources), 1) - - # Port ratio multiplier - if min(sinks, sources) > 0: - multi = sinks / sources - multi = (1 / multi) if multi > 1 else multi - else: - multi = 1 - - # Connection ratio multiplier - sink_multi = max(float(sum(map(lambda c: len(c.get_connections()), sink_list)) / max(sinks, 1.0)), 1.0) - source_multi = max(float(sum(map(lambda c: len(c.get_connections()), source_list)) / max(sources, 1.0)), 1.0) - dbal = dbal + (base * multi * sink_multi * source_multi) - - blocks = float(len(flowgraph.blocks)) - connections = float(len(flowgraph.connections)) - elements = blocks + connections - disabled_connections = len(filter(lambda c: not c.get_enabled(), flowgraph.connections)) - variables = elements - blocks - connections - enabled = float(len(flowgraph.get_enabled_blocks())) - - # Disabled multiplier - if enabled > 0: - disabled_multi = 1 / (max(1 - ((blocks - enabled) / max(blocks, 1)), 0.05)) - else: - disabled_multi = 1 - - # Connection multiplier (How many connections ) - if (connections - disabled_connections) > 0: - conn_multi = 1 / (max(1 - (disabled_connections / max(connections, 1)), 0.05)) - else: - conn_multi = 1 - - final = round(max((dbal - 1) * disabled_multi * conn_multi * connections, 0.0) / 1000000, 6) - return final 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..0e1b68761c --- /dev/null +++ b/grc/core/utils/descriptors/evaluated.py @@ -0,0 +1,117 @@ +# 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 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, six.text_type) 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): + if isinstance(allowed_values, six.string_types): + allowed_values = set(allowed_values.split()) + self.allowed_values = allowed_values + default = default if default is not None else next(iter(self.allowed_values)) + 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/epy_block_io.py b/grc/core/utils/epy_block_io.py index a094ab7ad5..823116adb9 100644 --- a/grc/core/utils/epy_block_io.py +++ b/grc/core/utils/epy_block_io.py @@ -1,7 +1,12 @@ +from __future__ import absolute_import + import inspect import collections +import six +from six.moves import zip + TYPE_MAP = { 'complex64': 'complex', 'complex': 'complex', @@ -32,10 +37,10 @@ def _ports(sigs, msgs): def _find_block_class(source_code, cls): ns = {} try: - exec source_code in ns + exec(source_code, ns) except Exception as e: raise ValueError("Can't interpret source code: " + str(e)) - for var in ns.itervalues(): + for var in six.itervalues(ns): if inspect.isclass(var) and issubclass(var, cls): return var raise ValueError('No python block class found in code') @@ -53,7 +58,7 @@ def extract(cls): spec = inspect.getargspec(cls.__init__) init_args = spec.args[1:] - defaults = map(repr, spec.defaults or ()) + defaults = [repr(arg) for arg in (spec.defaults or ())] doc = cls.__doc__ or cls.__init__.__doc__ or '' cls_name = cls.__name__ diff --git a/grc/core/utils/expr_utils.py b/grc/core/utils/expr_utils.py index 2059ceff9f..427585e93c 100644 --- a/grc/core/utils/expr_utils.py +++ b/grc/core/utils/expr_utils.py @@ -17,18 +17,111 @@ 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 string -VAR_CHARS = string.letters + string.digits + '_' +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] -class graph(object): + 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): """ 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: @@ -50,13 +143,13 @@ class graph(object): self._graph[src_node_key].remove(dest_node_key) def get_nodes(self): - return self._graph.keys() + return list(self._graph.keys()) def get_edges(self, node_key): 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. @@ -85,43 +178,10 @@ def expr_split(expr, var_chars=VAR_CHARS): toks.append(char) tok = '' toks.append(tok) - return filter(lambda t: t, toks) - - -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 replace_dict.keys(): - expr_splits[i] = replace_dict[es] - return ''.join(expr_splits) + return [t for t in toks if t] -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(var for var in vars if var in expr_toks) - - -def get_graph(exprs): +def _get_graph(exprs): """ Get a graph representing the variable dependencies @@ -131,19 +191,19 @@ def get_graph(exprs): Returns: a graph of variable deps """ - vars = exprs.keys() + 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 exprs.iteritems(): + for var, expr in six.iteritems(exprs): for dep in get_variable_dependencies(expr, vars): if dep != var: var_graph.add_edge(dep, var) return var_graph -def sort_variables(exprs): +def _sort_variables(exprs): """ Get a list of variables in order of dependencies. @@ -154,12 +214,12 @@ 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(): # Get a list of nodes with no edges - indep_vars = filter(lambda var: not var_graph.get_edges(var), var_graph.get_nodes()) + indep_vars = [var for var in var_graph.get_nodes() if not var_graph.get_edges(var)] if not indep_vars: raise Exception('circular dependency caught in sort_variables') # Add the indep vars to the end of the list @@ -168,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 a6e0bc971e..7688f98de5 100644 --- a/grc/core/utils/extract_docs.py +++ b/grc/core/utils/extract_docs.py @@ -17,15 +17,19 @@ 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 sys import re import subprocess import threading import json -import Queue import random import itertools +import six +from six.moves import queue, filter, range + ############################################################################### # The docstring extraction @@ -94,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__} @@ -124,7 +127,7 @@ class SubprocessLoader(object): self.callback_query_result = callback_query_result self.callback_finished = callback_finished or (lambda: None) - self._queue = Queue.Queue() + self._queue = queue.Queue() self._thread = None self._worker = None self._shutdown = threading.Event() @@ -157,14 +160,15 @@ class SubprocessLoader(object): cmd, args = self._last_cmd if cmd == 'query': msg += " (crashed while loading {0!r})".format(args[0]) - print >> sys.stderr, msg + print(msg, file=sys.stderr) continue # restart else: break # normal termination, return finally: - self._worker.terminate() + if self._worker: + self._worker.terminate() else: - print >> sys.stderr, "Warning: docstring loader crashed too often" + print("Warning: docstring loader crashed too often", file=sys.stderr) self._thread = None self._worker = None self.callback_finished() @@ -203,9 +207,9 @@ class SubprocessLoader(object): key, docs = args self.callback_query_result(key, docs) elif cmd == 'error': - print args + print(args) else: - print >> sys.stderr, "Unknown response:", cmd, args + print("Unknown response:", cmd, args, file=sys.stderr) def query(self, key, imports=None, make=None): """ Request docstring extraction for a certain key """ @@ -270,12 +274,12 @@ if __name__ == '__worker__': elif __name__ == '__main__': def callback(key, docs): - print key - for match, doc in docs.iteritems(): - print '-->', match - print doc.strip() - print - print + print(key) + for match, doc in six.iteritems(docs): + print('-->', match) + print(str(doc).strip()) + print() + print() r = SubprocessLoader(callback) diff --git a/grc/core/utils/flow_graph_complexity.py b/grc/core/utils/flow_graph_complexity.py new file mode 100644 index 0000000000..e8962b0ae3 --- /dev/null +++ b/grc/core/utils/flow_graph_complexity.py @@ -0,0 +1,54 @@ + +def calculate(flowgraph): + """ Determines the complexity of a flowgraph """ + + try: + dbal = 0.0 + for block in flowgraph.blocks: + if block.key == "options": + continue + + # Determine the base value for this block + sinks = sum(1.0 for port in block.sinks if not port.optional) + sources = sum(1.0 for port in block.sources if not port.optional) + base = max(min(sinks, sources), 1) + + # Determine the port multiplier + block_connections = 0.0 + for port in block.sources: + block_connections += sum(1.0 for c in port.connections()) + source_multi = max(block_connections / max(sources, 1.0), 1.0) + + # Port ratio multiplier + multi = 1.0 + if min(sinks, sources) > 0: + multi = float(sinks / sources) + multi = float(1 / multi) if multi > 1 else multi + + dbal += base * multi * source_multi + + blocks = float(len(flowgraph.blocks) - 1) + connections = float(len(flowgraph.connections)) + variables = float(len(flowgraph.get_variables())) + + enabled = float(len(flowgraph.get_enabled_blocks())) + enabled_connections = float(len(flowgraph.get_enabled_connections())) + disabled_connections = connections - enabled_connections + + # Disabled multiplier + if enabled > 0: + disabled_multi = 1 / (max(1 - ((blocks - enabled) / max(blocks, 1)), 0.05)) + else: + disabled_multi = 1 + + # Connection multiplier (How many connections ) + if (connections - disabled_connections) > 0: + conn_multi = 1 / (max(1 - (disabled_connections / max(connections, 1)), 0.05)) + else: + conn_multi = 1 + + final = round(max((dbal - 1) * disabled_multi * conn_multi * connections, 0.0) / 1000000, 6) + return final + + except Exception: + return "<Error>" diff --git a/grc/core/utils/hide_bokeh_gui_options_if_not_installed.py b/grc/core/utils/hide_bokeh_gui_options_if_not_installed.py index fc0141851a..ab4a42b2e7 100644 --- a/grc/core/utils/hide_bokeh_gui_options_if_not_installed.py +++ b/grc/core/utils/hide_bokeh_gui_options_if_not_installed.py @@ -16,13 +16,12 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -def hide_bokeh_gui_options_if_not_installed(options): +def hide_bokeh_gui_options_if_not_installed(options_blk): try: import bokehgui except ImportError: - generate_option = options.get_param('generate_options') - list_generate_option = generate_option.get_options() - for option in list_generate_option: - if option.get_key() == 'bokeh_gui': - list_generate_option.remove(option) - return + for param in options_blk.parameters_data: + if param['id'] == 'generate_options': + ind = param['options'].index('bokeh_gui') + del param['options'][ind] + del param['option_labels'][ind] diff --git a/grc/core/utils/odict.py b/grc/core/utils/odict.py deleted file mode 100644 index 740a81c604..0000000000 --- a/grc/core/utils/odict.py +++ /dev/null @@ -1,115 +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 UserDict import DictMixin - - -class odict(DictMixin): - - def __init__(self, d={}): - self._keys = list(d.keys()) - self._data = dict(d.copy()) - - def __setitem__(self, key, value): - if key not in self._data: - self._keys.append(key) - self._data[key] = value - - def __getitem__(self, key): - return self._data[key] - - def __delitem__(self, key): - del self._data[key] - self._keys.remove(key) - - def keys(self): - return list(self._keys) - - def copy(self): - copy_dict = odict() - copy_dict._data = self._data.copy() - copy_dict._keys = list(self._keys) - return copy_dict - - def insert_after(self, pos_key, key, val): - """ - Insert the new key, value entry after the entry given by the position key. - If the positional key is None, insert at the end. - - Args: - pos_key: the positional key - key: the key for the new entry - val: the value for the new entry - """ - index = (pos_key is None) and len(self._keys) or self._keys.index(pos_key) - if key in self._keys: - raise KeyError('Cannot insert, key "{}" already exists'.format(str(key))) - self._keys.insert(index+1, key) - self._data[key] = val - - def insert_before(self, pos_key, key, val): - """ - Insert the new key, value entry before the entry given by the position key. - If the positional key is None, insert at the beginning. - - Args: - pos_key: the positional key - key: the key for the new entry - val: the value for the new entry - """ - index = (pos_key is not None) and self._keys.index(pos_key) or 0 - if key in self._keys: - raise KeyError('Cannot insert, key "{}" already exists'.format(str(key))) - self._keys.insert(index, key) - self._data[key] = val - - def find(self, key): - """ - Get the value for this key if exists. - - Args: - key: the key to search for - - Returns: - the value or None - """ - if key in self: - return self[key] - return None - - def findall(self, key): - """ - Get a list of values for this key. - - Args: - key: the key to search for - - Returns: - a list of values or empty list - """ - obj = self.find(key) - if obj is None: - obj = list() - if isinstance(obj, list): - return obj - return [obj] - - def clear(self): - self._data.clear() - del self._keys[:]
\ No newline at end of file |