File: auto_rst4api.py

package info (click to toggle)
taurus 3.0.0-2
  • links: PTS
  • area: main
  • in suites: wheezy
  • size: 87,664 kB
  • sloc: python: 56,016; sh: 16; makefile: 14
file content (369 lines) | stat: -rw-r--r-- 18,127 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
#!/usr/bin/env python
# -*- coding: utf-8 -*-

#############################################################################
##
## This file is part of Taurus, a Tango User Interface Library
## 
## http://www.tango-controls.org/static/taurus/latest/doc/html/index.html
##
## Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
## 
## Taurus is free software: you can redistribute it and/or modify
## it under the terms of the GNU Lesser General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
## 
## Taurus is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU Lesser General Public License for more details.
## 
## You should have received a copy of the GNU Lesser General Public License
## along with Taurus.  If not, see <http://www.gnu.org/licenses/>.
##
###########################################################################

''' Creates a tree of dirs and restructured text stub files for documenting 
the API of a python module with sphinx'''

import sys, os, inspect, glob, re
from jinja2 import Environment, FileSystemLoader


class Auto_rst4API_Creator(object):
    AUTOGEN_SIGNATURE = '.. AUTO_RST4API'
    AUTOGEN_MESSAGE ='.. This file was generated by auto_rst4api.py. Changes may be lost'
    
    def __init__(self, templatespath='./', moduletemplate='api_module.rst', classtemplate='api_class.rst',
                 classindextemplate='api_AllClasses.rst', exclude_patterns=(), verbose=True, overwrite_old=False):
        '''
        :param templates: (str) path to dir where template files are located
        :param moduletemplate: (str) name of the template to be used for module pages
        :param classtemplate: (str) name of the template to be used for class pages
        :param classindextemplate: (str) name of the template to be used for class index page
        :param exclude_patterns: (seq<str>) sequence of strings containing regexp
                                 patterns. Each candidate to be documented will be 
                                 matched against these patterns and will be excluded
                                 from documentation if it matches any of them.
        :param verbose: (bool) If True (default) status messages will be printed to stdout
        '''
        self.env = Environment(loader=FileSystemLoader(templatespath))
        self.moduletemplate = self.env.get_template(moduletemplate)
        self.classtemplate = self.env.get_template(classtemplate)
        self.classindextemplate = self.env.get_template(classindextemplate)
        self.exclude_patterns = [re.compile(p) for p in exclude_patterns]
        self.verbose = verbose
        self.overwrite_old = overwrite_old
    
    def _matchesAnyPattern(self, name, paterns):
        for p in paterns:
            if re.match(p,name) is not None:
                if self.verbose: print 'excluding "%s"'%name
                return True
        return False
    
    def _getlocalmembernames(self, module, predicate=None):
        ret =[]
        modulepath, tail = os.path.split(inspect.getabsfile(module))
        for n,v in inspect.getmembers(module, predicate):
            if inspect.isbuiltin(v): 
                continue #ignore builtin functions
            try: 
                memberpath, tail = os.path.split(inspect.getabsfile(v))
            except TypeError: 
                continue #ignore builtin modules
            if memberpath == modulepath: 
                ret.append(n)
        return ret
    
    def _getSubmodulesFromPath(self, modulepath):
        g = glob.glob(os.path.join(modulepath,'*','__init__.py'))
        ret = [re.findall(r".+\/(.*)\/__init__.py", s)[0] for s in g]
        return ret
    
    def _isclass_with_init(self, obj):
        return inspect.isclass(obj) and hasattr(obj,'__init__')
    
    def _isautogeneratedfile(self, fname):
        ret = False
        f = open(fname,'r')
        lines = f.readlines()
        for l in lines:
            if l.startswith(self.AUTOGEN_SIGNATURE):
                ret = True
                break
        f.close()
        
        return ret
    
    def _isenumeration(self, obj):
