diff options
Diffstat (limited to 'grc/python/base')
-rw-r--r-- | grc/python/base/Block.py | 542 | ||||
-rw-r--r-- | grc/python/base/CMakeLists.txt | 43 | ||||
-rw-r--r-- | grc/python/base/Connection.py | 139 | ||||
-rw-r--r-- | grc/python/base/Constants.py | 50 | ||||
-rw-r--r-- | grc/python/base/Element.py | 98 | ||||
-rw-r--r-- | grc/python/base/FlowGraph.py | 482 | ||||
-rw-r--r-- | grc/python/base/Param.py | 203 | ||||
-rw-r--r-- | grc/python/base/ParseXML.py | 155 | ||||
-rw-r--r-- | grc/python/base/Platform.py | 274 | ||||
-rw-r--r-- | grc/python/base/Port.py | 136 | ||||
-rw-r--r-- | grc/python/base/__init__.py | 20 | ||||
-rw-r--r-- | grc/python/base/block_tree.dtd | 26 | ||||
-rw-r--r-- | grc/python/base/domain.dtd | 35 | ||||
-rw-r--r-- | grc/python/base/flow_graph.dtd | 38 | ||||
-rw-r--r-- | grc/python/base/odict.py | 105 |
15 files changed, 2346 insertions, 0 deletions
diff --git a/grc/python/base/Block.py b/grc/python/base/Block.py new file mode 100644 index 0000000000..77c3145173 --- /dev/null +++ b/grc/python/base/Block.py @@ -0,0 +1,542 @@ +""" +Copyright 2008-2011 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 . import odict +from . Constants import ADVANCED_PARAM_TAB, DEFAULT_PARAM_TAB +from . Constants import BLOCK_FLAG_THROTTLE, BLOCK_FLAG_DISABLE_BYPASS +from . Constants import BLOCK_ENABLED, BLOCK_BYPASSED, BLOCK_DISABLED +from Element import Element + +from Cheetah.Template import Template +from UserDict import UserDict +from itertools import imap + + +class TemplateArg(UserDict): + """ + 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): + UserDict.__init__(self) + self._param = param + if param.is_enum(): + for key in param.get_opt_keys(): + self[key] = str(param.get_opt(key)) + + def __str__(self): + return str(self._param.to_code()) + + def __call__(self): + return self._param.get_evaluated() + + +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 "%s" not found in %s.'%(key, _get_keys(lst)) + + +class Block(Element): + + 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 + """ + #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') + self._category = n.find('category') or '' + 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 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 "%s" already exists in params'%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 "%s" already exists in sources'%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 "%s" already exists in sinks'%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. + is_virtual_or_pad = self._key in ( + "virtual_source", "virtual_sink", "pad_source", "pad_sink") + is_variable = self._key.startswith('variable') + + # Disable blocks that are virtual/pads or variables + if is_virtual_or_pad or is_variable: + self._flags += BLOCK_FLAG_DISABLE_BYPASS + + if not (is_virtual_or_pad or 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 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 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 + }) + )) + + 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 - %s - %s(%s)'%(self.get_id(), self.get_name(), self.get_key()) + + def get_id(self): return self.get_param('id').get_value() + def is_block(self): return True + def get_name(self): return self._name + def get_key(self): return self._key + def get_category(self): return self._category + def set_category(self, cat): self._category = cat + def get_doc(self): return '' + 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 + + ############################################## + # 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 paramater 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((p.get_key(), TemplateArg(p)) for p in self.get_params()) + try: + return str(Template(tmpl, n)) + except Exception as err: + return "Template error: %s\n %s" % (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 port_controller_modify(self, direction): + """ + Change the port controller. + + Args: + direction: +1 or -1 + + Returns: + true for change + """ + return False + + 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 (not 'bus' 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 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 + """ + get_hash = lambda: hash(tuple(map(hash, self.get_params()))) + my_hash = 0 + while 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 = 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/python/base/CMakeLists.txt b/grc/python/base/CMakeLists.txt new file mode 100644 index 0000000000..bdc8a5006f --- /dev/null +++ b/grc/python/base/CMakeLists.txt @@ -0,0 +1,43 @@ +# 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. + +######################################################################## +GR_PYTHON_INSTALL(FILES + odict.py + ParseXML.py + Block.py + Connection.py + Constants.py + Element.py + FlowGraph.py + Param.py + Platform.py + Port.py + __init__.py + DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/base + COMPONENT "grc" +) + +install(FILES + block_tree.dtd + domain.dtd + flow_graph.dtd + DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/base + COMPONENT "grc" +) diff --git a/grc/python/base/Connection.py b/grc/python/base/Connection.py new file mode 100644 index 0000000000..8df0f5ad53 --- /dev/null +++ b/grc/python/base/Connection.py @@ -0,0 +1,139 @@ +""" +Copyright 2008-2011 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 .Element import Element +from . import odict + + +class Connection(Element): + + def __init__(self, flow_graph, porta, portb): + """ + 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) + @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 + if port.is_sink(): sink = port + if not source: raise ValueError('Connection could not isolate source') + if not sink: raise ValueError('Connection could not isolate sink') + 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 self.get_parent().get_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 + + def __str__(self): + return 'Connection (\n\t%s\n\t\t%s\n\t%s\n\t\t%s\n)'%( + self.get_source().get_parent(), + self.get_source(), + self.get_sink().get_parent(), + self.get_sink(), + ) + + def is_connection(self): return True + + def validate(self): + """ + 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.get_connection_templates(): + self.add_error_message('No connection known for domains "%s", "%s"' + % (source_domain, sink_domain)) + too_many_other_sinks = ( + source_domain in platform.get_domains() and + not platform.get_domain(key=source_domain)['multiple_sinks'] and + len(self.get_source().get_enabled_connections()) > 1 + ) + too_many_other_sources = ( + sink_domain in platform.get_domains() and + not platform.get_domain(key=sink_domain)['multiple_sources'] and + len(self.get_sink().get_enabled_connections()) > 1 + ) + if too_many_other_sinks: + self.add_error_message( + 'Domain "%s" can have only one downstream block' % source_domain) + if too_many_other_sources: + self.add_error_message( + 'Domain "%s" can have only one upstream block' % sink_domain) + + def get_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() + + ############################# + # Access Ports + ############################# + def get_sink(self): return self._sink + def get_source(self): return self._source + + ############################################## + ## Import/Export Methods + ############################################## + def export_data(self): + """ + Export this connection's info. + + 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 diff --git a/grc/python/base/Constants.py b/grc/python/base/Constants.py new file mode 100644 index 0000000000..1e83de63b5 --- /dev/null +++ b/grc/python/base/Constants.py @@ -0,0 +1,50 @@ +""" +Copyright 2008, 2009 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 + +#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') + +# file format versions: +# 0: undefined / legacy +# 1: non-numeric message port keys (label is used instead) +FLOW_GRAPH_FILE_FORMAT_VERSION = 1 + +# Param tabs +DEFAULT_PARAM_TAB = "General" +ADVANCED_PARAM_TAB = "Advanced" + +# Port domains +DOMAIN_DTD = os.path.join(DATA_DIR, 'domain.dtd') +GR_STREAM_DOMAIN = "gr_stream" +GR_MESSAGE_DOMAIN = "gr_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_NEED_WX_GUI = 'need_ex_gui' + +# Block States +BLOCK_DISABLED = 0 +BLOCK_ENABLED = 1 +BLOCK_BYPASSED = 2 diff --git a/grc/python/base/Element.py b/grc/python/base/Element.py new file mode 100644 index 0000000000..3b604a5340 --- /dev/null +++ b/grc/python/base/Element.py @@ -0,0 +1,98 @@ +""" +Copyright 2008, 2009 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 + """ + return not self.get_error_messages() or not self.get_enabled() + + 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 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(), self.get_children()): + for msg in child.get_error_messages(): + error_messages.append("%s:\n\t%s"%(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 + + ############################################## + ## Tree-like API + ############################################## + def get_parent(self): return self._parent + def get_children(self): return list() + + ############################################## + ## Type testing methods + ############################################## + def is_element(self): return True + def is_platform(self): return False + def is_flow_graph(self): return False + def is_connection(self): return False + def is_block(self): return False + def is_dummy_block(self): return False + def is_source(self): return False + def is_sink(self): return False + def is_port(self): return False + def is_param(self): return False diff --git a/grc/python/base/FlowGraph.py b/grc/python/base/FlowGraph.py new file mode 100644 index 0000000000..42faab6a16 --- /dev/null +++ b/grc/python/base/FlowGraph.py @@ -0,0 +1,482 @@ +""" +Copyright 2008-2011 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 time +from operator import methodcaller +from itertools import ifilter + +from grc.gui import Messages + +from . import odict +from .Element import Element +from .Constants import FLOW_GRAPH_FILE_FORMAT_VERSION + + +class FlowGraph(Element): + + def __init__(self, platform): + """ + Make a flow graph from the arguments. + + Args: + platform: a platforms with blocks and contrcutors + + Returns: + the flow graph object + """ + #initialize + Element.__init__(self, platform) + self._elements = [] + self._timestamp = time.ctime() + #inital blank import + self.import_data() + + def _get_unique_id(self, base_id=''): + """ + Get a unique id starting with the base id. + + Args: + base_id: the id starts with this and appends a count + + Returns: + a unique id + """ + index = 0 + while True: + id = '%s_%d' % (base_id, index) + index += 1 + #make sure that the id is not used by another block + if not filter(lambda b: b.get_id() == id, self.get_blocks()): return id + + def __str__(self): + return 'FlowGraph - %s(%s)' % (self.get_option('title'), self.get_option('id')) + + def get_complexity(self): + """ + Determines the complexity of a flowgraph + """ + dbal = 0 + block_list = self.get_blocks() + for block in block_list: + # 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) + + elements = float(len(self.get_elements())) + connections = float(len(self.get_connections())) + disabled_connections = len(filter(lambda c: not c.get_enabled(), self.get_connections())) + blocks = float(len(block_list)) + variables = elements - blocks - connections + enabled = float(len(self.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 + + def rewrite(self): + def refactor_bus_structure(): + + for block in self.get_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); + + for child in self.get_children(): child.rewrite() + refactor_bus_structure() + + def get_option(self, key): + """ + Get the option for a given key. + The option comes from the special options block. + + Args: + key: the param key for the options block + + Returns: + the value held by that param + """ + return self._options_block.get_param(key).get_evaluated() + + def is_flow_graph(self): return True + + ############################################## + ## Access Elements + ############################################## + def get_block(self, id): + for block in self.iter_blocks(): + if block.get_id() == id: + return block + raise KeyError('No block with ID {0!r}'.format(id)) + + def iter_blocks(self): + return ifilter(methodcaller('is_block'), self.get_elements()) + + def get_blocks(self): + return list(self.iter_blocks()) + + def iter_connections(self): + return ifilter(methodcaller('is_connection'), self.get_elements()) + + def get_connections(self): + return list(self.iter_connections()) + + def get_elements(self): + """ + Get a list of all the elements. + Always ensure that the options block is in the list (only once). + + Returns: + the element list + """ + options_block_count = self._elements.count(self._options_block) + if not options_block_count: + self._elements.append(self._options_block) + for i in range(options_block_count-1): + self._elements.remove(self._options_block) + return self._elements + + get_children = get_elements + + def iter_enabled_blocks(self): + """ + Get an iterator of all blocks that are enabled and not bypassed. + """ + return ifilter(methodcaller('get_enabled'), self.iter_blocks()) + + def get_enabled_blocks(self): + """ + Get a list of all blocks that are enabled and not bypassed. + + Returns: + a list of blocks + """ + return list(self.iter_enabled_blocks()) + + def get_bypassed_blocks(self): + """ + Get a list of all blocks that are bypassed. + + Returns: + a list of blocks + """ + return filter(methodcaller('get_bypassed'), self.iter_blocks()) + + def get_enabled_connections(self): + """ + Get a list of all connections that are enabled. + + Returns: + a list of connections + """ + return filter(methodcaller('get_enabled'), self.get_connections()) + + def get_new_block(self, key): + """ + Get a new block of the specified key. + Add the block to the list of elements. + + Args: + key: the block key + + Returns: + the new block or None if not found + """ + if key not in self.get_parent().get_block_keys(): return None + block = self.get_parent().get_new_block(self, key) + self.get_elements().append(block); + if block._bussify_sink: + block.bussify({'name':'bus','type':'bus'}, 'sink') + if block._bussify_source: + block.bussify({'name':'bus','type':'bus'}, 'source') + return block; + + def connect(self, porta, portb): + """ + Create a connection between porta and portb. + + Args: + porta: a port + portb: another port + @throw Exception bad connection + + Returns: + the new connection + """ + connection = self.get_parent().Connection(flow_graph=self, porta=porta, portb=portb) + self.get_elements().append(connection) + return connection + + def remove_element(self, element): + """ + Remove the element from the list of elements. + If the element is a port, remove the whole block. + If the element is a block, remove its connections. + If the element is a connection, just remove the connection. + """ + if element not in self.get_elements(): return + #found a port, set to parent signal block + if element.is_port(): + element = element.get_parent() + #remove block, remove all involved connections + if element.is_block(): + for port in element.get_ports(): + map(self.remove_element, port.get_connections()) + if element.is_connection(): + 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.get_elements().remove(element) + + def evaluate(self, expr): + """ + Evaluate the expression. + + Args: + expr: the string expression + @throw NotImplementedError + """ + raise NotImplementedError + + ############################################## + ## Import/Export Methods + ############################################## + def export_data(self): + """ + Export this flow graph to nested data. + Export all block and connection data. + + Returns: + a nested data odict + """ + # sort blocks and connections for nicer diffs + blocks = sorted(self.iter_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.get_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': self.get_parent().get_version_short(), + 'format': FLOW_GRAPH_FILE_FORMAT_VERSION, + }) + return odict({'flow_graph': n, '_instructions': instructions}) + + def import_data(self, n=None): + """ + Import blocks and connections into this flow graph. + Clear this flowgraph of all previous blocks and connections. + Any blocks or connections in error will be ignored. + + Args: + n: the nested data odict + """ + errors = False + self._elements = list() # remove previous elements + # 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 + + 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() + + # build the blocks + self._options_block = self.get_parent().get_new_block(self, 'options') + for block_n in fg_n.findall('block'): + key = block_n.find('key') + block = self._options_block if key == 'options' else self.get_new_block(key) + + if not block: # looks like this block key cannot be found + # create a dummy block instead + block = self.get_new_block('dummy_block') + # Ugly ugly ugly + _initialize_dummy_block(block, block_n) + Messages.send_error_msg_load('Block key "%s" not found' % key) + + block.import_data(block_n) + + # build the connections + def verify_and_get_port(key, block, dir): + ports = block.get_sinks() if dir == 'sink' else block.get_sources() + for port in ports: + if key == port.get_key(): + break + if not key.isdigit() and port.get_type() == '' and key == port.get_name(): + break + else: + if block.is_dummy_block(): + port = _dummy_block_add_port(block, key, dir) + else: + raise LookupError('%s key %r not in %s block keys' % (dir, key, dir)) + return port + + 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) + + # 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) + + # 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') + self.connect(source_port, sink_port) + except LookupError as e: + Messages.send_error_load( + 'Connection between %s(%s) and %s(%s) could not be made.\n\t%s' % ( + source_block_id, source_key, sink_block_id, sink_key, e)) + errors = True + + self.rewrite() # global rewrite + return errors + + +def _update_old_message_port_keys(source_key, sink_key, source_block, sink_block): + """Backward compatibility for message port keys + + Message ports use their names as key (like in the 'connect' method). + Flowgraph files from former versions still have numeric keys stored for + message connections. These have to be replaced by the name of the + respective port. The correct message port is deduced from the integer + value of the key (assuming the order has not changed). + + The connection ends are updated only if both ends translate into a + 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() + 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'}) + block.get_params().append(block.get_parent().get_parent().Param(block=block, n=new_param_n)) + + +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/python/base/Param.py b/grc/python/base/Param.py new file mode 100644 index 0000000000..b246d9f759 --- /dev/null +++ b/grc/python/base/Param.py @@ -0,0 +1,203 @@ +""" +Copyright 2008-2011 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 . import odict +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 "%s" not found in %s.'%(key, _get_keys(lst)) + +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 "%s" into key:value'%opt + #test against repeated keys + if self._opts.has_key(key): + raise Exception, 'Key "%s" already exists in option'%key + #store the option + self._opts[key] = value + + def __str__(self): return 'Option %s(%s)'%(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 Param(Element): + + 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 not self._tab_label 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() + 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 "%s" already exists in options'%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 "%s" are not unique.'%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 "%s" are not identical across all options.'%opt_keys + #if a value is specified, it must be in the options keys + self._value = value if value or value in self.get_option_keys() else self.get_option_keys()[0] + if self.get_value() not in self.get_option_keys(): + raise Exception, 'The value "%s" is not in the possible values of "%s".'%(self.get_value(), self.get_option_keys()) + else: self._value = value or '' + self._default = value + + 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 "%s" is not a possible type.'%self.get_type()) + + def get_evaluated(self): raise NotImplementedError + + def to_code(self): + """ + Convert the value to code. + @throw NotImplementedError + """ + raise NotImplementedError + + def get_types(self): + """ + Get a list of all possible param types. + @throw NotImplementedError + """ + raise NotImplementedError + + def get_color(self): return '#FFFFFF' + def __str__(self): return 'Param - %s(%s)'%(self.get_name(), self.get_key()) + def is_param(self): return True + def get_name(self): return self.get_parent().resolve_dependencies(self._name).strip() + def get_key(self): return self._key + def get_hide(self): return self.get_parent().resolve_dependencies(self._hide).strip() + + 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): self._value = str(value) #must be a string + + def value_is_default(self): + return self._default == self._value + + def get_type(self): return self.get_parent().resolve_dependencies(self._type) + def get_tab_label(self): return self._tab_label + def is_enum(self): return self._type == 'enum' + + def __repr__(self): + """ + Get the repr (nice string format) for this param. + Just return the value (special case enum). + Derived classes can handle complex formatting. + + Returns: + the string representation + """ + if self.is_enum(): return self.get_option(self.get_value()).get_name() + return self.get_value() + + ############################################## + # 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/python/base/ParseXML.py b/grc/python/base/ParseXML.py new file mode 100644 index 0000000000..2d5fed0862 --- /dev/null +++ b/grc/python/base/ParseXML.py @@ -0,0 +1,155 @@ +""" +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 +""" + +from lxml import etree +from . import odict + +xml_failures = {} + + +class XMLSyntaxError(Exception): + def __init__(self, error_log): + self._error_log = error_log + xml_failures[error_log.last_error.filename] = error_log + + def __str__(self): + return '\n'.join(map(str, self._error_log.filter_from_errors())) + + +def validate_dtd(xml_file, dtd_file=None): + """ + Validate an xml file against its dtd. + + Args: + xml_file: the xml file + dtd_file: the optional dtd file + @throws Exception validation fails + """ + # perform parsing, use dtd validation if dtd file is not specified + parser = etree.XMLParser(dtd_validation=not dtd_file) + try: + xml = etree.parse(xml_file, parser=parser) + except etree.LxmlError: + pass + if parser.error_log: + raise XMLSyntaxError(parser.error_log) + + # perform dtd validation if the dtd file is specified + if not dtd_file: + return + try: + dtd = etree.DTD(dtd_file) + if not dtd.validate(xml.getroot()): + raise XMLSyntaxError(dtd.error_log) + except etree.LxmlError: + raise XMLSyntaxError(dtd.error_log) + + +def from_file(xml_file): + """ + Create nested data from an xml file using the from xml helper. + Also get the grc version information. + + Args: + xml_file: the xml file path + + Returns: + the nested data with grc version information + """ + xml = etree.parse(xml_file) + nested_data = _from_file(xml.getroot()) + + # 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) + return nested_data + + +def _from_file(xml): + """ + Recursively parse the xml tree into nested data format. + + Args: + xml: the xml tree + + Returns: + the nested data + """ + tag = xml.tag + if not len(xml): + return odict({tag: xml.text or ''}) # store empty tags (text is None) as empty string + nested_data = odict() + for elem in xml: + key, value = _from_file(elem).items()[0] + if nested_data.has_key(key): nested_data[key].append(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] + + return odict({tag: nested_data}) + + +def to_file(nested_data, xml_file): + """ + Write to an xml file and insert processing instructions for versioning + + Args: + nested_data: the nested data + xml_file: the xml file path + """ + xml_data = "" + instructions = nested_data.pop('_instructions', None) + if instructions: # create the processing instruction from the array + xml_data += etree.tostring(etree.ProcessingInstruction( + 'grc', ' '.join( + "{0}='{1}'".format(*item) for item in instructions.iteritems()) + ), 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: + fp.write(xml_data) + + +def _to_file(nested_data): + """ + Recursively parse the nested data into xml tree format. + + Args: + nested_data: the nested data + + Returns: + the xml tree filled with child nodes + """ + nodes = list() + for key, values in nested_data.iteritems(): + # 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) + else: + node.extend(_to_file(value)) + nodes.append(node) + return nodes diff --git a/grc/python/base/Platform.py b/grc/python/base/Platform.py new file mode 100644 index 0000000000..367140f8ae --- /dev/null +++ b/grc/python/base/Platform.py @@ -0,0 +1,274 @@ +""" +Copyright 2008-2011 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 sys + +import os + +from .Block import Block as _Block +from .Connection import Connection as _Connection +from .Constants import BLOCK_TREE_DTD, FLOW_GRAPH_DTD, DOMAIN_DTD +from .Element import Element as _Element +from .FlowGraph import FlowGraph as _FlowGraph +from .Param import Param as _Param +from .Port import Port as _Port +from . import ParseXML, odict + + +class Platform(_Element): + def __init__(self, name, version, key, + block_paths, block_dtd, default_flow_graph, generator, + license='', website=None, colors=None): + """ + Make a platform from the arguments. + + Args: + name: the platform name + version: the version string + key: the unique platform key + block_paths: the file paths to blocks in this platform + block_dtd: the dtd validator for xml block wrappers + default_flow_graph: the default flow graph file path + generator: the generator class for this platform + colors: a list of title, color_spec tuples + license: a multi-line license (first line is copyright) + website: the website url for this platform + + Returns: + a platform object + """ + _Element.__init__(self) + self._name = name + # Save the verion string to the first + self._version = version[0] + self._version_major = version[1] + self._version_api = version[2] + self._version_minor = version[3] + self._version_short = version[1] + "." + version[2] + "." + version[3] + + self._key = key + self._license = license + self._website = website + self._block_paths = list(set(block_paths)) + self._block_dtd = block_dtd + self._default_flow_graph = default_flow_graph + self._generator = generator + self._colors = colors or [] + #create a dummy flow graph for the blocks + self._flow_graph = _Element(self) + + self._blocks = None + self._blocks_n = None + self._category_trees_n = None + self._domains = dict() + self._connection_templates = dict() + self.load_blocks() + + def load_blocks(self): + """load the blocks and block tree from the search paths""" + # reset + self._blocks = odict() + self._blocks_n = odict() + self._category_trees_n = list() + 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) + + def iter_xml_files(self): + """Iterator for block descriptions and category trees""" + get_path = lambda x: os.path.abspath(os.path.expanduser(x)) + for block_path in map(get_path, self._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, self._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 "%s" already exists.\n\tIgnoring: %s' % (key, xml_file) + else: # store the block + self._blocks[key] = block + self._blocks_n[key] = n + return block + + def load_category_tree_xml(self, xml_file): + """Validate and parse category tree file and add it to list""" + ParseXML.validate_dtd(xml_file, BLOCK_TREE_DTD) + n = ParseXML.from_file(xml_file).find('cat') + self._category_trees_n.append(n) + + def load_domain_xml(self, xml_file): + """Load a domain properties and connection templates from XML""" + ParseXML.validate_dtd(xml_file, DOMAIN_DTD) + n = ParseXML.from_file(xml_file).find('domain') + + key = n.find('key') + if not key: + print >> sys.stderr, 'Warning: Domain with emtpy key.\n\tIgnoring: %s' % xml_file + return + if key in self.get_domains(): # test against repeated keys + print >> sys.stderr, 'Warning: Domain with key "%s" already exists.\n\tIgnoring: %s' % (key, xml_file) + return + + to_bool = lambda s, d: d if s is None else \ + s.lower() not in ('false', 'off', '0', '') + + 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 "%s" for domain "%s" ' % (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%s' % xml_file + elif key in self._connection_templates: + print >> sys.stderr, 'Warning: Connection template "%s" already exists.\n\t%s' % (key, xml_file) + else: + self._connection_templates[key] = connection_n.find('make') or '' + + 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._default_flow_graph + open(flow_graph_file, 'r') # test open + ParseXML.validate_dtd(flow_graph_file, FLOW_GRAPH_DTD) + return ParseXML.from_file(flow_graph_file) + + def load_block_tree(self, block_tree): + """ + Load a block tree with categories and blocks. + Step 1: Load all blocks from the xml specification. + Step 2: Load blocks with builtin category specifications. + + Args: + block_tree: the block tree object + """ + #recursive function to load categories and blocks + def load_category(cat_n, parent=None): + #add this category + parent = (parent or []) + [cat_n.find('name')] + block_tree.add_block(parent) + #recursive call to load sub categories + map(lambda c: load_category(c, parent), cat_n.findall('cat')) + #add blocks in this category + for block_key in cat_n.findall('block'): + if block_key not in self.get_block_keys(): + print >> sys.stderr, 'Warning: Block key "%s" not found when loading category tree.' % (block_key) + continue + block = self.get_block(block_key) + #if it exists, the block's category shall not be overridden by the xml tree + if not block.get_category(): + block.set_category(parent) + + # recursively load the category trees and update the categories for each block + for category_tree_n in self._category_trees_n: + load_category(category_tree_n) + + #add blocks to block tree + for block in self.get_blocks(): + #blocks with empty categories are hidden + if not block.get_category(): continue + block_tree.add_block(block.get_category(), block) + + def __str__(self): return 'Platform - %s(%s)'%(self.get_key(), self.get_name()) + + def is_platform(self): return True + + def get_new_flow_graph(self): return self.FlowGraph(platform=self) + + def get_generator(self): return self._generator + + ############################################## + # Access Blocks + ############################################## + def get_block_keys(self): return self._blocks.keys() + def get_block(self, key): return self._blocks[key] + 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_domains(self): return self._domains + def get_domain(self, key): return self._domains.get(key) + def get_connection_templates(self): return self._connection_templates + + def get_name(self): return self._name + def get_version(self): return self._version + def get_version_major(self): return self._version_major + def get_version_api(self): return self._version_api + def get_version_minor(self): return self._version_minor + def get_version_short(self): return self._version_short + + def get_key(self): return self._key + def get_license(self): return self._license + def get_website(self): return self._website + def get_colors(self): return self._colors + def get_block_paths(self): return self._block_paths + + ############################################## + # Constructors + ############################################## + FlowGraph = _FlowGraph + Connection = _Connection + Block = _Block + Port = _Port + Param = _Param diff --git a/grc/python/base/Port.py b/grc/python/base/Port.py new file mode 100644 index 0000000000..39166d18f7 --- /dev/null +++ b/grc/python/base/Port.py @@ -0,0 +1,136 @@ +""" +Copyright 2008-2011 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 Element import Element +from . Constants import GR_STREAM_DOMAIN, GR_MESSAGE_DOMAIN + +class Port(Element): + + 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 source or sink + """ + #build the port + Element.__init__(self, block) + #grab the data + self._name = n['name'] + self._key = n['key'] + self._type = n['type'] + self._domain = n['domain'] + self._hide = n.find('hide') or '' + self._dir = dir + self._hide_evaluated = False # updated on rewrite() + + def validate(self): + """ + Validate the port. + The port must be non-empty and type must a possible type. + """ + Element.validate(self) + if self.get_type() not in self.get_types(): + self.add_error_message('Type "%s" is not a possible type.' % self.get_type()) + platform = self.get_parent().get_parent().get_parent() + if self.get_domain() not in platform.get_domains(): + self.add_error_message('Domain key "%s" is not registered.' % self.get_domain()) + + def rewrite(self): + """resolve dependencies in for type and hide""" + Element.rewrite(self) + hide = self.get_parent().resolve_dependencies(self._hide).strip().lower() + self._hide_evaluated = False if hide in ('false', 'off', '0') else bool(hide) + # 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 __str__(self): + if self.is_source(): + return 'Source - %s(%s)'%(self.get_name(), self.get_key()) + if self.is_sink(): + return 'Sink - %s(%s)'%(self.get_name(), self.get_key()) + + def get_types(self): + """ + Get a list of all possible port types. + @throw NotImplementedError + """ + raise NotImplementedError + + def is_port(self): return True + def get_color(self): return '#FFFFFF' + 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 + def is_sink(self): return self._dir == 'sink' + 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().get_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/python/base/__init__.py b/grc/python/base/__init__.py new file mode 100644 index 0000000000..2682db8125 --- /dev/null +++ b/grc/python/base/__init__.py @@ -0,0 +1,20 @@ +""" +Copyright 2009 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 odict import odict diff --git a/grc/python/base/block_tree.dtd b/grc/python/base/block_tree.dtd new file mode 100644 index 0000000000..9e23576477 --- /dev/null +++ b/grc/python/base/block_tree.dtd @@ -0,0 +1,26 @@ +<!-- +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 +--> +<!-- + 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)> diff --git a/grc/python/base/domain.dtd b/grc/python/base/domain.dtd new file mode 100644 index 0000000000..b5b0b8bf39 --- /dev/null +++ b/grc/python/base/domain.dtd @@ -0,0 +1,35 @@ +<!-- +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/python/base/flow_graph.dtd b/grc/python/base/flow_graph.dtd new file mode 100644 index 0000000000..bdfe1dc059 --- /dev/null +++ b/grc/python/base/flow_graph.dtd @@ -0,0 +1,38 @@ +<!-- +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 +--> +<!-- + flow_graph.dtd + Josh Blum + The document type definition for flow graph xml files. + --> +<!ELEMENT flow_graph (timestamp?, block*, connection*)> <!-- optional timestamp --> +<!ELEMENT timestamp (#PCDATA)> +<!-- Block --> +<!ELEMENT block (key, param*, bus_sink?, bus_source?)> +<!ELEMENT param (key, value)> +<!ELEMENT key (#PCDATA)> +<!ELEMENT value (#PCDATA)> +<!ELEMENT bus_sink (#PCDATA)> +<!ELEMENT bus_source (#PCDATA)> +<!-- Connection --> +<!ELEMENT connection (source_block_id, sink_block_id, source_key, sink_key)> +<!ELEMENT source_block_id (#PCDATA)> +<!ELEMENT sink_block_id (#PCDATA)> +<!ELEMENT source_key (#PCDATA)> +<!ELEMENT sink_key (#PCDATA)> diff --git a/grc/python/base/odict.py b/grc/python/base/odict.py new file mode 100644 index 0000000000..70ab67d053 --- /dev/null +++ b/grc/python/base/odict.py @@ -0,0 +1,105 @@ +""" +Copyright 2008-2011 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 "%s" already exists'%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 begining. + + 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 "%s" already exists'%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 self.has_key(key): 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] |