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

Source Code for Module mvpa.base.dochelpers

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