#        return isinstance(obj, taurus.core.util)
        return False #@todo
    
    def cleanAutogenerated(self, apipath):
        '''Removes any previously autogenerated rst file in the given path 
        or its subdirectories
        
        :param apipath: (str) directory to clean
        '''
        for dirpath,dirnames,filenames in os.walk(apipath):
            for f in filenames:
                if f.endswith('.rst'):
                    fullname = os.path.join(dirpath,f)
                    try :
                        if self._isautogeneratedfile(fullname):
                            print "Removing %s"%fullname
                            os.remove(fullname)
                    except Exception, e:
                        print 'Error accessing %s:%s'%(fullname,repr(e))

    def getAll(self, info, key):
        mname = info['modulename']
        try:
            ret = [(mname,el) for el in info[key]]
        except KeyError:
            return []
        for sminfo in info['submodules'].itervalues():
            ret += self.getAll(sminfo,key)
        return ret
        
    def createClassIndex(self, info, ofname ):
        '''
        Creates a class index page using the classindextemplate.
        
        :param info: (dict) dictionary containing the information about the
                     items to document for this module (as generated by 
                     :meth:`exploreModule`
        :param ofname: (str) output file name       
        '''
        classes = self.getAll(info, 'localclassnames') #this is a list of tuples of (modulename,class)
        classes = sorted(classes, key=lambda item: item[1]) #sort it by class name
        classes = ['.'.join((m,c)) for m,c in classes] # make a full classname 
        if self.verbose: print 'creating "%s" ...'%ofname, 
        if not os.path.exists(ofname) or (self.overwrite_old and self._isautogeneratedfile(ofname)):
            text = self.classindextemplate.render(info=info, classes=classes)
            f = open(ofname, "w")
            f.write('\n'.join((self.AUTOGEN_SIGNATURE, self.AUTOGEN_MESSAGE, text)))
            f.close()
            if self.verbose: print ' ok.'
        else:
            if self.verbose: print ' skipping (file already exists)'
            
            
    def exploreModule(self, modulename):
        '''Recursive function that gathers info on a module and all its submodules.
        
        :param modulename: the name of the module to explore
        
        :return: (dict<str,object>) a dictionary containing submodulenames, 
                 localclassnames, localfunctionnames, localenumerationnames,
                 externalmembernames, submodules, warnings
        '''
        if self.verbose: print "Exploring %s..."%modulename  
        warnings = []
        try:
          module = __import__(modulename, fromlist = [''])
        except Exception,e:
          msg = 'exploreModule: WARNING: skipping documentation for %s. Reason: %s'%(modulename,repr(e))
          warnings.append(msg)
          if self.verbose: print msg
          return dict(modulename = modulename,
                    basemodulename = modulename.split('.')[-1],
                    modulepath = None,
                    submodulenames = [], 
                    localclassnames = [], 
                    localfunctionnames = [],
                    localenumerationnames = [],
                    externalmembernames = [],
                    submodules = {},
                    warnings = warnings)
        modulepath, tail = os.path.split(inspect.getabsfile(module))
        
        submodulenames = sorted(self._getSubmodulesFromPath(modulepath))
        localclassnames =  sorted(self._getlocalmembernames(module, self._isclass_with_init)) 
        localfunctionnames = sorted(self._getlocalmembernames(module, inspect.isfunction))
        localenumerationnames = sorted([])#@todo
        externalmembernames = sorted([]) #@todo
