Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2@file
3@brief Modification to doxypy.py
4(which you can found here: http://code.foosel.org/doxypy).
6The main tweaks are:
8- the documentation is not moved before the function or class definition
9- it uses a function to modify every line of documentation to use rst syntax.
10"""
11from __future__ import print_function
12import sys
13import re
14from argparse import ArgumentParser as OptionParser
17_allowed = re.compile("^([a-zA-Z]:)?[^:*?\"<>|]+$")
20def is_file_string(s):
21 """
22 Says if the string s could be a filename.
24 @param s string
25 @return boolean
26 """
27 if len(s) >= 3000:
28 return False
29 global _allowed
30 if not _allowed.search(s):
31 return False
32 for c in s:
33 if ord(c) < 32:
34 return False
35 return True
38__applicationName__ = "doxypy"
39__blurb__ = """
40doxypy is an input filter for Doxygen. It preprocesses python
41files so that docstrings of classes and functions are reformatted
42into Doxygen-conform documentation blocks.
43"""
45__doc__ = __blurb__ + \
46 """
47In order to make Doxygen preprocess files through doxypy, simply
48add the following lines to your Doxyfile:
50 - ``FILTER_SOURCE_FILES = YES``
51 - ``INPUT_FILTER = "python /path/to/doxypy.py"``
52"""
54__version__ = "0.4.2"
55__date__ = "14th October 2009"
56__website__ = "http://code.foosel.org/doxypy"
58__author__ = (
59 "Philippe 'demod' Neumann (doxypy at demod dot org)",
60 "Gina 'foosel' Haeussge (gina at foosel dot net)"
61)
63__licenseName__ = "GPL v2"
64__license__ = """This program is free software: you can redistribute it and/or modify
65it under the terms of the GNU General Public License as published by
66the Free Software Foundation, either version 2 of the License, or
67(at your option) any later version.
69This program is distributed in the hope that it will be useful,
70but WITHOUT ANY WARRANTY; without even the implied warranty of
71MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
72GNU General Public License for more details.
74You should have received a copy of the GNU General Public License
75along with this program. If not, see `GNU <http://www.gnu.org/licenses/>`_.
76"""
79class FSM(object):
81 """
82 Implements a finite state machine.
84 Transitions are given as 4-tuples, consisting of an origin state, a target
85 state, a condition for the transition (given as a reference to a function
86 which gets called with a given piece of input) and a pointer to a function
87 to be called upon the execution of the given transition.
89 @var transitions holds the transitions
90 @var current_state holds the current state
91 @var current_input holds the current input
92 @var current_transition hold the currently active transition
93 """
95 def __init__(self, start_state=None, transitions=None):
96 if transitions is None:
97 transitions = []
98 self.transitions = transitions
99 self.current_state = start_state
100 self.current_input = None
101 self.current_transition = None
103 def setStartState(self, state):
104 self.current_state = state
106 def addTransition(self, from_state, to_state, condition, callback):
107 self.transitions.append([from_state, to_state, condition, callback])
109 def makeTransition(self, input):
110 """
111 Makes a transition based on the given input.
113 @param input input to parse by the FSM
114 """
115 for transition in self.transitions:
116 [from_state, to_state, condition, callback] = transition
117 if from_state == self.current_state:
118 match = condition(input)
119 if match:
120 self.current_state = to_state
121 self.current_input = input
122 self.current_transition = transition
123 if options.debug: # pragma: no cover
124 sys.stderr.write(
125 "# FSM: executing ({} -> {}) for line '{}'\n".format(from_state, to_state, input))
126 callback(match)
127 return
130class Doxypy(object):
132 def __init__(self, print_output, process_comment, information):
133 """
134 Constructor for Doxypy.
136 @param print_output function which will receive the output
137 @param process_comment function applied to the help to modify it
138 @param information a dictionary with additional information such
139 as filename, first_row
140 """
141 string_prefixes = "[uU]?[rR]?"
143 self.start_single_comment_re = re.compile(
144 "^\\s*%s(''')" % string_prefixes)
145 self.end_single_comment_re = re.compile("(''')\\s*$")
147 self.start_double_comment_re = re.compile(
148 "^\\s*%s(\"\"\")" % string_prefixes)
149 self.end_double_comment_re = re.compile("(\"\"\")\\s*$")
151 self.single_comment_re = re.compile(
152 "^\\s*%s(''').*(''')\\s*$" % string_prefixes)
153 self.double_comment_re = re.compile(
154 "^\\s*%s(\"\"\").*(\"\"\")\\s*$" % string_prefixes)
156 self.defclass_re = re.compile(
157 "^(\\s*)(def .+:|class .+:)\\s*([#].*?)?$")
158 self.empty_re = re.compile("^\\s*$")
159 self.hashline_re = re.compile("^\\s*#.*$")
160 self.importline_re = re.compile("^\\s*(import |from .+ import)")
162 self.multiline_defclass_start_re = re.compile(
163 "^(\\s*)(def|class)(\\s.*)?$")
164 self.multiline_defclass_end_re = re.compile(":\\s*([#].*?)?$")
165 self.print_output = print_output
166 self.process_comment = process_comment
167 self.information = information
169 # Transition list format
170 # ["FROM", "TO", condition, action]
171 transitions = [
172 # FILEHEAD
174 # single line comments
175 ["FILEHEAD", "FILEHEAD", self.single_comment_re.search,
176 self.appendCommentLine],
177 ["FILEHEAD", "FILEHEAD", self.double_comment_re.search,
178 self.appendCommentLine],
180 # multiline comments
181 ["FILEHEAD", "FILEHEAD_COMMENT_SINGLE",
182 self.start_single_comment_re.search, self.appendCommentLine],
183 ["FILEHEAD_COMMENT_SINGLE", "FILEHEAD",
184 self.end_single_comment_re.search, self.appendCommentLine],
185 ["FILEHEAD_COMMENT_SINGLE", "FILEHEAD_COMMENT_SINGLE",
186 self.catchall, self.appendCommentLine],
187 ["FILEHEAD", "FILEHEAD_COMMENT_DOUBLE",
188 self.start_double_comment_re.search, self.appendCommentLine],
189 ["FILEHEAD_COMMENT_DOUBLE", "FILEHEAD",
190 self.end_double_comment_re.search, self.appendCommentLine],
191 ["FILEHEAD_COMMENT_DOUBLE", "FILEHEAD_COMMENT_DOUBLE",
192 self.catchall, self.appendCommentLine],
194 # other lines
195 ["FILEHEAD", "FILEHEAD", self.empty_re.search,
196 self.appendFileheadLine],
197 ["FILEHEAD", "FILEHEAD", self.hashline_re.search,
198 self.appendFileheadLine],
199 ["FILEHEAD", "FILEHEAD", self.importline_re.search,
200 self.appendFileheadLine],
201 ["FILEHEAD", "DEFCLASS", self.defclass_re.search,
202 self.resetCommentSearch],
203 ["FILEHEAD", "DEFCLASS_MULTI",
204 self.multiline_defclass_start_re.search, self.resetCommentSearch],
205 ["FILEHEAD", "DEFCLASS_BODY",
206 self.catchall, self.appendFileheadLine],
208 # DEFCLASS
210 # single line comments
211 ["DEFCLASS", "DEFCLASS_BODY",
212 self.single_comment_re.search, self.appendCommentLine],
213 ["DEFCLASS", "DEFCLASS_BODY",
214 self.double_comment_re.search, self.appendCommentLine],
216 # multiline comments
217 ["DEFCLASS", "COMMENT_SINGLE",
218 self.start_single_comment_re.search, self.appendCommentLine],
219 ["COMMENT_SINGLE", "DEFCLASS_BODY",
220 self.end_single_comment_re.search, self.appendCommentLine],
221 ["COMMENT_SINGLE", "COMMENT_SINGLE",
222 self.catchall, self.appendCommentLine],
223 ["DEFCLASS", "COMMENT_DOUBLE",
224 self.start_double_comment_re.search, self.appendCommentLine],
225 ["COMMENT_DOUBLE", "DEFCLASS_BODY",
226 self.end_double_comment_re.search, self.appendCommentLine],
227 ["COMMENT_DOUBLE", "COMMENT_DOUBLE",
228 self.catchall, self.appendCommentLine],
230 # other lines
231 ["DEFCLASS", "DEFCLASS", self.empty_re.search,
232 self.appendDefclassLine],
233 ["DEFCLASS", "DEFCLASS", self.defclass_re.search,
234 self.resetCommentSearch],
235 ["DEFCLASS", "DEFCLASS_MULTI",
236 self.multiline_defclass_start_re.search, self.resetCommentSearch],
237 ["DEFCLASS", "DEFCLASS_BODY",
238 self.catchall, self.stopCommentSearch],
240 # DEFCLASS_BODY
242 ["DEFCLASS_BODY", "DEFCLASS",
243 self.defclass_re.search, self.startCommentSearch],
244 ["DEFCLASS_BODY", "DEFCLASS_MULTI",
245 self.multiline_defclass_start_re.search, self.startCommentSearch],
246 ["DEFCLASS_BODY", "DEFCLASS_BODY",
247 self.catchall, self.appendNormalLine],
249 # DEFCLASS_MULTI
250 ["DEFCLASS_MULTI", "DEFCLASS",
251 self.multiline_defclass_end_re.search, self.appendDefclassLine],
252 ["DEFCLASS_MULTI", "DEFCLASS_MULTI",
253 self.catchall, self.appendDefclassLine],
254 ]
256 self.fsm = FSM("FILEHEAD", transitions)
257 self.outstream = sys.stdout
259 self.output = []
260 self.comment = []
261 self.filehead = []
262 self.defclass = []
263 self.indent = ""
265 def __closeComment(self):
266 """
267 Appends any open comment block and triggering block to the output.
268 """
270 if options.autobrief:
271 if len(self.comment) == 1 \
272 or (len(self.comment) > 2 and self.comment[1].strip() == ''):
273 self.comment[0] = self.__docstringSummaryToBrief(
274 self.comment[0])
276 if self.defclass:
277 self.output.extend(self.defclass)
279 if self.comment:
280 block = self.makeCommentBlock()
281 self.output.extend(block)
283 def __docstringSummaryToBrief(self, line):
284 """
285 Adds \\brief to the docstrings summary line.
287 A \\brief is prepended, provided no other doxygen command is at the
288 start of the line.
289 """
290 stripped = line.strip()
291 if stripped and not stripped[0] in ('@', '\\'):
292 return "@brief " + line
293 else:
294 return line
296 def __flushBuffer(self):
297 """
298 Flushes the current outputbuffer to the outstream.
299 """
300 if self.output:
301 if options.debug: # pragma: no cover
302 sys.stderr.write("# OUTPUT: {0}\n".format(self.output))
303 self.print_output("\n".join(self.output), file=self.outstream)
304 self.outstream.flush()
305 self.output = []
307 def catchall(self, input):
308 """
309 The catchall-condition, always returns true.
310 """
311 return True
313 def resetCommentSearch(self, match):
314 """
315 Restarts a new comment search for a different triggering line.
317 Closes the current commentblock and starts a new comment search.
318 """
319 if options.debug: # pragma: no cover
320 sys.stderr.write("# CALLBACK: resetCommentSearch")
321 self.__closeComment()
322 self.startCommentSearch(match)
324 def startCommentSearch(self, match):
325 """
326 Starts a new comment search.
328 Saves the triggering line, resets the current comment and saves
329 the current indentation.
330 """
331 if options.debug: # pragma: no cover
332 sys.stderr.write("# CALLBACK: startCommentSearch")
333 self.defclass = [self.fsm.current_input]
334 self.comment = []
335 self.indent = match.group(1)
337 def stopCommentSearch(self, match):
338 """
339 Stops a comment search.
341 Closes the current commentblock, resets the triggering line and
342 appends the current line to the output.
343 """
344 if options.debug: # pragma: no cover
345 sys.stderr.write("# CALLBACK: stopCommentSearch")
346 self.__closeComment()
348 self.defclass = []
349 self.output.append(self.fsm.current_input)
351 def appendFileheadLine(self, match):
352 """
353 Appends a line in the FILEHEAD state.
355 Closes the open comment block, resets it and appends the current line.
356 """
357 if options.debug: # pragma: no cover
358 sys.stderr.write("# CALLBACK: appendFileheadLine")
359 self.__closeComment()
360 self.comment = []
361 self.output.append(self.fsm.current_input)
363 def appendCommentLine(self, match):
364 """
365 Appends a comment line.
367 The comment delimiter is removed from multiline start and ends as
368 well as singleline comments.
369 """
370 if options.debug: # pragma: no cover
371 sys.stderr.write("# CALLBACK: appendCommentLine")
372 from_state, to_state, condition, callback = self.fsm.current_transition # pylint: disable=W0612
374 # single line comment
375 if (from_state == "DEFCLASS" and to_state == "DEFCLASS_BODY") \
376 or (from_state == "FILEHEAD" and to_state == "FILEHEAD"):
377 # remove comment delimiter from begin and end of the line
378 activeCommentDelim = match.group(1)
379 line = self.fsm.current_input
380 self.comment.append(line[line.find(
381 activeCommentDelim) + len(activeCommentDelim):line.rfind(activeCommentDelim)])
383 if (to_state == "DEFCLASS_BODY"):
384 self.__closeComment()
385 self.defclass = []
386 # multiline start
387 elif from_state in ("DEFCLASS", "FILEHEAD"):
388 # remove comment delimiter from begin of the line
389 activeCommentDelim = match.group(1)
390 line = self.fsm.current_input
391 self.comment.append(
392 line[line.find(activeCommentDelim) + len(activeCommentDelim):])
393 # multiline end
394 elif to_state in ("DEFCLASS_BODY", "FILEHEAD"):
395 # remove comment delimiter from end of the line
396 activeCommentDelim = match.group(1)
397 line = self.fsm.current_input
398 self.comment.append(line[0:line.rfind(activeCommentDelim)])
399 if (to_state == "DEFCLASS_BODY"):
400 self.__closeComment()
401 self.defclass = []
402 # in multiline comment
403 else:
404 # just append the comment line
405 self.comment.append(self.fsm.current_input)
407 def appendNormalLine(self, match):
408 """
409 Appends a line to the output.
410 """
411 if options.debug: # pragma: no cover
412 self.print_output("# CALLBACK: appendNormalLine", file=sys.stderr)
413 self.output.append(self.fsm.current_input)
415 def appendDefclassLine(self, match):
416 """
417 Appends a line to the triggering block.
418 """
419 if options.debug: # pragma: no cover
420 self.print_output(
421 "# CALLBACK: appendDefclassLine", file=sys.stderr)
422 self.defclass.append(self.fsm.current_input)
424 def makeCommentBlock(self):
425 """
426 Indents the current comment block with respect to the current
427 indentation level.
429 @returns a list of indented comment lines
430 """
431 if options.debug: # pragma: no cover
432 self.print_output("# makeCommentBlock", file=sys.stderr)
433 indent4 = " " if len(self.defclass) > 0 else ""
434 doxyStart = "%s\"\"\"" % indent4
435 doxyEnd = "%s\"\"\"" % indent4
437 commentLines = self.comment
438 full_indent = self.indent + indent4
439 if full_indent:
440 if commentLines and commentLines[0].lstrip() == commentLines[0]:
441 commentLines[0] = full_indent + commentLines[0]
442 # commentLines = ["%s%s%s" % (self.indent, indent4, x) for x in commentLines]
444 commentLines = self.process_comment(commentLines,
445 self.information.get(
446 "first_row", 0) + self._index_row + 1,
447 self.information.get("filename", "filename is not present"))
449 # We remove the indentation.
450 while commentLines and commentLines[0].strip() == '':
451 del commentLines[0]
452 while commentLines and commentLines[-1].strip() == '':
453 del commentLines[-1]
455 # Back to doxypy.
456 li = [self.indent + doxyStart]
457 li.extend(commentLines)
458 li.append(self.indent + doxyEnd)
460 return li
462 def parse(self, input):
463 """
464 Parses a python file given as input string and returns the doxygen-
465 compatible representation.
467 @param input the python code to parse
468 @returns the modified python code
469 """
470 lines = input.split("\n")
472 for line in lines:
473 self.fsm.makeTransition(line)
475 if self.fsm.current_state == "DEFCLASS":
476 self.__closeComment()
478 return "\n".join(self.output)
480 def parseFile(self, filename):
481 """
482 Parses a :epkg:`python` file given as input string and returns`
483 the :epkg:`doxygen` compatible representation.
485 @param filename the :epkg:`python` code to parse (filename)
486 """
487 self._index_row = 0
488 if isinstance(filename, list):
489 for line in filename:
490 self.parseLine(line.rstrip('\r\n'))
491 self._index_row += 1
492 else:
493 import os
494 if is_file_string(filename) and os.path.exists(filename):
495 with open(filename, 'r') as fh:
496 for line in fh:
497 self.parseLine(line.rstrip('\r\n'))
498 self._index_row += 1
499 else:
500 for line in filename.split("\n"):
501 self.parseLine(line.rstrip('\r\n'))
502 self._index_row += 1
504 if self.fsm.current_state == "DEFCLASS":
505 self.__closeComment()
506 self.__flushBuffer()
508 def parseLine(self, line):
509 """
510 Parses one line of python and flush the resulting output to the
511 outstream.
513 @param line the python code line to parse
514 """
515 self.fsm.makeTransition(line)
516 self.__flushBuffer()
519def optParse():
520 """
521 Parses commandline options.
522 """
523 parser = OptionParser(prog=__applicationName__)
525 # parser.set_usage("%prog [options] filename")
526 parser.add_argument("--autobrief",
527 action="store_true", dest="autobrief",
528 help="use the docstring summary line as @brief description",
529 version="%prog " + __version__
530 )
531 parser.add_argument("--debug",
532 action="store_true", dest="debug",
533 help="enable debug output on stderr",
534 version="%prog " + __version__
535 )
537 # parse options
538 global options
539 (options, filename) = parser.parse_args()
541 if not filename:
542 sys.stderr.write("No filename given.")
543 sys.exit(-1)
545 return filename[0]
548def main(file=None, print_output=None):
549 """
550 Starts the parser on the file given by the filename as the first
551 argument on the commandline.
553 @param file if equal to None, take this one on the command line
554 @param print_output every string is sent to that funtion
555 """
556 filename = file if file is not None else optParse()
557 fsm = Doxypy(print_output, lambda a, b, c: a, {})
558 fsm.parseFile(filename)
561class Opt:
563 def __init__(self):
564 self.debug = False
565 self.autobrief = True
568options = Opt()
571def process_string(content, print_output, process_comment, filename, first_row, debug=False):
572 """
573 Applies the doxypy like process to a string.
575 @param content string
576 @param print_output every string is sent to that funtion
577 @param process_comment function applied to the help to modifies it
578 @param filename or None if there is no file
579 @param first_row True: to display error messages
580 @param debug if True, display more information
581 """
582 options.debug = debug
583 fsm = Doxypy(print_output, process_comment,
584 {"filename": filename, "first_row": first_row})
585 fsm.parseFile(content)
588if __name__ == "__main__":
589 with open(__file__, "r") as f:
590 local_content = f.read()
591 # main(__file__, print_output = print)
593 def pprint(*la, **kw):
594 print(*la, **kw)
596 process_string(local_content, pprint, lambda a, b, c: a, "", 0)