Hide keyboard shortcuts

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). 

5 

6The main tweaks are: 

7 

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 

15 

16 

17_allowed = re.compile("^([a-zA-Z]:)?[^:*?\"<>|]+$") 

18 

19 

20def is_file_string(s): 

21 """ 

22 Says if the string s could be a filename. 

23 

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 

36 

37 

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""" 

44 

45__doc__ = __blurb__ + \ 

46 """ 

47In order to make Doxygen preprocess files through doxypy, simply 

48add the following lines to your Doxyfile: 

49 

50 - ``FILTER_SOURCE_FILES = YES`` 

51 - ``INPUT_FILTER = "python /path/to/doxypy.py"`` 

52""" 

53 

54__version__ = "0.4.2" 

55__date__ = "14th October 2009" 

56__website__ = "http://code.foosel.org/doxypy" 

57 

58__author__ = ( 

59 "Philippe 'demod' Neumann (doxypy at demod dot org)", 

60 "Gina 'foosel' Haeussge (gina at foosel dot net)" 

61) 

62 

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. 

68 

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. 

73 

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""" 

77 

78 

79class FSM(object): 

80 

81 """ 

82 Implements a finite state machine. 

83 

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. 

88 

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 """ 

94 

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 

102 

103 def setStartState(self, state): 

104 self.current_state = state 

105 

106 def addTransition(self, from_state, to_state, condition, callback): 

107 self.transitions.append([from_state, to_state, condition, callback]) 

108 

109 def makeTransition(self, input): 

110 """ 

111 Makes a transition based on the given input. 

112 

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 

128 

129 

130class Doxypy(object): 

131 

132 def __init__(self, print_output, process_comment, information): 

133 """ 

134 Constructor for Doxypy. 

135 

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]?" 

142 

143 self.start_single_comment_re = re.compile( 

144 "^\\s*%s(''')" % string_prefixes) 

145 self.end_single_comment_re = re.compile("(''')\\s*$") 

146 

147 self.start_double_comment_re = re.compile( 

148 "^\\s*%s(\"\"\")" % string_prefixes) 

149 self.end_double_comment_re = re.compile("(\"\"\")\\s*$") 

150 

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) 

155 

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)") 

161 

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 

168 

169 # Transition list format 

170 # ["FROM", "TO", condition, action] 

171 transitions = [ 

172 # FILEHEAD 

173 

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], 

179 

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], 

193 

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], 

207 

208 # DEFCLASS 

209 

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], 

215 

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], 

229 

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], 

239 

240 # DEFCLASS_BODY 

241 

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], 

248 

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 ] 

255 

256 self.fsm = FSM("FILEHEAD", transitions) 

257 self.outstream = sys.stdout 

258 

259 self.output = [] 

260 self.comment = [] 

261 self.filehead = [] 

262 self.defclass = [] 

263 self.indent = "" 

264 

265 def __closeComment(self): 

266 """ 

267 Appends any open comment block and triggering block to the output. 

268 """ 

269 

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]) 

275 

276 if self.defclass: 

277 self.output.extend(self.defclass) 

278 

279 if self.comment: 

280 block = self.makeCommentBlock() 

281 self.output.extend(block) 

282 

283 def __docstringSummaryToBrief(self, line): 

284 """ 

285 Adds \\brief to the docstrings summary line. 

286 

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 

295 

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 = [] 

306 

307 def catchall(self, input): 

308 """ 

309 The catchall-condition, always returns true. 

310 """ 

311 return True 

312 

313 def resetCommentSearch(self, match): 

314 """ 

315 Restarts a new comment search for a different triggering line. 

316 

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) 

323 

324 def startCommentSearch(self, match): 

325 """ 

326 Starts a new comment search. 

327 

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) 

336 

337 def stopCommentSearch(self, match): 

338 """ 

339 Stops a comment search. 

340 

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() 

347 

348 self.defclass = [] 

349 self.output.append(self.fsm.current_input) 

350 

351 def appendFileheadLine(self, match): 

352 """ 

353 Appends a line in the FILEHEAD state. 

354 

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) 

362 

363 def appendCommentLine(self, match): 

364 """ 

365 Appends a comment line. 

366 

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 

373 

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)]) 

382 

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) 

406 

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) 

414 

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) 

423 

424 def makeCommentBlock(self): 

425 """ 

426 Indents the current comment block with respect to the current 

427 indentation level. 

428 

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 

436 

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] 

443 

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")) 

448 

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] 

454 

455 # Back to doxypy. 

456 li = [self.indent + doxyStart] 

457 li.extend(commentLines) 

458 li.append(self.indent + doxyEnd) 

459 

460 return li 

461 

462 def parse(self, input): 

463 """ 

464 Parses a python file given as input string and returns the doxygen- 

465 compatible representation. 

466 

467 @param input the python code to parse 

468 @returns the modified python code 

469 """ 

470 lines = input.split("\n") 

471 

472 for line in lines: 

473 self.fsm.makeTransition(line) 

474 

475 if self.fsm.current_state == "DEFCLASS": 

476 self.__closeComment() 

477 

478 return "\n".join(self.output) 

479 

480 def parseFile(self, filename): 

481 """ 

482 Parses a :epkg:`python` file given as input string and returns` 

483 the :epkg:`doxygen` compatible representation. 

484 

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 

503 

504 if self.fsm.current_state == "DEFCLASS": 

505 self.__closeComment() 

506 self.__flushBuffer() 

507 

508 def parseLine(self, line): 

509 """ 

510 Parses one line of python and flush the resulting output to the 

511 outstream. 

512 

513 @param line the python code line to parse 

514 """ 

515 self.fsm.makeTransition(line) 

516 self.__flushBuffer() 

517 

518 

519def optParse(): 

520 """ 

521 Parses commandline options. 

522 """ 

523 parser = OptionParser(prog=__applicationName__) 

524 

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 ) 

536 

537 # parse options 

538 global options 

539 (options, filename) = parser.parse_args() 

540 

541 if not filename: 

542 sys.stderr.write("No filename given.") 

543 sys.exit(-1) 

544 

545 return filename[0] 

546 

547 

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. 

552 

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) 

559 

560 

561class Opt: 

562 

563 def __init__(self): 

564 self.debug = False 

565 self.autobrief = True 

566 

567 

568options = Opt() 

569 

570 

571def process_string(content, print_output, process_comment, filename, first_row, debug=False): 

572 """ 

573 Applies the doxypy like process to a string. 

574 

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) 

586 

587 

588if __name__ == "__main__": 

589 with open(__file__, "r") as f: 

590 local_content = f.read() 

591 # main(__file__, print_output = print) 

592 

593 def pprint(*la, **kw): 

594 print(*la, **kw) 

595 

596 process_string(local_content, pprint, lambda a, b, c: a, "", 0)