#        localmembers = list(submodules) + localfunctionnames + localclassnames + localenumerationnames
#        externalmembers = [n for n,v in inspect.getmembers(object) if (n not in localmembers and not n.startswith('_'))]
        
        #filter out excluded members
        submodulenames = [n for n in submodulenames if not self._matchesAnyPattern(os.path.join(modulename,n), self.exclude_patterns)]
        localclassnames = [n for n in localclassnames if not self._matchesAnyPattern(os.path.join(modulename,n), self.exclude_patterns)]
        localfunctionnames = [n for n in localfunctionnames if not self._matchesAnyPattern(os.path.join(modulename,n), self.exclude_patterns)]
        localenumerationnames = [n for n in localenumerationnames if not self._matchesAnyPattern(os.path.join(modulename,n), self.exclude_patterns)]
        externalmembernames = [n for n in externalmembernames if not self._matchesAnyPattern(os.path.join(modulename,n), self.exclude_patterns)]
        
        #recurse
        submodules = {}
        for n in submodulenames:
            sm_name = '.'.join((modulename, n))
            submodules[n] = self.exploreModule(sm_name)
        
        return dict(modulename = modulename,
                    basemodulename = modulename.split('.')[-1],
                    modulepath = modulepath,
                    submodulenames = submodulenames, 
                    localclassnames = localclassnames, 
                    localfunctionnames = localfunctionnames,
                    localenumerationnames = localenumerationnames,
                    externalmembernames = externalmembernames,
                    submodules = submodules,
                    warnings = warnings)
    
    def createStubs(self, info, docparentpath):
        '''creates rst stub files for modules and classes according to the
        information contained in info.

        :param info: (dict) dictionary containing the information about the
                     items to document for this module (as generated by 
                     :meth:`exploreModule`)
        :docparentpath: (str) path to the directory in which the documentation 
                        files will be written   
        '''
        #create the module doc dir if it didn't exist
        absdocpath = os.path.join(docparentpath, info['basemodulename'])   
        if not os.path.exists(absdocpath):
            os.makedirs(absdocpath, mode=0755)
        #create module index stub in doc parent dir
        ofname = os.path.join(docparentpath,"%s.rst"%info['basemodulename'])
        if self.verbose: print 'creating "%s" ...'%ofname, 
        if not os.path.exists(ofname) or (self.overwrite_old and self._isautogeneratedfile(ofname)):
            text = self.moduletemplate.render(info=info)
            f = open(ofname, "w")
            f.write('\n'.join((self.AUTOGEN_SIGNATURE, self.AUTOGEN_MESSAGE, text)))
            f.close()
            if self.verbose: print ' ok.'
        else:
            if self.verbose: print ' skipping (file already exists)'
        #create class stubs
        for name in info['localclassnames']:
            ofname = os.path.join(absdocpath,"_%s.rst"%name)
            if self.verbose: print 'creating "%s" ...'%ofname, 
            if not os.path.exists(ofname) or (self.overwrite_old and self._isautogeneratedfile(ofname)):
                text = self.classtemplate.render(info=info, classname=name)
                f = open(ofname, "w")
                f.write('\n'.join((self.AUTOGEN_SIGNATURE, self.AUTOGEN_MESSAGE, text)))
                f.close()
                if self.verbose: print ' ok.'
            else:
                if self.verbose: print ' skipping (file already exists)'
        #recurse for submodules
        for sminfo in info['submodules'].itervalues():
            self.createStubs(sminfo, absdocpath)
    
    def documentModule(self, modulename, docparentpath):
        '''
        recursive function that walks on the module structure and generates
        documentation files for the given module and its submodules. It also
        creates a class index for the root module
        
        :param modulename: (str) name of the module to document
        :docparentpath: (str) path to the directory in which the documentation 
                        files will be written
                        
        :return: (list<str>) list of warning messages 
        '''
        if self.verbose: print "\nDocumenting %s..."%modulename  
        moduleinfo = self.exploreModule(modulename)
        self.createStubs(moduleinfo, docparentpath)
        self.createClassIndex(moduleinfo, os.path.join(docparentpath,"%s_AllClasses.rst"%modulename))
        w=self.getAll(moduleinfo, 'warnings')
        if len (w) == 0: return []
        else: return zip(*w)[1]
        

        
#    def document_module_OLD(self, modulename, docparentpath):
#        '''
#        recursive function that walks on the module structure and generates 
#        documentation files for the given module and its submodules
#        
#        :param modulename: (str) name of the module to document
#        :docparentpath: (str) path to the directory in which the documentation 
#                        files will be written
#                        
#        :return: (list<str>) list of warning messages 
#        '''
#        if self.verbose: print "\nDocumenting %s..."%modulename  
#        try:
#          module = __import__(modulename, fromlist = [''])
#        except Exception,e:
#          msg = 'WARNING: skipping documentation for %s. Reason: %s'%(modulename,repr(e))
#          self.warnings.append(msg)
#          if self.verbose: print msg
#          return self.warnings
#        modulepath, tail = os.path.split(inspect.getabsfile(module))
#        basemodulename = modulename.split('.')[-1]
#        absdocpath = os.path.join(docparentpath, basemodulename)
#        
#        submodules = self._getSubmodulesFromPath(modulepath)
#        localclassnames =  self._getlocalmembernames(module, self._isclass_with_init)  
#        localfunctionnames = self._getlocalmembernames(module, inspect.isfunction)
#        localenumerationnames = []#@todo
#        externalmembers = [] #@todo
##        localmembers = list(submodules) + localfunctionnames + localclassnames + localenumerationnames
##        externalmembers = [n for n,v in inspect.getmembers(object) if (n not in localmembers and not n.startswith('_'))]
#        
#        #filter out excluded members
#        submodules = [n for n in submodules if not self._matchesAnyPattern(os.path.join(modulename,n), self.exclude_patterns)]
#        localclassnames = [n for n in localclassnames if not self._matchesAnyPattern(os.path.join(modulename,n), self.exclude_patterns)]
#        localfunctionnames = [n for n in localfunctionnames if not self._matchesAnyPattern(os.path.join(modulename,n), self.exclude_patterns)]
#        localenumerationnames = [n for n in localenumerationnames if not self._matchesAnyPattern(os.path.join(modulename,n), self.exclude_patterns)]
#        externalmembers = [n for n in externalmembers if not self._matchesAnyPattern(os.path.join(modulename,n), self.exclude_patterns)]
#        
#        #create the module doc dir if it didn't exist
#        if not os.path.exists(absdocpath):
#            os.makedirs(absdocpath, mode=0755)
#        #create module index stub in doc parent dir
#        text = self.moduletemplate.render(modulename=modulename,
#                                 basemodulename=basemodulename,
#                                 submodules=submodules,
#                                 localclassnames=localclassnames,
#                                 localfunctionnames=localfunctionnames,
#                                 localenumerationnames=localenumerationnames,
#                                 externalmembers = externalmembers
#                                 )
#        ofname = os.path.join(docparentpath,"%s.rst"%basemodulename)
#        if self.verbose: print 'creating "%s" ...'%ofname, 
#        if os.path.exists(ofname) and self._isautogeneratedfile(ofname):
#            if self.verbose: print ' skipping (manually edited file already exists)'
#        else:
#            f = open(ofname, "w")
#            f.write('\n'.join((self.AUTOGEN_SIGNATURE, self.AUTOGEN_MESSAGE, text)))
#            f.close()
#            if self.verbose: print ' ok.'
#        #create class stubs
#        for name in localclassnames:
#            text = self.classtemplate.render(classname=name, modulename=modulename)
#            ofname = os.path.join(absdocpath,"_%s.rst"%name)
#            if self.verbose: print 'creating "%s" ...'%ofname, 
#            if os.path.exists(ofname) and self._isautogeneratedfile(ofname):
#                if self.verbose: print ' skipping (manually edited file already exists)'
#            else:
#                f = open(ofname, "w")
#                f.write('\n'.join((self.AUTOGEN_SIGNATURE, self.AUTOGEN_MESSAGE, text)))
#                f.close()
#                if self.verbose: print ' ok.'
#        #recurse for submodules
#        for sm in submodules:
#        	sm_name = '.'.join((modulename, sm))
#        	self.documentModule(sm_name, absdocpath)
#        return self.warnings
        
def main():    
    import sys
    if len(sys.argv) != 3:
        print 'Usage:\n\t%s modulename docpreffix\n\n'%sys.argv[0]
        sys.exit(1)            
    modulename, docparentpath = sys.argv[1:]
    creator = Auto_rst4API_Creator(exclude_patterns = ['.*/ui'], verbose=True)
    r = creator.documentModule(modulename, docparentpath)
    print '\n\n'+'*'*50
    print "Auto Creation of API docs for %s Finished with %i warnings:"%(modulename,len(r))
    print '\n'.join(r)
    print '*'*50+'\n'
    
if __name__ == "__main__":
    main()