diff --git a/.travis.yml b/.travis.yml index 335452a..c1743ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: -- '3.6' +- '3.7' install: pip install -r requirements.txt script: - pip install -q -U setuptools @@ -8,25 +8,37 @@ script: - pip install -q -U flake8 - pip install -q -U xmldiff - pip install -q -r requirements.txt -- python setup.py install -- flake8 argparse2tool --ignore=E2,E3,E4,E5,W3,W505 -- PYTHONPATH=$(argparse2tool) python examples/example.py --generate_galaxy_xml > tmp.xml +- python3 -m pip install --no-deps --no-cache-dir --force-reinstall . +# temporarily install galaxyxml from branch https://github.com/hexylena/galaxyxml/pull/18 +- git clone -b topic/macros https://github.com/bernt-matthias/galaxyxml.git && cd galaxyxml && python3 -m pip install --no-deps --no-cache-dir --force-reinstall . && cd .. +- flake8 argparse2tool --ignore=C901,E2,E3,E4,E5,W3,W505 +- PYTHONPATH=$(argparse2tool) python examples/example.py --generate_galaxy_xml -m examples/macros.xml > tmp.xml - xmldiff tmp.xml examples/example.xml -- planemo lint --report_level all --fail_level error --xsd tmp.xml +- planemo lint --skip tests --report_level all --fail_level error --xsd tmp.xml # Galaxy tool generation for example with subparsers -- generating one large (invalid) tool - echo '' > tmp-sub.xml # wrap in extra level -- PYTHONPATH=$(argparse2tool) python examples/example-sub.py --generate_galaxy_xml >> tmp-sub.xml +- PYTHONPATH=$(argparse2tool) python examples/example-sub.py --generate_galaxy_xml -m examples/macros.xml >> tmp-sub.xml - echo '' >> tmp-sub.xml - xmldiff tmp-sub.xml <(echo ""; cat examples/example-sub.xml; echo "") -# Galaxy tool generation for example with subparsers -- generating separate tools -- PYTHONPATH=$(argparse2tool) python examples/example-sub.py --generate_galaxy_xml --command foo > tmp-sub-foo.xml -- PYTHONPATH=$(argparse2tool) python examples/example-sub.py --generate_galaxy_xml --command bar > tmp-sub-bar.xml -- xmldiff tmp-sub-foo.xml examples/example-sub-foo.xml -- planemo lint --report_level all --fail_level error --xsd tmp-sub-foo.xml -- xmldiff tmp-sub-bar.xml examples/example-sub-bar.xml -- planemo lint --report_level all --fail_level error --xsd tmp-sub-bar.xml +# Galaxy tool generation for example with subparsers -- generating separate tools (this does not generate macro xml files automatically) +- PYTHONPATH=$(argparse2tool) python examples/example-sub.py --generate_galaxy_xml -m examples/macros.xml --command foo > tmp-sub-foo.xml +- PYTHONPATH=$(argparse2tool) python examples/example-sub.py --generate_galaxy_xml -m examples/macros.xml --command bar > tmp-sub-bar.xml +- xmldiff tmp-sub-foo.xml examples/example-sub/example_sub_foo.xml +- xmldiff tmp-sub-bar.xml examples/example-sub/example_sub_bar.xml + +# Galaxy tool generation for example with subparsers -- generating all tools and macros at once also include multiple macro files +- mkdir tmp && cp examples/macros*xml tmp/ +- PYTHONPATH=$(argparse2tool) python examples/example-sub.py --generate_galaxy_xml -m examples/macros.xml -m examples/macros_tests.xml --directory tmp +- ls tmp/ +- xmldiff tmp/example_sub_foo.xml examples/example-sub/example_sub_foo.xml +- planemo lint --skip tests --report_level all --fail_level error --xsd tmp/example_sub_foo.xml +- xmldiff tmp/example_sub_bar.xml examples/example-sub/example_sub_bar.xml +- planemo lint --skip tests --report_level all --fail_level error --xsd tmp/example_sub_bar.xml +- xmldiff tmp/example_sub_baz.xml examples/example-sub/example_sub_baz.xml +- xmldiff tmp/example_sub_qux.xml examples/example-sub/example_sub_qux.xml + - PYTHONPATH=$(argparse2tool) python examples/example.py --generate_cwl_tool > tmp.cwl - PYTHONPATH=$(argparse2tool) python examples/example-sub.py --generate_cwl_tool > tmp-sub.cwl @@ -37,7 +49,6 @@ script: - diff tmp-click.cwl examples/example-click.cwl before_deploy: - sed -i "s/__version__.*/__version__= '${TRAVIS_TAG}'/g" argparse2tool/__init__.py -deploy: deploy: provider: pypi username: __token__ diff --git a/argparse2tool/__init__.py b/argparse2tool/__init__.py index f87bc88..0245d43 100644 --- a/argparse2tool/__init__.py +++ b/argparse2tool/__init__.py @@ -48,3 +48,11 @@ def load_conflicting_package(name, not_name, module_number): imp.load_module(random_name, f, pathname, desc) return sys.modules[random_name] return None + + +def remove_extension(name): + if name is not None: + name = name.replace('.py', '') + name = name.replace('-', '_') + name = name.replace(' ', '_') + return name diff --git a/argparse2tool/cmdline2gxml/__init__.py b/argparse2tool/cmdline2gxml/__init__.py index 81a0e9d..050157a 100644 --- a/argparse2tool/cmdline2gxml/__init__.py +++ b/argparse2tool/cmdline2gxml/__init__.py @@ -23,5 +23,9 @@ def __init__(self): def process_arguments(self): self.parser.add_argument('--generate_galaxy_xml', action='store_true') + self.parser.add_argument('-d', '--directory', + help='Directory to store tool descriptions') + self.parser.add_argument('-m', '--macro', action="append", + help='A global macro file to include in all tools') self.parser.add_argument('--command', action='store', default="") return vars(self.parser.parse_args()) diff --git a/argparse2tool/dropins/argparse/__init__.py b/argparse2tool/dropins/argparse/__init__.py index 616267a..9f11d5e 100644 --- a/argparse2tool/dropins/argparse/__init__.py +++ b/argparse2tool/dropins/argparse/__init__.py @@ -1,6 +1,11 @@ +import os.path import re +import shutil import sys -from argparse2tool import load_argparse +from argparse2tool import ( + load_argparse, + remove_extension +) from argparse2tool.cmdline2gxml import Arg2GxmlParser from argparse2tool.cmdline2cwl import Arg2CWLParser @@ -21,6 +26,15 @@ setattr(__selfmodule__, x, getattr(ap, x)) tools = [] +# set of prog names where the argparser actually should be represented +# as a macro, i.e. XYZ will be in the set if there is a +# ArgumentParser(..., parents=[XYZ], ...) +macros = set() + +# mapping tools to list of required macros (which are actually identical to the +# parents parameter passed ArgumentParser. but I could not find out how this is +# stored in the created ArgumentParser objects +used_macros = dict() class ArgumentParser(ap.ArgumentParser): @@ -37,10 +51,20 @@ def __init__(self, argument_default=None, conflict_handler='error', add_help=True): + global macros + global used_macros self.argument_list = [] self.argument_names = [] tools.append(self) + if len(parents) > 0: + p = set([remove_extension(_.prog) for _ in parents]) + macros = macros.union(p) + used_macros[remove_extension(prog)] = p + + if '--generate_galaxy_xml' in sys.argv: + parents = [] + super(ArgumentParser, self).__init__(prog=prog, usage=usage, description=description, @@ -109,6 +133,8 @@ def parse_args_cwl(self, *args, **kwargs): tool.inputs.append(cwlt_parameter) if isinstance(cwlt_parameter, cwlt.OutputParam): tool.outputs.append(cwlt_parameter) + else: + print("%s not implemented (%s)" % (argument_type, result.option_strings), file=sys.stderr) if argp.epilog is not None: tool.description += argp.epilog @@ -128,6 +154,31 @@ def parse_args_cwl(self, *args, **kwargs): sys.exit(0) def parse_args_galaxy(self, *args, **kwargs): + global used_macros + + directory = kwargs.get('directory', None) + macro = kwargs.get('macro', None) + + # copy macros to destination dir + if directory and macro: + for i, m in enumerate(macro): + macro[i] = os.path.basename(m) + shutil.copyfile(m, os.path.join(directory, macro[i])) + + # since macros can also make use of macros (i.e. the parent relation + # specified in the arguments can be nester) we need to extend the + # used macros such that really all are included + ext_used_macros = dict() + for tool, macros in used_macros.items(): + ext_used_macros[tool] = set() + q = list(macros) + while len(q) > 0: + m = q.pop() + if m in used_macros: + q.extend(used_macros[m]) + ext_used_macros[tool].add(m) + used_macros = ext_used_macros + for argp in tools: # make subparser description out of its help message if argp._subparsers: @@ -139,53 +190,119 @@ def parse_args_galaxy(self, *args, **kwargs): subparser.choices[choice_action.dest].description = choice_action.help else: if kwargs.get('command', argp.prog) in argp.prog: - data = self._parse_args_galaxy_argp(argp) - print(data) + data = self._parse_args_galaxy_argp(argp, macro) + if directory: + if directory[-1] != '/': + directory += '/' + filename = remove_extension(argp.prog) + ".xml" + filename = directory + filename + with open(filename, 'w') as f: + f.write(data) + else: + print(data) else: continue sys.exit(0) - def _parse_args_galaxy_argp(self, argp): + def _parse_args_galaxy_argp(self, argp, macro): + global macros + global used_macros + try: version = self.print_version() or '1.0' except AttributeError: # handle the potential absence of print_version version = '1.0' - tool = gxt.Tool(argp.prog, - argp.prog.replace(" ", "_"), - version, - argp.description, - "python "+argp.prog, - interpreter=None, - version_command='python %s --version' % argp.prog) + print("_parse_args_galaxy_argp _subparsers %s" % argp._subparsers) + prog = remove_extension(argp.prog) - inputs = gxtp.Inputs() - outputs = gxtp.Outputs() + # tid = argp.prog.split()[-1] + + # get the list of file names of the used macros + mx = used_macros.get(prog, []) + mx = sorted(["%s.xml" % _.split(" ")[-1] for _ in mx]) + + if prog not in macros: + tpe = gxt.Tool + if macro: + mx.extend(macro) + else: + tpe = gxt.MacrosTool + + tool = tpe(prog, + prog.replace(" ", "_"), + version, + argp.description, + "python "+argp.prog, + interpreter=None, + version_command='python %s --version' % argp.prog, + macros=mx) + + inputs = tool.inputs + outputs = tool.outputs + sections = dict() at = agt.ArgparseGalaxyTranslation() - # Only build up arguments if the user actually requests it - for result in argp.argument_list: + + for group in argp._action_groups: + if group in [argp._positionals, argp._optionals]: + continue + argument_type = group.__class__.__name__ + methodToCall = getattr(at, argument_type) + sections[group] = methodToCall(group, tool=tool) + + for action in argp._actions: # I am SO thankful they return the argument here. SO useful. - argument_type = result.__class__.__name__ + argument_type = action.__class__.__name__ # http://stackoverflow.com/a/3071 if hasattr(at, argument_type): methodToCall = getattr(at, argument_type) - gxt_parameter = methodToCall(result, tool=tool) - if gxt_parameter is not None: - if isinstance(gxt_parameter, gxtp.InputParameter): - inputs.append(gxt_parameter) - else: - outputs.append(gxt_parameter) + gxt_parameter = methodToCall(action, tool=tool) + ## print(action, gxt_parameter) + print(action.option_strings, action.container) + if gxt_parameter is None: # e.g. for help and version actions + continue + if not isinstance(gxt_parameter, gxtp.InputParameter): + outputs.append(gxt_parameter) + elif action.container in sections: + sections[action.container].append(gxt_parameter) + else: + inputs.append(gxt_parameter) + else: + print("%s not implemented (%s)" % (argument_type, action.option_strings), file=sys.stderr) + + for section in sections: + inputs.append(sections[section]) + - # TODO: replace with argparse-esque library to do this. - stdout = gxtp.OutputData('default', 'txt') - stdout.command_line_override = '> $default' - outputs.append(stdout) +# print("argp._action_groups %s" % argp._action_groups) +# # Only build up arguments if the user actually requests it +# for result in argp.argument_list: +# # I am SO thankful they return the argument here. SO useful. +# argument_type = result.__class__.__name__ +# print("_parse_args_galaxy_argp %s type %s [%s]" % (getattr(result, "title", "no title"), argument_type, hasattr(at, argument_type))) +# # http://stackoverflow.com/a/3071 +# if hasattr(at, argument_type): +# methodToCall = getattr(at, argument_type) +# gxt_parameter = methodToCall(result, tool=tool) +# if gxt_parameter is not None: +# if isinstance(gxt_parameter, gxtp.InputParameter): +# inputs.append(gxt_parameter) +# else: +# outputs.append(gxt_parameter) + + if prog in used_macros: + for m in sorted(used_macros[prog]): + inputs.append(gxtp.ExpandIO(m + "_inmacro")) + outputs.append(gxtp.ExpandIO(m + "_outmacro")) + + if prog not in macros: + # TODO: replace with argparse-esque library to do this. + stdout = gxtp.OutputData('default', 'txt') + stdout.command_line_override = '> $default' + outputs.append(stdout) - tool.inputs = inputs - tool.outputs = outputs if argp.epilog is not None: tool.help = argp.epilog else: tool.help = "TODO: Write help" - return tool.export() diff --git a/argparse2tool/dropins/argparse/argparse_cwl_translation.py b/argparse2tool/dropins/argparse/argparse_cwl_translation.py index 11950f6..9fa4dbc 100644 --- a/argparse2tool/dropins/argparse/argparse_cwl_translation.py +++ b/argparse2tool/dropins/argparse/argparse_cwl_translation.py @@ -77,6 +77,12 @@ def __args_from_nargs(self, param): return param + def _VersionAction(self, param, tool=None): + pass + + def _HelpAction(self, param, tool=None): + pass + def _StoreAction(self, param): param = self.__args_from_nargs(param) cwlparam = self.__cwl_param_from_type(param) @@ -110,6 +116,11 @@ def __StoreBoolAction(self, param): cwlparam = self.__cwl_param_from_type(param) return cwlparam + def _StoreConstAction(self, param): + param.type = bool + cwlparam = self.__cwl_param_from_type(param) + return cwlparam + @staticmethod def get_cwl_type(py_type): """ diff --git a/argparse2tool/dropins/argparse/argparse_galaxy_translation.py b/argparse2tool/dropins/argparse/argparse_galaxy_translation.py index 7898967..2345108 100644 --- a/argparse2tool/dropins/argparse/argparse_galaxy_translation.py +++ b/argparse2tool/dropins/argparse/argparse_galaxy_translation.py @@ -1,7 +1,7 @@ -import galaxyxml.tool.parameters as gxtp from collections import Counter from pydoc import locate +import galaxyxml.tool.parameters as gxtp class ArgparseGalaxyTranslation(object): @@ -30,7 +30,7 @@ def __gxtp_param_from_type(self, param, flag, label, num_dashes, gxparam_extra_k gxparam = gxtp.DataParam(flag, label=label, num_dashes=num_dashes, **gxparam_extra_kwargs) elif isinstance(param.type, FileType): if 'w' in param.type._mode: - gxparam = gxtp.OutputParameter( + gxparam = gxtp.OutputData( flag, format='data', default=default, label=label, num_dashes=num_dashes, **gxparam_extra_kwargs ) @@ -119,7 +119,7 @@ def __args_from_nargs(self, param, repeat_name, repeat_var_name, positional, fla gxrepeat_cli_after = '' gxrepeat_cli_before = """\n#set %s = '" "'.join([ str($var.%s) for $var in $%s ])""" % (repeat_var_name, flag, repeat_name) else: - raise Exception("TODO: Handle argparse.REMAINDER") + raise Exception("Unknown nargs value %s" % param.nargs) return (gxrepeat_args, gxrepeat_kwargs, gxrepeat_cli_after, gxrepeat_cli_before, gxrepeat_cli_actual, gxparam_cli_before, gxparam_cli_after) @@ -138,6 +138,9 @@ def _VersionAction(self, param, tool=None): # Count the repeats for unique names # TODO improve + def _HelpAction(self, param, tool=None): + pass + def _StoreAction(self, param, tool=None): """ Parse argparse arguments action type of "store", the default. @@ -148,7 +151,6 @@ def _StoreAction(self, param, tool=None): gxrepeat = None self.repeat_count += 1 gxparam_extra_kwargs = {} - if not param.required: gxparam_extra_kwargs['optional'] = True @@ -247,3 +249,8 @@ def _StoreConstAction(self, param, **kwargs): gxparam = gxtp.BooleanParam(flag_wo_dashes, label=param.help, num_dashes=num_dashes) return gxparam + + def _ArgumentGroup(self, param, **kwargs): + return gxtp.Section(name= param.title.replace(" ", "_"), + title=param.title, + help=param.description) diff --git a/examples/example-sub.cwl b/examples/example-sub.cwl index db95e46..fcd0d41 100644 --- a/examples/example-sub.cwl +++ b/examples/example-sub.cwl @@ -1,5 +1,73 @@ #!/usr/bin/env cwl-runner -# This tool description was generated automatically by argparse2tool +# This tool description was generated automatically by argparse2tool ver. 0.4.9 +# To generate again: $ example-sub.py --generate_cwl_tool +# Help: $ example --help_arg2cwl + +cwlVersion: v1.0 + +class: CommandLineTool +baseCommand: ['example-sub.py', 'qux'] + +doc: | + None + +inputs: + + qux: + type: + - "null" + - type: array + items: string + + doc: qux help + inputBinding: + prefix: --qux + + +outputs: + [] + +#!/usr/bin/env cwl-runner +# This tool description was generated automatically by argparse2tool ver. 0.4.9 +# To generate again: $ example-sub.py --generate_cwl_tool +# Help: $ example --help_arg2cwl + +cwlVersion: v1.0 + +class: CommandLineTool +baseCommand: ['example-sub.py', 'baz'] + +doc: | + None + +inputs: + + qux: + type: + - "null" + - type: array + items: array + + doc: qux help + inputBinding: + prefix: --qux + + baz: + type: + - "null" + - type: array + items: string + + doc: baz help + inputBinding: + prefix: --baz + + +outputs: + [] + +#!/usr/bin/env cwl-runner +# This tool description was generated automatically by argparse2tool ver. 0.4.9 # To generate again: $ example-sub.py --generate_cwl_tool # Help: $ example --help_arg2cwl @@ -13,6 +81,26 @@ doc: | inputs: + qux: + type: + - "null" + - type: array + items: array + + doc: qux help + inputBinding: + prefix: --qux + + baz: + type: + - "null" + - type: array + items: array + + doc: baz help + inputBinding: + prefix: --baz + keyword: type: type: array @@ -31,6 +119,13 @@ inputs: inputBinding: position: 2 + accumulate: + type: ["null", boolean] + default: + doc: sum the integers (default - find the max) + inputBinding: + prefix: -s + foo: type: ["null", string] doc: foo help @@ -60,7 +155,7 @@ outputs: [] #!/usr/bin/env cwl-runner -# This tool description was generated automatically by argparse2tool +# This tool description was generated automatically by argparse2tool ver. 0.4.9 # To generate again: $ example-sub.py --generate_cwl_tool # Help: $ example --help_arg2cwl @@ -74,6 +169,16 @@ doc: | inputs: + qux: + type: + - "null" + - type: array + items: array + + doc: qux help + inputBinding: + prefix: --qux + false: type: ["null", boolean] default: True diff --git a/examples/example-sub.py b/examples/example-sub.py index b5da603..51fdd5c 100644 --- a/examples/example-sub.py +++ b/examples/example-sub.py @@ -3,30 +3,31 @@ parent = argparse.ArgumentParser(description='Process some integers.', prefix_chars='-+', epilog="here's some epilog text", formatter_class=argparse.ArgumentDefaultsHelpFormatter) - subparsers = parent.add_subparsers() -parser_foo = subparsers.add_parser('foo') -parser_bar = subparsers.add_parser('bar') +parser_qux = subparsers.add_parser('qux', add_help=False) +parser_qux.add_argument('--qux', metavar='QUX', type=str, nargs=1, help='qux help') -parser_foo.add_argument('keyword', metavar='Q', type=str, nargs=1, help='action keyword') +parser_baz = subparsers.add_parser('baz', parents=[parser_qux], add_help=False) +parser_baz.add_argument('--baz', metavar='BAZ', type=str, nargs=1, help='baz help') +parser_foo = subparsers.add_parser('foo', parents=[parser_baz]) +parser_foo.add_argument('keyword', metavar='Q', type=str, nargs=1, + help='action keyword') parser_foo.add_argument('integers', metavar='N', type=int, nargs='+', - help='an integer for the accumulator') - + help='an integer for the accumulator') parser_foo.add_argument('--sum', '-s', dest='accumulate', action='store_const', - const=sum, default=max, help='sum the integers (default: find the max)') - + const=sum, default=max, + help='sum the integers (default: find the max)') parser_foo.add_argument('--foo', nargs='?', help='foo help') parser_foo.add_argument('--bar', nargs='*', default=[1, 2, 3], help='BAR!') parser_foo.add_argument('--true', action='store_true', help='Store a true') + +parser_bar = subparsers.add_parser('bar', parents=[parser_qux]) parser_bar.add_argument('--false', action='store_false', help='Store a false') parser_bar.add_argument('--append', action='append', help='Append a value') - parser_bar.add_argument('--nargs2', nargs=2, help='nargs2') - parser_bar.add_argument('--mode', choices=['rock', 'paper', 'scissors'], default='scissors') - - parser_bar.add_argument('--version', action='version', version='2.0') + args = parent.parse_args() diff --git a/examples/example-sub.xml b/examples/example-sub.xml index 56dae02..980580f 100644 --- a/examples/example-sub.xml +++ b/examples/example-sub.xml @@ -1,8 +1,35 @@ - + + + + + + + + + + + + + + + + + + + + + + + - - - + + example_sub_baz.xml + example_sub_qux.xml + examples/macros.xml + + + $default]]> - - - + + + - - - - + + + + - + + + - + + - + - - - - python example-sub.py bar --version + + example_sub_qux.xml + examples/macros.xml + + + + $default]]> - - - + + + - - + + - + + - + + + diff --git a/examples/example-sub-bar.xml b/examples/example-sub/example_sub_bar.xml similarity index 53% rename from examples/example-sub-bar.xml rename to examples/example-sub/example_sub_bar.xml index e2462ee..386e986 100644 --- a/examples/example-sub-bar.xml +++ b/examples/example-sub/example_sub_bar.xml @@ -1,8 +1,12 @@ - + - - - + + example_sub_qux.xml + examples/macros.xml + examples/macros_tests.xml + + + $default]]> - - + + - + + - + + - diff --git a/examples/example-sub/example_sub_baz.xml b/examples/example-sub/example_sub_baz.xml new file mode 100644 index 0000000..debf8e3 --- /dev/null +++ b/examples/example-sub/example_sub_baz.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/examples/example-sub-foo.xml b/examples/example-sub/example_sub_foo.xml similarity index 53% rename from examples/example-sub-foo.xml rename to examples/example-sub/example_sub_foo.xml index 8eacac6..46c7b71 100644 --- a/examples/example-sub-foo.xml +++ b/examples/example-sub/example_sub_foo.xml @@ -1,8 +1,13 @@ - + - - - + + example_sub_baz.xml + example_sub_qux.xml + examples/macros.xml + examples/macros_tests.xml + + + $default]]> - + - + - + + + - + + - diff --git a/examples/example-sub/example_sub_qux.xml b/examples/example-sub/example_sub_qux.xml new file mode 100644 index 0000000..264e043 --- /dev/null +++ b/examples/example-sub/example_sub_qux.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/examples/example.xml b/examples/example.xml index ccaba3f..5839e5b 100644 --- a/examples/example.xml +++ b/examples/example.xml @@ -1,8 +1,10 @@ - + Process some integers. - - - + + examples/macros.xml + + + $default]]> - - - + + + - - - - + + + + - - - - + + + + - - + + - + - + + diff --git a/examples/macros.xml b/examples/macros.xml new file mode 100644 index 0000000..9bd6ed4 --- /dev/null +++ b/examples/macros.xml @@ -0,0 +1,27 @@ + + + 4.4 + 0 + + + bash + + + + + + + + + + + + + doi:10.1093/nar/gkw343 + + + + + + + diff --git a/examples/macros_tests.xml b/examples/macros_tests.xml new file mode 100644 index 0000000..ba6ea77 --- /dev/null +++ b/examples/macros_tests.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + +