source: trunk/adm/website/sphinxext/docscrape.py @ 50

Last change on this file since 50 was 36, checked in by pinsard, 13 years ago

add end users website production

File size: 14.5 KB
Line 
1"""Extract reference documentation from the NumPy source tree.
2
3"""
4
5import inspect
6import textwrap
7import re
8import pydoc
9from StringIO import StringIO
10from warnings import warn
114
12class Reader(object):
13    """A line-based string reader.
14
15    """
16    def __init__(self, data):
17        """
18        Parameters
19        ----------
20        data : str
21           String with lines separated by '\n'.
22
23        """
24        if isinstance(data,list):
25            self._str = data
26        else:
27            self._str = data.split('\n') # store string as list of lines
28
29        self.reset()
30
31    def __getitem__(self, n):
32        return self._str[n]
33
34    def reset(self):
35        self._l = 0 # current line nr
36
37    def read(self):
38        if not self.eof():
39            out = self[self._l]
40            self._l += 1
41            return out
42        else:
43            return ''
44
45    def seek_next_non_empty_line(self):
46        for l in self[self._l:]:
47            if l.strip():
48                break
49            else:
50                self._l += 1
51
52    def eof(self):
53        return self._l >= len(self._str)
54
55    def read_to_condition(self, condition_func):
56        start = self._l
57        for line in self[start:]:
58            if condition_func(line):
59                return self[start:self._l]
60            self._l += 1
61            if self.eof():
62                return self[start:self._l+1]
63        return []
64
65    def read_to_next_empty_line(self):
66        self.seek_next_non_empty_line()
67        def is_empty(line):
68            return not line.strip()
69        return self.read_to_condition(is_empty)
70
71    def read_to_next_unindented_line(self):
72        def is_unindented(line):
73            return (line.strip() and (len(line.lstrip()) == len(line)))
74        return self.read_to_condition(is_unindented)
75
76    def peek(self,n=0):
77        if self._l + n < len(self._str):
78            return self[self._l + n]
79        else:
80            return ''
81
82    def is_empty(self):
83        return not ''.join(self._str).strip()
84
85
86class NumpyDocString(object):
87    def __init__(self,docstring):
88        docstring = textwrap.dedent(docstring).split('\n')
89
90        self._doc = Reader(docstring)
91        self._parsed_data = {
92            'Signature': '',
93            'Summary': [''],
94            'Extended Summary': [],
95            'Parameters': [],
96            'Returns': [],
97            'Raises': [],
98            'Warns': [],
99            'Other Parameters': [],
100            'Attributes': [],
101            'Methods': [],
102            'See Also': [],
103            'Notes': [],
104            'Warnings': [],
105            'References': '',
106            'Examples': '',
107            'index': {}
108            }
109
110        self._parse()
111
112    def __getitem__(self,key):
113        return self._parsed_data[key]
114
115    def __setitem__(self,key,val):
116        if not self._parsed_data.has_key(key):
117            warn("Unknown section %s" % key)
118        else:
119            self._parsed_data[key] = val
120
121    def _is_at_section(self):
122        self._doc.seek_next_non_empty_line()
123
124        if self._doc.eof():
125            return False
126
127        l1 = self._doc.peek().strip()  # e.g. Parameters
128
129        if l1.startswith('.. index::'):
130            return True
131
132        l2 = self._doc.peek(1).strip() #    ---------- or ==========
133        return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1))
134
135    def _strip(self,doc):
136        i = 0
137        j = 0
138        for i,line in enumerate(doc):
139            if line.strip(): break
140
141        for j,line in enumerate(doc[::-1]):
142            if line.strip(): break
143
144        return doc[i:len(doc)-j]
145
146    def _read_to_next_section(self):
147        section = self._doc.read_to_next_empty_line()
148
149        while not self._is_at_section() and not self._doc.eof():
150            if not self._doc.peek(-1).strip(): # previous line was empty
151                section += ['']
152
153            section += self._doc.read_to_next_empty_line()
154
155        return section
156
157    def _read_sections(self):
158        while not self._doc.eof():
159            data = self._read_to_next_section()
160            name = data[0].strip()
161
162            if name.startswith('..'): # index section
163                yield name, data[1:]
164            elif len(data) < 2:
165                yield StopIteration
166            else:
167                yield name, self._strip(data[2:])
168
169    def _parse_param_list(self,content):
170        r = Reader(content)
171        params = []
172        while not r.eof():
173            header = r.read().strip()
174            if ' : ' in header:
175                arg_name, arg_type = header.split(' : ')[:2]
176            else:
177                arg_name, arg_type = header, ''
178
179            desc = r.read_to_next_unindented_line()
180            desc = dedent_lines(desc)
181
182            params.append((arg_name,arg_type,desc))
183
184        return params
185
186   
187    _name_rgx = re.compile(r"^\s*(:(?P<role>\w+):`(?P<name>[a-zA-Z0-9_.-]+)`|"
188                           r" (?P<name2>[a-zA-Z0-9_.-]+))\s*", re.X)
189    def _parse_see_also(self, content):
190        """
191        func_name : Descriptive text
192            continued text
193        another_func_name : Descriptive text
194        func_name1, func_name2, :meth:`func_name`, func_name3
195
196        """
197        items = []
198
199        def parse_item_name(text):
200            """Match ':role:`name`' or 'name'"""
201            m = self._name_rgx.match(text)
202            if m:
203                g = m.groups()
204                if g[1] is None:
205                    return g[3], None
206                else:
207                    return g[2], g[1]
208            raise ValueError("%s is not a item name" % text)
209
210        def push_item(name, rest):
211            if not name:
212                return
213            name, role = parse_item_name(name)
214            items.append((name, list(rest), role))
215            del rest[:]
216
217        current_func = None
218        rest = []
219       
220        for line in content:
221            if not line.strip(): continue
222
223            m = self._name_rgx.match(line)
224            if m and line[m.end():].strip().startswith(':'):
225                push_item(current_func, rest)
226                current_func, line = line[:m.end()], line[m.end():]
227                rest = [line.split(':', 1)[1].strip()]
228                if not rest[0]:
229                    rest = []
230            elif not line.startswith(' '):
231                push_item(current_func, rest)
232                current_func = None
233                if ',' in line:
234                    for func in line.split(','):
235                        push_item(func, [])
236                elif line.strip():
237                    current_func = line
238            elif current_func is not None:
239                rest.append(line.strip())
240        push_item(current_func, rest)
241        return items
242
243    def _parse_index(self, section, content):
244        """
245        .. index: default
246           :refguide: something, else, and more
247
248        """
249        def strip_each_in(lst):
250            return [s.strip() for s in lst]
251
252        out = {}
253        section = section.split('::')
254        if len(section) > 1:
255            out['default'] = strip_each_in(section[1].split(','))[0]
256        for line in content:
257            line = line.split(':')
258            if len(line) > 2:
259                out[line[1]] = strip_each_in(line[2].split(','))
260        return out
261   
262    def _parse_summary(self):
263        """Grab signature (if given) and summary"""
264        if self._is_at_section():
265            return
266
267        summary = self._doc.read_to_next_empty_line()
268        summary_str = " ".join([s.strip() for s in summary]).strip()
269        if re.compile('^([\w., ]+=)?\s*[\w\.]+\(.*\)$').match(summary_str):
270            self['Signature'] = summary_str
271            if not self._is_at_section():
272                self['Summary'] = self._doc.read_to_next_empty_line()
273        else:
274            self['Summary'] = summary
275
276        if not self._is_at_section():
277            self['Extended Summary'] = self._read_to_next_section()
278   
279    def _parse(self):
280        self._doc.reset()
281        self._parse_summary()
282
283        for (section,content) in self._read_sections():
284            if not section.startswith('..'):
285                section = ' '.join([s.capitalize() for s in section.split(' ')])
286            if section in ('Parameters', 'Attributes', 'Methods',
287                           'Returns', 'Raises', 'Warns'):
288                self[section] = self._parse_param_list(content)
289            elif section.startswith('.. index::'):
290                self['index'] = self._parse_index(section, content)
291            elif section == 'See Also':
292                self['See Also'] = self._parse_see_also(content)
293            else:
294                self[section] = content
295
296    # string conversion routines
297
298    def _str_header(self, name, symbol='-'):
299        return [name, len(name)*symbol]
300
301    def _str_indent(self, doc, indent=4):
302        out = []
303        for line in doc:
304            out += [' '*indent + line]
305        return out
306
307    def _str_signature(self):
308        if self['Signature']:
309            return [self['Signature'].replace('*','\*')] + ['']
310        else:
311            return ['']
312
313    def _str_summary(self):
314        if self['Summary']:
315            return self['Summary'] + ['']
316        else:
317            return []
318
319    def _str_extended_summary(self):
320        if self['Extended Summary']:
321            return self['Extended Summary'] + ['']
322        else:
323            return []
324
325    def _str_param_list(self, name):
326        out = []
327        if self[name]:
328            out += self._str_header(name)
329            for param,param_type,desc in self[name]:
330                out += ['%s : %s' % (param, param_type)]
331                out += self._str_indent(desc)
332            out += ['']
333        return out
334
335    def _str_section(self, name):
336        out = []
337        if self[name]:
338            out += self._str_header(name)
339            out += self[name]
340            out += ['']
341        return out
342
343    def _str_see_also(self, func_role):
344        if not self['See Also']: return []
345        out = []
346        out += self._str_header("See Also")
347        last_had_desc = True
348        for func, desc, role in self['See Also']:
349            if role:
350                link = ':%s:`%s`' % (role, func)
351            elif func_role:
352                link = ':%s:`%s`' % (func_role, func)
353            else:
354                link = "`%s`_" % func
355            if desc or last_had_desc:
356                out += ['']
357                out += [link]
358            else:
359                out[-1] += ", %s" % link
360            if desc:
361                out += self._str_indent([' '.join(desc)])
362                last_had_desc = True
363            else:
364                last_had_desc = False
365        out += ['']
366        return out
367
368    def _str_index(self):
369        idx = self['index']
370        out = []
371        out += ['.. index:: %s' % idx.get('default','')]
372        for section, references in idx.iteritems():
373            if section == 'default':
374                continue
375            out += ['   :%s: %s' % (section, ', '.join(references))]
376        return out
377
378    def __str__(self, func_role=''):
379        out = []
380        out += self._str_signature()
381        out += self._str_summary()
382        out += self._str_extended_summary()
383        for param_list in ('Parameters','Returns','Raises'):
384            out += self._str_param_list(param_list)
385        out += self._str_section('Warnings')
386        out += self._str_see_also(func_role)
387        for s in ('Notes','References','Examples'):
388            out += self._str_section(s)
389        out += self._str_index()
390        return '\n'.join(out)
391
392
393def indent(str,indent=4):
394    indent_str = ' '*indent
395    if str is None:
396        return indent_str
397    lines = str.split('\n')
398    return '\n'.join(indent_str + l for l in lines)
399
400def dedent_lines(lines):
401    """Deindent a list of lines maximally"""
402    return textwrap.dedent("\n".join(lines)).split("\n")
403
404def header(text, style='-'):
405    return text + '\n' + style*len(text) + '\n'
406
407
408class FunctionDoc(NumpyDocString):
409    def __init__(self, func, role='func', doc=None):
410        self._f = func
411        self._role = role # e.g. "func" or "meth"
412        if doc is None:
413            doc = inspect.getdoc(func) or ''
414        try:
415            NumpyDocString.__init__(self, doc)
416        except ValueError, e:
417            print '*'*78
418            print "ERROR: '%s' while parsing `%s`" % (e, self._f)
419            print '*'*78
420            #print "Docstring follows:"
421            #print doclines
422            #print '='*78
423
424        if not self['Signature']:
425            func, func_name = self.get_func()
426            try:
427                # try to read signature
428                argspec = inspect.getargspec(func)
429                argspec = inspect.formatargspec(*argspec)
430                argspec = argspec.replace('*','\*')
431                signature = '%s%s' % (func_name, argspec)
432            except TypeError, e:
433                signature = '%s()' % func_name
434            self['Signature'] = signature
435
436    def get_func(self):
437        func_name = getattr(self._f, '__name__', self.__class__.__name__)
438        if inspect.isclass(self._f):
439            func = getattr(self._f, '__call__', self._f.__init__)
440        else:
441            func = self._f
442        return func, func_name
443           
444    def __str__(self):
445        out = ''
446
447        func, func_name = self.get_func()
448        signature = self['Signature'].replace('*', '\*')
449
450        roles = {'func': 'function',
451                 'meth': 'method'}
452
453        if self._role:
454            if not roles.has_key(self._role):
455                print "Warning: invalid role %s" % self._role
456            out += '.. %s:: %s\n    \n\n' % (roles.get(self._role,''),
457                                             func_name)
458
459        out += super(FunctionDoc, self).__str__(func_role=self._role)
460        return out
461
462
463class ClassDoc(NumpyDocString):
464    def __init__(self,cls,modulename='',func_doc=FunctionDoc,doc=None):
465        if not inspect.isclass(cls):
466            raise ValueError("Initialise using a class. Got %r" % cls)
467        self._cls = cls
468
469        if modulename and not modulename.endswith('.'):
470            modulename += '.'
471        self._mod = modulename
472        self._name = cls.__name__
473        self._func_doc = func_doc
474
475        if doc is None:
476            doc = pydoc.getdoc(cls)
477
478        NumpyDocString.__init__(self, doc)
479
480    @property
481    def methods(self):
482        return [name for name,func in inspect.getmembers(self._cls)
483                if not name.startswith('_') and callable(func)]
484
485    def __str__(self):
486        out = ''
487        out += super(ClassDoc, self).__str__()
488        out += "\n\n"
489
490        #for m in self.methods:
491        #    print "Parsing `%s`" % m
492        #    out += str(self._func_doc(getattr(self._cls,m), 'meth')) + '\n\n'
493        #    out += '.. index::\n   single: %s; %s\n\n' % (self._name, m)
494
495        return out
496
497
Note: See TracBrowser for help on using the repository browser.