Package mvpa :: Package base :: Module dochelpers
[hide private]
[frames] | no frames]

Source Code for Module mvpa.base.dochelpers

  1  # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 
  2  # vi: set ft=python sts=4 ts=4 sw=4 et: 
  3  ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 
  4  # 
  5  #   See COPYING file distributed along with the PyMVPA package for the 
  6  #   copyright and license terms. 
  7  # 
  8  ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 
  9  """Various helpers to improve docstrings and textual output""" 
 10   
 11  __docformat__ = 'restructuredtext' 
 12   
 13  import re, textwrap 
 14   
 15  # for table2string 
 16  import numpy as N 
 17  from math import ceil 
 18  from StringIO import StringIO 
 19  from mvpa import cfg 
 20   
 21  from mvpa.base import externals 
 22  if __debug__: 
 23      from mvpa.base import debug 
 24   
 25  __add_init2doc = False 
 26  __in_ipython = externals.exists('running ipython env') 
 27  # if ran within IPython -- might need to add doc to init 
 28  if __in_ipython: 
 29      __rst_mode = 0                           # either to do ReST links at all 
 30      _rst_sep = "" 
 31      _rst_sep2 = "" 
 32      from IPython import Release 
 33      # XXX figure out exact version when init doc started to be added to class 
 34      # description 
 35      if Release.version <= '0.8.1': 
 36          __add_init2doc = True 
 37  else: 
 38      __rst_mode = 1 
 39      _rst_sep = "`" 
 40      _rst_sep2 = ":" 
 41   
