def _beautify_multiline_field(content): content = dedent_docstring(content) lines = content.split('\n') title = '' if len(lines): title = lines[0] if len(lines) > 1: content = '' for l in lines[1:]: l = l.strip() content = '{}{}{}'.format( content, ' ' if len(content) and l != '.' and content[-1] != '\n' else '', l if l != '.' else '\n') return title, content
def test_dedent(): assert_false(dedent_docstring("one liner").endswith("\n"))
def alter_interface_docs_for_cmdline(docs): """Apply modifications to interface docstrings for cmdline doc use.""" # central place to alter the impression of docstrings, # like removing Python API specific sections, and argument markup if not docs: return docs import re import textwrap docs = dedent_docstring(docs) # clean cmdline sections docs = re.sub(r'\|\| PYTHON \>\>.*?\<\< PYTHON \|\|', '', docs, flags=re.MULTILINE | re.DOTALL) # clean cmdline in-line bits docs = re.sub(r'\[PY:\s[^\[\]]*\sPY\]', '', docs, flags=re.MULTILINE | re.DOTALL) docs = re.sub(r'\[CMD:\s([^\[\]]*)\sCMD\]', lambda match: match.group(1), docs, flags=re.MULTILINE) docs = re.sub(r'\|\| CMDLINE \>\>(.*?)\<\< CMDLINE \|\|', lambda match: match.group(1), docs, flags=re.MULTILINE | re.DOTALL) # remove :role:`...` RST markup for cmdline docs docs = re.sub( r':\S+:`[^`]*`[\\]*', lambda match: ':'.join(match.group(0).split(':')[2:]).strip('`\\'), docs, flags=re.MULTILINE | re.DOTALL) # make the handbook doc references more accessible # the URL is a redirect configured at readthedocs docs = re.sub(r'(handbook:[0-9]-[0-9]*)', '\\1 (http://handbook.datalad.org/symbols)', docs) # remove None constraint. In general, `None` on the cmdline means don't # give option at all, but specifying `None` explicitly is practically # impossible docs = re.sub(r',\sor\svalue\smust\sbe\s`None`', '', docs, flags=re.MULTILINE | re.DOTALL) # capitalize variables and remove backticks to uniformize with # argparse output docs = re.sub( r'([^`]+)`([a-zA-Z0-9_]+)`([^`]+)', lambda match: f'{match.group(1)}{match.group(2).upper()}{match.group(3)}', docs) # select only the cmdline alternative from argument specifications docs = re.sub(r'``([a-zA-Z0-9_,.]+)\|\|([a-zA-Z0-9-,.]+)``', lambda match: f'``{match.group(2)}``', docs) # clean up sphinx API refs docs = re.sub(r'\~datalad\.api\.\S*', lambda match: "`{0}`".format(match.group(0)[13:]), docs) # dedicated support for version markup docs = docs.replace('.. versionadded::', 'New in version') docs = docs.replace('.. versionchanged::', 'Changed in version') docs = docs.replace('.. deprecated::', 'Deprecated in version') # Remove RST paragraph markup docs = re.sub(r'^.. \S+::', lambda match: match.group(0)[3:-2].upper(), docs, flags=re.MULTILINE) docs = re.sub(r'^([ ]*)\|\| REFLOW \>\>\n(.*?)\<\< REFLOW \|\|', lambda match: textwrap.fill( match.group(2), subsequent_indent=match.group(1)), docs, flags=re.MULTILINE | re.DOTALL) return docs
def __call__(plugin=None, dataset=None, showpluginhelp=False, showplugininfo=False, **kwargs): plugins = _get_plugins() if not plugin: max_name_len = max(len(k) for k in plugins.keys()) for plname, plinfo in sorted(plugins.items(), key=lambda x: x[0]): spacer = ' ' * (max_name_len - len(plname)) synopsis = None try: with open(plinfo['file']) as plf: for line in plf: if line.startswith('"""'): synopsis = line.strip().strip('"').strip() break except Exception as e: ui.message('{}{} [BROKEN] {}'.format( plname, spacer, exc_str(e))) continue if synopsis: msg = '{}{} - {}'.format(plname, spacer, synopsis) else: msg = '{}{} [no synopsis]'.format(plname, spacer) if showplugininfo: msg = '{} ({})'.format(msg, plinfo['file']) ui.message(msg) return args = None if isinstance(plugin, (list, tuple)): args = plugin[1:] plugin = plugin[0] if plugin not in plugins: raise ValueError("unknown plugin '{}', available: {}".format( plugin, ','.join(plugins.keys()))) user_supplied_args = set() if args: # we got some arguments in the plugin spec, parse them and add to # kwargs for arg in args: if isinstance(arg, tuple): # came from python item-style argname, argval = arg else: parsed = argspec.match(arg) if parsed is None: raise ValueError( "invalid plugin argument: '{}'".format(arg)) argname, argval = parsed.groups() if argname in kwargs: # argument was seen at least once before -> make list existing_val = kwargs[argname] if not isinstance(existing_val, list): existing_val = [existing_val] existing_val.append(argval) argval = existing_val kwargs[argname] = argval user_supplied_args.add(argname) plugin_call = _load_plugin(plugins[plugin]['file']) if showpluginhelp: # we don't need special docs for the cmdline, standard python ones # should be comprehensible enough ui.message( dedent_docstring(plugin_call.__doc__) if plugin_call. __doc__ else 'This plugin has no documentation') return # # argument preprocessing # # check the plugin signature and filter out all unsupported args plugin_args, _, _, arg_defaults = inspect.getargspec(plugin_call) supported_args = {k: v for k, v in kwargs.items() if k in plugin_args} excluded_args = user_supplied_args.difference(supported_args.keys()) if excluded_args: lgr.warning( 'ignoring plugin argument(s) %s, not supported by plugin', excluded_args) # always overwrite the dataset arg if one is needed if 'dataset' in plugin_args: supported_args['dataset'] = require_dataset( # use dedicated arg if given, also anything the came with the plugin args # or curdir as the last resort dataset if dataset else kwargs.get('dataset', curdir), # note 'dataset' arg is always first, if we have defaults for all args # we have a default for 'dataset' to -> it is optional check_installed=len(arg_defaults) != len(plugin_args), purpose='handover to plugin') # call as a generator for res in plugin_call(**supported_args): if not res: continue if dataset: # enforce standard regardless of what plugin did res['refds'] = getattr(dataset, 'path', dataset) elif 'refds' in res: # no base dataset, results must not have them either del res['refds'] if 'logger' not in res: # make sure we have a logger res['logger'] = lgr yield res