Source code for clictk.model

import xml.etree.ElementTree as ET
import subprocess
from itertools import chain

__all__ = ["Parameter", "ParameterGroup", "Executable", "XMLArgumentNotSupportedByExecutable"]


[docs]class ParameterGroup(object): """A list of multiple :py:class:`Parameter`. XML: Starts a group of parameters. """ def __init__(self, label=None, description=None, advanced=None, parameters=None): self.parameters = parameters or list() """ :type: list[Parameter] """ self.label = label """Label text of the parameter section <xsd:documentation>A short string used as the label for this group.</xsd:documentation> :type: str """ self.description = description """Description text of this parameter section <xsd:documentation>A description of this parameter group.</xsd:documentation> :type: str """ self.advanced = advanced """marks if this paramter section is marked as advanced. XML:: This value is usually used in GUI generators to decide if the parameters belonging to this group should be initially hidden to the user or not. :type: bool """
[docs] def as_xml(self): root = ET.Element("parameters", {'advanced': str(self.advanced).lower()}) ET.SubElement(root, "label").text = str(self.label) ET.SubElement(root, "description").text = str(self.description) for p in self.parameters: root.append(p.as_xml()) return root
def __repr__(self): return "ParameterGroup(label=%(label)r, description=%(description)r, advanced=%(advanced)r, parameters=%(parameters)s )" % \ self.__dict__
[docs] def append(self, parameter): self.parameters.append(parameter)
def __getitem__(self, item): if isinstance(item, str): for p in self: if p.name == item: return p raise KeyError("Could not find Parameter %s" % item) else: return self.parameters.__getitem__(item) def __setitem__(self, key, value): return self.parameters.__setitem__(key, value) def __setslice__(self, i, j, sequence): return self.parameters.__setslice__(i, j, sequence) def __iter__(self): if self.parameters: return iter(self.parameters) return iter(tuple()) def __getslice__(self, i, j): return self.parameters.__getslice__(i, j) def __eq__(self, other): if isinstance(other, ParameterGroup) and other is not None: try: l = True for p in self.parameters: po = other[p.name] l = l and po == p for p in other.parameters: po = self[p.name] l = l and po == p except KeyError: l = False return l and self.advanced == other.advanced and self.label == other.label and \ self.description == other.description else: return False
[docs]class Parameter(object): """Represents a CLI parameter. You must either specify "flag" or "longflag" (or both) or "index". """ XML_FIELDS = ('name', 'index', 'flag', 'longflag', 'description', 'label', 'default', 'channel', 'hidden') def __init__(self, name, type, default, description=None, channel=None, values=None, flag=None, index=None, label=None, longflag=None, file_ext=None): self.name = name """The unique name (within this module) of the parameter. This is only used internally. Pattern:: [_a-zA-Z][_a-zA-Z0-9]* :type: str """ self.type = type """type of this parameter (interger, float, ... ) :type: str """ self.default = default """A default value for the parameter. The default must be a type that is compatible with the parameter type. The vector parameters are specified as comma separated values of the atomic parameter type. :type: str """ self.description = description or " " """A brief description of the parameter. :type: str """ self.hidden = False """not documentated :type: bool """ self.channel = channel """Specifies whether the parameter is an input or output parameter. Output parameters can for example specify file paths where to write output data (e.g. using the "image" element) or they can represent "simple return parameters", indicated by providing an "index" of 1000. The current values of suche simple return parameters are not passed to the module during its execution. Rather, the module itself reports these parameter values during execution. :type: str """ self.values = values """Enumerable values :type: str """ self.flag = flag """ :type: str """ self.label = label or name """ label for parameter. :type: str """ self.index = index """An integer starting at 0, that specifies a module argument that has no flags. The index value 1000 is reserved as a marker for output parameters (see the "channel" element) to indicate that this parameter is used to return results during the execution of this module and does not need to be set. :type: int """ self.longflag = longflag """long flag :type: str """ self.file_ext = file_ext """acceptable file extensions :type: str """ def __repr__(self): return "Parameter(name=%(name)r, type=%(type)r, default=%(default)r, " \ "description=%(description)r, channel=%(channel)r, values=%(values)r," \ "index=%(index)r, label=%(label)r, longflag=%(longflag)r, file_ext=%(file_ext)r)" % self.__dict__ def __eq__(self, other): if isinstance(other, Parameter): return self.__dict__ == other.__dict__ return False
[docs] def as_xml(self): root = ET.Element(self.type, {'fileExtension': self.file_ext} if self.file_ext else {}) for attrib in Parameter.XML_FIELDS: value = getattr(self, attrib) if value: xml_name = attrib.replace("_", "-") e = ET.SubElement(root, xml_name) e.text = str(value) return root
@staticmethod
[docs] def from_xml_node(xml_node): """constructs a CLI.Parameter from an xml node. :param xml_node: :type xml_node: xml.etree.ElementTree.Element :rtype: Executable.Parameter :return: """ def gather_enum_values(): l = [] for element in xml_node.iterfind('element'): l.append(element.text) return l name = xml_node.findtext("name") type = xml_node.tag if type in ("label", "description"): return None default = xml_node.findtext("default") longflag = xml_node.findtext('longflag') if default: default = default.replace('"', '').replace("'", '') index = xml_node.findtext('index') label = xml_node.findtext('label') or name or longflag doc = xml_node.findtext('description') values = gather_enum_values() channel = xml_node.findtext('channel') file_ext = xml_node.attrib.get('fileExtensions', None) return Parameter(name, type, default, doc, channel, values=values, index=index, label=label, longflag=longflag, file_ext=file_ext)
[docs]class Executable(object): """Represents an CLI executable. The root element for each module XML description. It must contain at least one "parameters" element. """ def __init__(self, xml=None, executable=None, category=None, title=None, description=None, version=None, license=None, contributor=None, acknowledgements=None, documentation_url=None, parameter_groups=None): """Creates a model about every fact from the CLI XML :param xml: :type xml: xml.etree.ElementTree.ElementTree :return: """ self._xml = xml """XML datastructure :type: xml.etree.ElementTree.ElementTree """ self.executable = executable """Path to the executable :type: str """ self.parameter_groups = parameter_groups or [] """list of :py:class:`Parameters` :type: list[Parameters] """ self.category = category """Classifies the module (e.g. Filtering, Segmentation). The value can be a dot separated string to create category hierarchies. :type: str """ self.title = title """A human-readable name for the module **required** for valid xml! :type: str """ self.description = description or str() """A detailed description of the modules purpose **required** for valid xml! :type: str """ self.version = version """The modules version number. A suggested format is: `major.minor.patch.build.status` * vc: version controlled (pre-alpha), build can be a serial revision number, if any (like svn might have). * a: alpha * b: beta * rc: release candidate * fcs: first customer ship :type: str """ self.license = license """The type of license or a URL containing the license :type: str """ self.contributor = contributor """The author(s) of the command line module :type: str """ self.documentation_url = documentation_url """A URL pointing to a documentation or home page of the module. :type: str """ self.acknowledgements = acknowledgements """Acknowledgements for funding agency, employer, colleague, etc :type: str """ @property def name(self): return self.title or self.executable XML_FIELDS = ('category', 'title', 'description', 'version', 'documentation_url', 'license', 'contributor', 'acknowledgements')
[docs] def as_xml(self): root = ET.Element("executable") for attrib in self.XML_FIELDS: value = getattr(self, attrib) if value: xml_name = attrib.replace("_", "-") se = ET.SubElement(root, xml_name) se.text = str(value) for element in self.parameter_groups: root.append(element.as_xml()) return root
def __iter__(self): """Iterate over all parameters in this executable :returns: Iterable """ return chain(*self.parameter_groups) def __repr__(self): return "Executable(executable=%(executable)r, category=%(category)r, title=%(title)r, description=%(description)r, version=%(version)r," \ "license=%(license)r, contributor=%(contributor)r, acknowledgements=%(acknowledgements)r, documentation_url=%(documentation_url)r," \ "parameter_groups=%(parameter_groups)r)" % self.__dict__
[docs] def cmdline(self, **kwargs): """Generates a valid command line call for the executable given by call arguments in `kwargs` :param kwargs: values to call the executable :return: the command line :rtype: list """ args = [self.executable] indexed_args = [] for key, value in kwargs.iteritems(): parameter = self[key] if value != parameter.default: no_value = parameter.type == 'boolean' if parameter.longflag: # use longflag where possible args.append("--%s" % parameter.longflag) if no_value: args.append(str(value)) elif parameter.flag: args.append("-%s" % parameter.flag) if no_value: args.append(str(value)) elif parameter.index: indexed_args.insert(int(parameter.index), str(value)) return args + indexed_args
[docs] def run(self, **kwargs): """runs the executable :param kwargs: :return: a dictionary with the output channels values """ # subprocess.Popen... pass
def __getitem__(self, item): for p in self: if p.name == item: return p raise KeyError("Parameter %s not found" % item) def __eq__(self, other): if isinstance(other, Executable) and other is not None: return all((other.executable == self.executable, other.version == self.version, other.acknowledgements == self.acknowledgements, other.contributor == self.contributor, other.documentation_url == self.documentation_url, other.description == self.description, other.title == self.title, other.parameter_groups == self.parameter_groups)) else: return False @staticmethod
[docs] def from_exe(executable): """ :param executable: :return: """ command = "%s --xml" % executable sp = subprocess.Popen(command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True) sp.wait() if sp.returncode == 0: xml = sp.stdout.read() tree = ET.fromstring(xml) e = Executable.from_etree(tree) e.executable = executable return e else: raise XMLArgumentNotSupportedByExecutable("called %s got %s" % (command, sp.returncode))
@staticmethod
[docs] def from_xml(filename): """ :param filename: :return: """ tree = ET.parse(filename) return Executable.from_etree(tree)
@staticmethod
[docs] def from_etree(tree): """Constructs an executable form a given ElementTree structure. :param tree: :type tree: xml.etree.ElementTree.ElementTree :rtype: Executable """ exe = Executable(tree) exe.category = tree.findtext('category') exe.version = tree.findtext('version') exe.title = tree.findtext('title') or exe.name exe.description = tree.findtext('description') exe.license = tree.findtext('license') or "unknown" exe.contributor = tree.findtext('contributor') for ps in tree.iterfind("parameters"): assert isinstance(ps, ET.Element) paras = ParameterGroup( ps.findtext("label"), ps.findtext("description"), ps.attrib.get('advanced', "false") == "true", filter(lambda x: x is not None, map(Parameter.from_xml_node, list(ps)))) exe.parameter_groups.append(paras) return exe
[docs]class XMLArgumentNotSupportedByExecutable(Exception): """ """ pass