42 -def _rst(s, snotrst=''):
43 """Produce s only in __rst mode""" 44 if __rst_mode: 45 return s 46 else: 47 return snotrst
48
49 -def rstUnderline(text, markup):
50 """Add and underline RsT string matching the length of the given string. 51 """ 52 return text + '\n' + markup * len(text)
53 54
55 -def singleOrPlural(single, plural, n):
56 """Little helper to spit out single or plural version of a word. 57 """ 58 ni = int(n) 59 if ni > 1 or ni == 0: 60 # 1 forest, 2 forests, 0 forests 61 return plural 62 else: 63 return single
64 65
66 -def handleDocString(text, polite=True):
67 """Take care of empty and non existing doc strings.""" 68 if text == None or not len(text): 69 if polite: 70 return 'No documentation found. Sorry!' 71 else: 72 return '' 73 else: 74 # Problem is that first line might often have no offset, so might 75 # need to be ignored from dedent call 76 if not text.startswith(' '): 77 lines = text.split('\n') 78 text2 = '\n'.join(lines[1:]) 79 return lines[0] + "\n" + textwrap.dedent(text2) 80 else: 81 return textwrap.dedent(text)
82 83
84 -def _indent(text, istr=' '):
85 """Simple indenter 86 """ 87 return '\n'.join(istr + s for s in text.split('\n'))
88 89
90 -def _splitOutParametersStr(initdoc, initial=False):
91 """Split documentation into (header, parameters, suffix) 92 93 :Parameters: 94 initdoc : string 95 The documentation string 96 initial : bool 97 Either this is an original initdoc or the one which 98 was already processed (i.e. of parent class) 99 """ 100 sep = (_rst_sep2, ':')[int(initial)] 101 parameters_str = "%sParameters%s" % (sep, sep) 102 103 # TODO: bind it to the only word in the line 104 if not (parameters_str in initdoc): 105 result = initdoc, "", "" 106 else: 107 # Could have been accomplished also via re.match 108 109 # where new line is after :Parameters: 110 # parameters header index 111 ph_i = initdoc.index(parameters_str) 112 113 # parameters body index 114 pb_i = initdoc.index('\n', ph_i+1) 115 116 # end of parameters 117 try: 118 pe_i = initdoc.index('\n\n', pb_i) 119 except ValueError: 120 pe_i = len(initdoc) 121 122 result = initdoc[:ph_i].rstrip('\n '), \ 123 initdoc[pb_i:pe_i], initdoc[pe_i:] 124 125 # XXX a bit of duplication of effort since handleDocString might 126 # do splitting internally 127 return [handleDocString(x, polite=False).strip('\n') for x in result]
128 129 130 __re_params = re.compile('(?:\n\S.*?)+$') 131 __re_spliter1 = re.compile('(?:\n|\A)(?=\S)') 132 __re_spliter2 = re.compile('[\n:]')
133 -def _parseParameters(paramdoc):
134 """Parse parameters and return list of (name, full_doc_string) 135 136 It is needed to remove multiple entries for the same parameter 137 like it could be with adding parameters from the parent class 138 139 It assumes that previousely parameters were unwrapped, so their 140 documentation starts at the begining of the string, like what 141 should it be after _splitOutParametersStr 142 """ 143 entries = __re_spliter1.split(paramdoc) 144 result = [(__re_spliter2.split(e)[0].strip(), e) 145 for e in entries if e != ''] 146 if __debug__: 147 debug('DOCH', 'parseParameters: Given "%s", we split into %s' % 148 (paramdoc, result)) 149 return result
150 151
152 -def enhancedDocString(item, *args, **kwargs):
153 """Generate enhanced doc strings for various items. 154 155 :Parameters: 156 item : basestring or class 157 What object requires enhancing of documentation 158 *args : list 159 Includes base classes to look for parameters, as well, first item 160 must be a dictionary of locals if item is given by a string 161 force_extend : bool 162 Either to force looking for the documentation in the parents. 163 By default force_extend = False, and lookup happens only if kwargs 164 is one of the arguments to the respective function (e.g. item.__init__) 165 skip_params : list of basestring 166 List of parameters (in addition to [kwargs]) which should not 167 be added to the documentation of the class. 168 169 It is to be used from a collector, ie whenever class is already created 170 """ 171 # Handling of arguments 172 if len(kwargs): 173 if set(kwargs.keys()).issubset(set(['force_extend'])): 174 raise ValueError, "Got unknown keyword arguments (smth among %s)" \ 175 " in enhancedDocString." % kwargs 176 force_extend = kwargs.get('force_extend', False) 177 skip_params = kwargs.get('skip_params', []) 178 179 # XXX make it work also not only with classes but with methods as well 180 if isinstance(item, basestring): 181 if len(args)<1 or not isinstance(args[0], dict): 182 raise ValueError, \ 183 "Please provide locals for enhancedDocString of %s" % item 184 name = item 185 lcl = args[0] 186 args = args[1:] 187 elif hasattr(item, "im_class"): 188 # bound method 189 raise NotImplementedError, \ 190 "enhancedDocString is not yet implemented for methods" 191 elif hasattr(item, "__name__"): 192 name = item.__name__ 193 lcl = item.__dict__ 194 else: 195 raise ValueError, "Don't know how to extend docstring for %s" % item 196 197 # check whether docstring magic is requested or not 198 if not cfg.getboolean('doc', 'pimp docstrings', True): 199 return lcl['__doc__'] 200 201 #return lcl['__doc__'] 202 rst_lvlmarkup = ["=", "-", "_"] 203 204 # would then be called for any child... ok - ad hoc for SVM??? 205 if hasattr(item, '_customizeDoc') and name=='SVM': 206 item._customizeDoc() 207 208 initdoc = "" 209 if lcl.has_key('__init__'): 210 func = lcl['__init__'] 211 initdoc = func.__doc__ 212 213 # either to extend arguments 214 # do only if kwargs is one of the arguments 215 # in python 2.5 args are no longer in co_names but in varnames 216 extend_args = force_extend or \ 217 'kwargs' in (func.func_code.co_names + 218 func.func_code.co_varnames) 219 220 if __debug__ and not extend_args: 221 debug('DOCH', 'Not extending parameters for %s' % name) 222 223 if initdoc is None: 224 initdoc = "Initialize instance of %s" % name 225 226 initdoc, params, suffix = _splitOutParametersStr(initdoc, initial=True) 227 228 if lcl.has_key('_paramsdoc'): 229 params += '\n' + handleDocString(lcl['_paramsdoc']) 230 231 params_list = _parseParameters(params) 232 known_params = set([i[0] for i in params_list]) 233 # no need for placeholders 234 skip_params = set(skip_params + ['kwargs', '**kwargs']) 235 236 # XXX we do evil check here, refactor code to separate 237 # regressions out of the classifiers, and making 238 # retrainable flag not available for those classes which 239 # can't actually do retraining. Although it is not 240 # actually that obvious for Meta Classifiers 241 if hasattr(item, '_clf_internals'): 242 clf_internals = item._clf_internals 243 skip_params.update([i for i in ('regression', 'retrainable') 244 if not (i in clf_internals)]) 245 246 known_params.update(skip_params) 247 if extend_args: 248 # go through all the parents and obtain their init parameters 249 parent_params_list = [] 250 for i in args: 251 if hasattr(i, '__init__'): 252 # XXX just assign within a class to don't redo without need 253 initdoc_ = i.__init__.__doc__ 254 if initdoc_ is None: 255 continue 256 splits_ = _splitOutParametersStr(initdoc_) 257 params_ = splits_[1] 258 parent_params_list += _parseParameters(params_.lstrip()) 259 260 # extend with ones which are not known to current init 261 for i, v in parent_params_list: 262 if not (i in known_params): 263 params_list += [(i, v)] 264 known_params.update([i]) 265 266 # if there are parameters -- populate the list 267 if len(params_list): 268 params_ = '\n'.join([i[1].rstrip() for i in params_list 269 if not i[0] in skip_params]) 270 initdoc += "\n\n%sParameters%s\n" % ( (_rst_sep2,)*2 ) \ 271 + _indent(params_) 272 273 if suffix != "": 274 initdoc += "\n\n" + suffix 275 276 initdoc = handleDocString(initdoc) 277 278 # Finally assign generated doc to the constructor 279 lcl['__init__'].__doc__ = initdoc 280 281 docs = [ handleDocString(lcl['__doc__']) ] 282 283 # Optionally populate the class documentation with it 284 if __add_init2doc and initdoc != "": 285 docs += [ rstUnderline('Constructor information for `%s` class' % name, 286 rst_lvlmarkup[2]), 287 initdoc ] 288 289 # Add information about the states if available 290 if lcl.has_key('_statesdoc'): 291 # no indent is necessary since states list must be already indented 292 docs += [_rst('.. note::\n ') + 'Available state variables:', 293 handleDocString(item._statesdoc)] 294 295 if len(args): 296 bc_intro = _rst(' ') + 'Please refer to the documentation of the ' \ 297 'base %s for more information:' \ 298 % (singleOrPlural('class', 'classes', len(args))) 299 300 docs += [_rst('\n.. seealso::'), 301 bc_intro, 302 ' ' + ',\n '.join(['%s%s.%s%s' % (_rst(':class:`~'), 303 i.__module__, 304 i.__name__, 305 _rst_sep) 306 for i in args]) 307 ] 308 309 itemdoc = '\n\n'.join(docs) 310 # remove some bogus new lines -- never 3 empty lines in doc are useful 311 result = re.sub("\s*\n\s*\n\s*\n", "\n\n", itemdoc) 312 313 return result
314 315
316 -def table2string(table, out=None):
317 """Given list of lists figure out their common widths and print to out 318 319 :Parameters: 320 table : list of lists of strings 321 What is aimed to be printed 322 out : None or stream 323 Where to print. If None -- will print and return string 324 325 :Returns: 326 string if out was None 327 """ 328 329 print2string = out is None 330 if print2string: 331 out = StringIO() 332 333 # equalize number of elements in each row 334 Nelements_max = max(len(x) for x in table) 335 for i, table_ in enumerate(table): 336 table[i] += [''] * (Nelements_max - len(table_)) 337 338 # figure out lengths within each column 339 atable = N.asarray(table) 340 markup_strip = re.compile('^@[lrc]') 341 col_width = [ max( [len(markup_strip.sub('', x)) 342 for x in column] ) for column in atable.T ] 343 string = "" 344 for i, table_ in enumerate(table): 345 string_ = "" 346 for j, item in enumerate(table_): 347 item = str(item) 348 if item.startswith('@'): 349 align = item[1] 350 item = item[2:] 351 if not align in ['l', 'r', 'c']: 352 raise ValueError, 'Unknown alignment %s. Known are l,r,c' 353 else: 354 align = 'c' 355 356 NspacesL = ceil((col_width[j] - len(item))/2.0) 357 NspacesR = col_width[j] - NspacesL - len(item) 358 359 if align == 'c': 360 pass 361 elif align == 'l': 362 NspacesL, NspacesR = 0, NspacesL + NspacesR 363 elif align == 'r': 364 NspacesL, NspacesR = NspacesL + NspacesR, 0 365 else: 366 raise RuntimeError, 'Should not get here with align=%s' % align 367 368 string_ += "%%%ds%%s%%%ds " \ 369 % (NspacesL, NspacesR) % ('', item, '') 370 string += string_.rstrip() + '\n' 371 out.write(string) 372 373 if print2string: 374 value = out.getvalue() 375 out.close() 376 return value
377