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 __parameters_str_re = re.compile("[\n^]\s*:?Parameters?:?\s*\n") 90 """regexp to match :Parameter: and :Parameters: stand alone in a line""" 91
92 -def _splitOutParametersStr(initdoc):
93 """Split documentation into (header, parameters, suffix) 94 95 :Parameters: 96 initdoc : string 97 The documentation string 98 """ 99 100 # TODO: bind it to the only word in the line 101 p_res = __parameters_str_re.search(initdoc) 102 if p_res is None: 103 result = initdoc, "", "" 104 else: 105 # Could have been accomplished also via re.match 106 107 # where new line is after :Parameters: 108 # parameters header index 109 ph_i = p_res.start() 110 111 # parameters body index 112 pb_i = p_res.end() 113 114 # end of parameters 115 try: 116 pe_i = initdoc.index('\n\n', pb_i) 117 except ValueError: 118 pe_i = len(initdoc) 119 120 result = initdoc[:ph_i].rstrip('\n '), \ 121 initdoc[pb_i:pe_i], initdoc[pe_i:] 122 123 # XXX a bit of duplication of effort since handleDocString might 124 # do splitting internally 125 return [handleDocString(x, polite=False).strip('\n') for x in result]
126 127 128 __re_params = re.compile('(?:\n\S.*?)+$') 129 __re_spliter1 = re.compile('(?:\n|\A)(?=\S)') 130 __re_spliter2 = re.compile('[\n:]')
131 -def _parseParameters(paramdoc):
132 """Parse parameters and return list of (name, full_doc_string) 133 134 It is needed to remove multiple entries for the same parameter 135 like it could be with adding parameters from the parent class 136 137 It assumes that previousely parameters were unwrapped, so their 138 documentation starts at the begining of the string, like what 139 should it be after _splitOutParametersStr 140 """ 141 entries = __re_spliter1.split(paramdoc) 142 result = [(__re_spliter2.split(e)[0].strip(), e) 143 for e in entries if e != ''] 144 if __debug__: 145 debug('DOCH', 'parseParameters: Given "%s", we split into %s' % 146 (paramdoc, result)) 147 return result
148 149
150 -def enhancedDocString(item, *args, **kwargs):
151 """Generate enhanced doc strings for various items. 152 153 :Parameters: 154 item : basestring or class 155 What object requires enhancing of documentation 156 *args : list 157 Includes base classes to look for parameters, as well, first item 158 must be a dictionary of locals if item is given by a string 159 force_extend : bool 160 Either to force looking for the documentation in the parents. 161 By default force_extend = False, and lookup happens only if kwargs 162 is one of the arguments to the respective function (e.g. item.__init__) 163 skip_params : list of basestring 164 List of parameters (in addition to [kwargs]) which should not 165 be added to the documentation of the class. 166 167 It is to be used from a collector, ie whenever class is already created 168 """ 169 # Handling of arguments 170 if len(kwargs): 171 if set(kwargs.keys()).issubset(set(['force_extend'])): 172 raise ValueError, "Got unknown keyword arguments (smth among %s)" \ 173 " in enhancedDocString." % kwargs 174 force_extend = kwargs.get('force_extend', False) 175 skip_params = kwargs.get('skip_params', []) 176 177 # XXX make it work also not only with classes but with methods as well 178 if isinstance(item, basestring): 179 if len(args)<1 or not isinstance(args[0], dict): 180 raise ValueError, \ 181 "Please provide locals for enhancedDocString of %s" % item 182 name = item 183 lcl = args[0] 184 args = args[1:] 185 elif hasattr(item, "im_class"): 186 # bound method 187 raise NotImplementedError, \ 188 "enhancedDocString is not yet implemented for methods" 189 elif hasattr(item, "__name__"): 190 name = item.__name__ 191 lcl = item.__dict__ 192 else: 193 raise ValueError, "Don't know how to extend docstring for %s" % item 194 195 # check whether docstring magic is requested or not 196 if not cfg.getboolean('doc', 'pimp docstrings', True): 197 return lcl['__doc__'] 198 199 #return lcl['__doc__'] 200 rst_lvlmarkup = ["=", "-", "_"] 201 202 # would then be called for any child... ok - ad hoc for SVM??? 203 if hasattr(item, '_customizeDoc') and name=='SVM': 204 item._customizeDoc() 205 206 initdoc = "" 207 if lcl.has_key('__init__'): 208 func = lcl['__init__'] 209 initdoc = func.__doc__ 210 211 # either to extend arguments 212 # do only if kwargs is one of the arguments 213 # in python 2.5 args are no longer in co_names but in varnames 214 extend_args = force_extend or \ 215 'kwargs' in (func.func_code.co_names + 216 func.func_code.co_varnames) 217 218 if __debug__ and not extend_args: 219 debug('DOCH', 'Not extending parameters for %s' % name) 220 221 if initdoc is None: 222 initdoc = "Initialize instance of %s" % name 223 224 initdoc, params, suffix = _splitOutParametersStr(initdoc) 225 226 if lcl.has_key('_paramsdoc'): 227 params += '\n' + handleDocString(lcl['_paramsdoc']) 228 229 params_list = _parseParameters(params) 230 known_params = set([i[0] for i in params_list]) 231 # no need for placeholders 232 skip_params = set(skip_params + ['kwargs', '**kwargs']) 233 234 # XXX we do evil check here, refactor code to separate 235 # regressions out of the classifiers, and making 236 # retrainable flag not available for those classes which 237 # can't actually do retraining. Although it is not 238 # actually that obvious for Meta Classifiers 239 if hasattr(item, '_clf_internals'): 240 clf_internals = item._clf_internals 241 skip_params.update([i for i in ('regression', 'retrainable') 242 if not (i in clf_internals)]) 243 244 known_params.update(skip_params) 245 if extend_args: 246 # go through all the parents and obtain their init parameters 247 parent_params_list = [] 248 for i in args: 249 if hasattr(i, '__init__'): 250 # XXX just assign within a class to don't redo without need 251 initdoc_ = i.__init__.__doc__ 252 if initdoc_ is None: 253 continue 254 splits_ = _splitOutParametersStr(initdoc_) 255 params_ = splits_[1] 256 parent_params_list += _parseParameters(params_.lstrip()) 257 258 # extend with ones which are not known to current init 259 for i, v in parent_params_list: 260 if not (i in known_params): 261 params_list += [(i, v)] 262 known_params.update([i]) 263 264 # if there are parameters -- populate the list 265 if len(params_list): 266 params_ = '\n'.join([i[1].rstrip() for i in params_list 267 if not i[0] in skip_params]) 268 initdoc += "\n\n%sParameters%s\n" % ( (_rst_sep2,)*2 ) \ 269 + _indent(params_) 270 271 if suffix != "": 272 initdoc += "\n\n" + suffix 273 274 initdoc = handleDocString(initdoc) 275 276 # Finally assign generated doc to the constructor 277 lcl['__init__'].__doc__ = initdoc 278 279 docs = [ handleDocString(lcl['__doc__']) ] 280 281 # Optionally populate the class documentation with it 282 if __add_init2doc and initdoc != "": 283 docs += [ rstUnderline('Constructor information for `%s` class' % name, 284 rst_lvlmarkup[2]), 285 initdoc ] 286 287 # Add information about the states if available 288 if lcl.has_key('_statesdoc'): 289 # no indent is necessary since states list must be already indented 290 docs += [_rst('.. note::\n ') + 'Available state variables:', 291 handleDocString(item._statesdoc)] 292 293 if len(args): 294 bc_intro = _rst(' ') + 'Please refer to the documentation of the ' \ 295 'base %s for more information:' \ 296 % (singleOrPlural('class', 'classes', len(args))) 297 298 docs += [_rst('\n.. seealso::'), 299 bc_intro, 300 ' ' + ',\n '.join(['%s%s.%s%s' % (_rst(':class:`~'), 301 i.__module__, 302 i.__name__, 303 _rst_sep) 304 for i in args]) 305 ] 306 307 itemdoc = '\n\n'.join(docs) 308 # remove some bogus new lines -- never 3 empty lines in doc are useful 309 result = re.sub("\s*\n\s*\n\s*\n", "\n\n", itemdoc) 310 311 return result
312 313
314 -def table2string(table, out=None):
315 """Given list of lists figure out their common widths and print to out 316 317 :Parameters: 318 table : list of lists of strings 319 What is aimed to be printed 320 out : None or stream 321 Where to print. If None -- will print and return string 322 323 :Returns: 324 string if out was None 325 """ 326 327 print2string = out is None 328 if print2string: 329 out = StringIO() 330 331 # equalize number of elements in each row 332 Nelements_max = max(len(x) for x in table) 333 for i, table_ in enumerate(table): 334 table[i] += [''] * (Nelements_max - len(table_)) 335 336 # figure out lengths within each column 337 atable = N.asarray(table) 338 markup_strip = re.compile('^@[lrc]') 339 col_width = [ max( [len(markup_strip.sub('', x)) 340 for x in column] ) for column in atable.T ] 341 string = "" 342 for i, table_ in enumerate(table): 343 string_ = "" 344 for j, item in enumerate(table_): 345 item = str(item) 346 if item.startswith('@'): 347 align = item[1] 348 item = item[2:] 349 if not align in ['l', 'r', 'c']: 350 raise ValueError, 'Unknown alignment %s. Known are l,r,c' 351 else: 352 align = 'c' 353 354 NspacesL = ceil((col_width[j] - len(item))/2.0) 355 NspacesR = col_width[j] - NspacesL - len(item) 356 357 if align == 'c': 358 pass 359 elif align == 'l': 360 NspacesL, NspacesR = 0, NspacesL + NspacesR 361 elif align == 'r': 362 NspacesL, NspacesR = NspacesL + NspacesR, 0 363 else: 364 raise RuntimeError, 'Should not get here with align=%s' % align 365 366 string_ += "%%%ds%%s%%%ds " \ 367 % (NspacesL, NspacesR) % ('', item, '') 368 string += string_.rstrip() + '\n' 369 out.write(string) 370 371 if print2string: 372 value = out.getvalue() 373 out.close() 374 return value
375