def _get_args_from_signature(function): if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False): return spec = get_arg_spec(function) defaults = dict( zip(*[reversed(x) for x in (spec.args, spec.defaults or [])])) defaults.update(getattr(spec, 'kwonlydefaults', None) or {}) kwonly = getattr(spec, 'kwonlyargs', []) if sys.version_info < (3, 0): annotations = {} else: annotations = dict((k, v) for k, v in function.__annotations__.items() if isinstance(v, str)) # define the list of conflicting option strings # (short forms, i.e. single-character ones) chars = [a[0] for a in spec.args + kwonly] char_counts = dict((char, chars.count(char)) for char in set(chars)) conflicting_opts = tuple(char for char in char_counts if 1 < char_counts[char]) for name in spec.args + kwonly: flags = [] # name_or_flags akwargs = {} # keyword arguments for add_argument() if name in annotations: # help message: func(a : "b") -> add_argument("a", help="b") akwargs.update(help=annotations.get(name)) if name in defaults or name in kwonly: if name in defaults: akwargs.update(default=defaults.get(name)) else: akwargs.update(required=True) flags = ('-{0}'.format(name[0]), '--{0}'.format(name)) if name.startswith(conflicting_opts): # remove short name flags = flags[1:] else: # positional argument flags = (name, ) # cmd(foo_bar) -> add_argument('foo-bar') flags = tuple(x.replace('_', '-') for x in flags) yield dict(option_strings=flags, **akwargs) if spec.varargs: # *args yield dict(option_strings=[spec.varargs], nargs='*')
def _get_args_from_signature(function): if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False): return spec = get_arg_spec(function) defaults = dict(zip(*[reversed(x) for x in (spec.args, spec.defaults or [])])) defaults.update(getattr(spec, 'kwonlydefaults', None) or {}) kwonly = getattr(spec, 'kwonlyargs', []) if sys.version_info < (3,0): annotations = {} else: annotations = dict((k,v) for k,v in function.__annotations__.items() if isinstance(v, str)) # define the list of conflicting option strings # (short forms, i.e. single-character ones) chars = [a[0] for a in spec.args + kwonly] char_counts = dict((char, chars.count(char)) for char in set(chars)) conflicting_opts = tuple(char for char in char_counts if 1 < char_counts[char]) for name in spec.args + kwonly: flags = [] # name_or_flags akwargs = {} # keyword arguments for add_argument() if name in annotations: # help message: func(a : "b") -> add_argument("a", help="b") akwargs.update(help=annotations.get(name)) if name in defaults or name in kwonly: if name in defaults: akwargs.update(default=defaults.get(name)) else: akwargs.update(required=True) flags = ('-{0}'.format(name[0]), '--{0}'.format(name)) if name.startswith(conflicting_opts): # remove short name flags = flags[1:] else: # positional argument flags = (name,) # cmd(foo_bar) -> add_argument('foo-bar') flags = tuple(x.replace('_', '-') for x in flags) yield dict(option_strings=flags, **akwargs) if spec.varargs: # *args yield dict(option_strings=[spec.varargs], nargs='*')
def _call(): # Actually call the function if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False): result = function(namespace_obj) else: # namespace -> dictionary _flat_key = lambda key: key.replace('-', '_') all_input = dict( (_flat_key(k), v) for k, v in vars(namespace_obj).items()) # filter the namespace variables so that only those expected # by the actual function will pass spec = get_arg_spec(function) positional = [all_input[k] for k in spec.args] kwonly = getattr(spec, 'kwonlyargs', []) keywords = dict((k, all_input[k]) for k in kwonly) # *args if spec.varargs: positional += getattr(namespace_obj, spec.varargs) # **kwargs varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', [])) if varkw: not_kwargs = [DEST_FUNCTION] + spec.args + [spec.varargs ] + kwonly for k in vars(namespace_obj): if k.startswith('_') or k in not_kwargs: continue keywords[k] = getattr(namespace_obj, k) result = function(*positional, **keywords) # Yield the results if isinstance(result, (GeneratorType, list, tuple)): # yield each line ASAP, convert CommandError message to a line for line in result: yield line else: # yield non-empty non-iterable result as a single line if result is not None: yield result
def _call(): # Actually call the function if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False): result = function(namespace_obj) else: # namespace -> dictionary _flat_key = lambda key: key.replace('-', '_') all_input = dict((_flat_key(k), v) for k,v in vars(namespace_obj).items()) # filter the namespace variables so that only those expected # by the actual function will pass spec = get_arg_spec(function) positional = [all_input[k] for k in spec.args] kwonly = getattr(spec, 'kwonlyargs', []) keywords = dict((k, all_input[k]) for k in kwonly) # *args if spec.varargs: positional += getattr(namespace_obj, spec.varargs) # **kwargs varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', [])) if varkw: not_kwargs = [DEST_FUNCTION] + spec.args + [spec.varargs] + kwonly for k in vars(namespace_obj): if k.startswith('_') or k in not_kwargs: continue keywords[k] = getattr(namespace_obj, k) result = function(*positional, **keywords) # Yield the results if isinstance(result, (GeneratorType, list, tuple)): # yield each line ASAP, convert CommandError message to a line for line in result: yield line else: # yield non-empty non-iterable result as a single line if result is not None: yield result
def set_default_command(parser, function): """ Sets default command (i.e. a function) for given parser. If `parser.description` is empty and the function has a docstring, it is used as the description. .. note:: An attempt to set default command to a parser which already has subparsers (e.g. added with :func:`~argh.assembling.add_commands`) results in a `AssemblingError`. .. note:: If there are both explicitly declared arguments (e.g. via :func:`~argh.decorators.arg`) and ones inferred from the function signature (e.g. via :func:`~argh.decorators.command`), declared ones will be merged into inferred ones. If an argument does not conform function signature, `AssemblingError` is raised. .. note:: If the parser was created with ``add_help=True`` (which is by default), option name ``-h`` is silently removed from any argument. """ if parser._subparsers: _require_support_for_default_command_with_subparsers() spec = get_arg_spec(function) declared_args = getattr(function, ATTR_ARGS, []) inferred_args = list(_get_args_from_signature(function)) if inferred_args and declared_args: # We've got a mixture of declared and inferred arguments # a mapping of "dest" strings to argument declarations. # # * a "dest" string is a normalized form of argument name, i.e.: # # '-f', '--foo' → 'foo' # 'foo-bar' → 'foo_bar' # # * argument declaration is a dictionary representing an argument; # it is obtained either from _get_args_from_signature() or from # an @arg decorator (as is). # dests = OrderedDict() for argspec in inferred_args: dest = _get_parser_param_kwargs(parser, argspec)['dest'] dests[dest] = argspec for declared_kw in declared_args: # an argument is declared via decorator dest = _get_dest(parser, declared_kw) if dest in dests: # the argument is already known from function signature # # now make sure that this declared arg conforms to the function # signature and therefore only refines an inferred arg: # # @arg('my-foo') maps to func(my_foo) # @arg('--my-bar') maps to func(my_bar=...) # either both arguments are positional or both are optional decl_positional = _is_positional(declared_kw['option_strings']) infr_positional = _is_positional(dests[dest]['option_strings']) if decl_positional != infr_positional: kinds = {True: 'positional', False: 'optional'} raise AssemblingError( '{func}: argument "{dest}" declared as {kind_i} ' '(in function signature) and {kind_d} (via decorator)'. format( func=function.__name__, dest=dest, kind_i=kinds[infr_positional], kind_d=kinds[decl_positional], )) # merge explicit argument declaration into the inferred one # (e.g. `help=...`) dests[dest].update(**declared_kw) else: # the argument is not in function signature varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', [])) if varkw: # function accepts **kwargs; the argument goes into it dests[dest] = declared_kw else: # there's no way we can map the argument declaration # to function signature xs = (dests[x]['option_strings'] for x in dests) raise AssemblingError( '{func}: argument {flags} does not fit ' 'function signature: {sig}'.format( flags=', '.join(declared_kw['option_strings']), func=function.__name__, sig=', '.join('/'.join(x) for x in xs))) # pack the modified data back into a list inferred_args = dests.values() command_args = inferred_args or declared_args # add types, actions, etc. (e.g. default=3 implies type=int) command_args = [_guess(x) for x in command_args] for draft in command_args: draft = draft.copy() if 'help' not in draft: draft.update(help=DEFAULT_ARGUMENT_TEMPLATE) dest_or_opt_strings = draft.pop('option_strings') if parser.add_help and '-h' in dest_or_opt_strings: dest_or_opt_strings = [x for x in dest_or_opt_strings if x != '-h'] completer = draft.pop('completer', None) try: action = parser.add_argument(*dest_or_opt_strings, **draft) if COMPLETION_ENABLED and completer: action.completer = completer except Exception as e: raise type(e)('{func}: cannot add arg {args}: {msg}'.format( args='/'.join(dest_or_opt_strings), func=function.__name__, msg=e)) if function.__doc__ and not parser.description: parser.description = function.__doc__ parser.set_defaults(**{ DEST_FUNCTION: function, })
def set_default_command(parser, function): """ Sets default command (i.e. a function) for given parser. If `parser.description` is empty and the function has a docstring, it is used as the description. .. note:: An attempt to set default command to a parser which already has subparsers (e.g. added with :func:`~argh.assembling.add_commands`) results in a `AssemblingError`. .. note:: If there are both explicitly declared arguments (e.g. via :func:`~argh.decorators.arg`) and ones inferred from the function signature (e.g. via :func:`~argh.decorators.command`), declared ones will be merged into inferred ones. If an argument does not conform function signature, `AssemblingError` is raised. .. note:: If the parser was created with ``add_help=True`` (which is by default), option name ``-h`` is silently removed from any argument. """ if parser._subparsers: _require_support_for_default_command_with_subparsers() spec = get_arg_spec(function) declared_args = getattr(function, ATTR_ARGS, []) inferred_args = list(_get_args_from_signature(function)) if inferred_args and declared_args: # We've got a mixture of declared and inferred arguments # a mapping of "dest" strings to argument declarations. # # * a "dest" string is a normalized form of argument name, i.e.: # # '-f', '--foo' → 'foo' # 'foo-bar' → 'foo_bar' # # * argument declaration is a dictionary representing an argument; # it is obtained either from _get_args_from_signature() or from # an @arg decorator (as is). # dests = OrderedDict() for argspec in inferred_args: dest = _get_parser_param_kwargs(parser, argspec)['dest'] dests[dest] = argspec for declared_kw in declared_args: # an argument is declared via decorator dest = _get_dest(parser, declared_kw) if dest in dests: # the argument is already known from function signature # # now make sure that this declared arg conforms to the function # signature and therefore only refines an inferred arg: # # @arg('my-foo') maps to func(my_foo) # @arg('--my-bar') maps to func(my_bar=...) # either both arguments are positional or both are optional decl_positional = _is_positional(declared_kw['option_strings']) infr_positional = _is_positional(dests[dest]['option_strings']) if decl_positional != infr_positional: kinds = {True: 'positional', False: 'optional'} raise AssemblingError( '{func}: argument "{dest}" declared as {kind_i} ' '(in function signature) and {kind_d} (via decorator)' .format( func=function.__name__, dest=dest, kind_i=kinds[infr_positional], kind_d=kinds[decl_positional], )) # merge explicit argument declaration into the inferred one # (e.g. `help=...`) dests[dest].update(**declared_kw) else: # the argument is not in function signature varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', [])) if varkw: # function accepts **kwargs; the argument goes into it dests[dest] = declared_kw else: # there's no way we can map the argument declaration # to function signature xs = (dests[x]['option_strings'] for x in dests) raise AssemblingError( '{func}: argument {flags} does not fit ' 'function signature: {sig}'.format( flags=', '.join(declared_kw['option_strings']), func=function.__name__, sig=', '.join('/'.join(x) for x in xs))) # pack the modified data back into a list inferred_args = dests.values() command_args = inferred_args or declared_args # add types, actions, etc. (e.g. default=3 implies type=int) command_args = [_guess(x) for x in command_args] for draft in command_args: draft = draft.copy() if 'help' not in draft: draft.update(help=DEFAULT_ARGUMENT_TEMPLATE) dest_or_opt_strings = draft.pop('option_strings') if parser.add_help and '-h' in dest_or_opt_strings: dest_or_opt_strings = [x for x in dest_or_opt_strings if x != '-h'] completer = draft.pop('completer', None) try: action = parser.add_argument(*dest_or_opt_strings, **draft) if COMPLETION_ENABLED and completer: action.completer = completer except Exception as e: raise type(e)('{func}: cannot add arg {args}: {msg}'.format( args='/'.join(dest_or_opt_strings), func=function.__name__, msg=e)) if function.__doc__ and not parser.description: parser.description = function.__doc__ parser.set_defaults(**{ DEST_FUNCTION: function, })