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# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Defines a sphinx extension to give a title to a todo, 

5inspired from `todo.py <https://github.com/sphinx-doc/sphinx/blob/master/sphinx/ext/todo.py>`_. 

6""" 

7import os 

8from docutils import nodes 

9from docutils.parsers.rst import directives 

10from docutils.frontend import Values 

11 

12import sphinx 

13from sphinx.locale import _ as locale_ 

14from sphinx.errors import NoUri 

15from docutils.parsers.rst import Directive 

16from docutils.parsers.rst.directives.admonitions import BaseAdmonition 

17from sphinx.util.nodes import set_source_info, process_index_entry 

18from sphinx import addnodes 

19from ..texthelper.texts_language import TITLES 

20from .sphinxext_helper import try_add_config_value 

21 

22 

23class todoext_node(nodes.admonition): 

24 """ 

25 Defines ``todoext`` node. 

26 """ 

27 pass 

28 

29 

30class todoextlist(nodes.General, nodes.Element): 

31 """ 

32 Defines ``todoextlist`` node. 

33 """ 

34 pass 

35 

36 

37class TodoExt(BaseAdmonition): 

38 """ 

39 A ``todoext`` entry, displayed in the form of an admonition. 

40 It takes the following options: 

41 

42 * *title:* a title for the todo (mandatory) 

43 * *tag:* a tag to have several categories of todo (mandatory) 

44 * *issue:* the issue requires `extlinks <https://www.sphinx-doc.org/en/master/ext/extlinks.html#confval-extlinks>`_ 

45 to be defined and must contain key ``'issue'`` (optional) 

46 * *cost:* a cost if the todo were to be fixed (optional) 

47 * *priority:* to prioritize items (optional) 

48 * *hidden:* if true, the todo does not appear where it is inserted but it 

49 will with a todolist (optional) 

50 * *date:* date (optional) 

51 * *release:* release number (optional) 

52 

53 Example:: 

54 

55 .. todoext:: 

56 :title: title for the todo 

57 :tag: issue 

58 :issue: issue number 

59 

60 Description of the todo 

61 

62 .. todoext:: 

63 :title: add option hidden to hide the item 

64 :tag: done 

65 :date: 2016-06-23 

66 :hidden: 

67 :issue: 17 

68 :release: 1.4 

69 :cost: 0.2 

70 

71 Once an item is done, it can be hidden from the documentation 

72 and show up in a another page. 

73 

74 If the option ``issue`` is filled, the configuration must contain a key in ``extlinks``: 

75 

76 extlinks=dict(issue=('https://link/%s', 

77 'issue {0} on somewhere'))) 

78 """ 

79 

80 node_class = todoext_node 

81 has_content = True 

82 required_arguments = 0 

83 optional_arguments = 0 

84 final_argument_whitespace = False 

85 option_spec = { 

86 'class': directives.class_option, 

87 'title': directives.unchanged, 

88 'tag': directives.unchanged, 

89 'issue': directives.unchanged, 

90 'cost': directives.unchanged, 

91 'priority': directives.unchanged, 

92 'hidden': directives.unchanged, 

93 'date': directives.unchanged, 

94 'release': directives.unchanged, 

95 'index': directives.unchanged, 

96 } 

97 

98 def run(self): 

99 """ 

100 builds the todo text 

101 """ 

102 sett = self.state.document.settings 

103 language_code = sett.language_code 

104 lineno = self.lineno 

105 

106 env = self.state.document.settings.env if hasattr( 

107 self.state.document.settings, "env") else None 

108 docname = None if env is None else env.docname 

109 if docname is not None: 

110 docname = docname.replace("\\", "/").split("/")[-1] 

111 legend = "{0}:{1}".format(docname, lineno) 

112 else: 

113 legend = '' 

114 

115 if not self.options.get('class'): 

116 self.options['class'] = ['admonition-todoext'] 

117 

118 # link to issue 

119 issue = self.options.get('issue', "").strip() 

120 if issue is not None and len(issue) > 0: 

121 if hasattr(sett, "extlinks"): 

122 extlinks = sett.extlinks 

123 elif env is not None and hasattr(env.config, "extlinks"): 

124 extlinks = env.config.extlinks 

125 else: # pragma: no cover 

126 available = "\n".join(sorted(sett.__dict__.keys())) 

127 available2 = "\n".join( 

128 sorted(env.config.__dict__.keys())) if env is not None else "-" 

129 mes = ("extlinks (wih a key 'issue') is not defined in the " 

130 "documentation settings, available in sett\n{0}\nCONFIG\n{1}") 

131 raise ValueError( # pragma: no cover 

132 mes.format(available, available2)) 

133 

134 if "issue" not in extlinks: 

135 raise KeyError( # pragma: no cover 

136 "key 'issue' is not present in extlinks") 

137 url, label = extlinks["issue"] 

138 url = url % str(issue) 

139 lab = label.format(issue) 

140 linkin = nodes.reference(lab, locale_(lab), refuri=url) 

141 link = nodes.paragraph() 

142 link += linkin 

143 else: 

144 link = None 

145 

146 # cost 

147 cost = self.options.get('cost', "").strip() 

148 if cost: # pragma: no cover 

149 try: 

150 fcost = float(cost) 

151 except ValueError: 

152 raise ValueError( 

153 "unable to convert cost '{0}' into float".format(cost)) 

154 else: 

155 fcost = 0.0 

156 

157 # priority 

158 prio = self.options.get('priority', "").strip() 

159 

160 # hidden 

161 hidden = self.options.get('hidden', "false").strip().lower() in { 

162 'true', '1', ''} 

163 

164 # body 

165 (todoext,) = super(TodoExt, self).run() 

166 if isinstance(todoext, nodes.system_message): 

167 return [todoext] 

168 

169 # link 

170 if link: 

171 todoext += link 

172 

173 # title 

174 title = self.options.get('title', "").strip() 

175 todotag = self.options.get('tag', '').strip() 

176 if len(title) > 0: 

177 title = ": " + title 

178 

179 # prefix 

180 prefix = TITLES[language_code]["todo"] 

181 tododate = self.options.get('date', "").strip() 

182 todorelease = self.options.get('release', "").strip() 

183 infos = [] 

184 if len(todotag) > 0: 

185 infos.append(todotag) 

186 if len(prio) > 0: 

187 infos.append('P=%s' % prio) 

188 if fcost > 0: 

189 if int(fcost) == fcost: 

190 infos.append('C=%d' % int(fcost)) 

191 else: 

192 infos.append('C=%1.1f' % fcost) 

193 if todorelease: 

194 infos.append('v{0}'.format(todorelease)) 

195 if tododate: 

196 infos.append(tododate) 

197 if infos: 

198 prefix += "({0})".format(" - ".join(infos)) 

199 

200 # main node 

201 title = nodes.title(text=locale_(prefix + title)) 

202 todoext.insert(0, title) 

203 todoext['todotag'] = todotag 

204 todoext['todocost'] = fcost 

205 todoext['todoprio'] = prio 

206 todoext['todohidden'] = hidden 

207 todoext['tododate'] = tododate 

208 todoext['todorelease'] = todorelease 

209 todoext['todotitle'] = self.options.get('title', "").strip() 

210 set_source_info(self, todoext) 

211 

212 if hidden: 

213 todoext['todoext_copy'] = todoext.deepcopy() 

214 todoext.clear() 

215 

216 if env is not None: 

217 targetid = 'indextodoe-%s' % env.new_serialno('indextodoe') 

218 targetnode = nodes.target(legend, '', ids=[targetid]) 

219 set_source_info(self, targetnode) 

220 self.state.add_target(targetid, '', targetnode, lineno) 

221 

222 # index node 

223 index = self.options.get('index', None) 

224 if index is not None: 

225 indexnode = addnodes.index() 

226 indexnode['entries'] = ne = [] 

227 indexnode['inline'] = False 

228 set_source_info(self, indexnode) 

229 for entry in index.split(","): 

230 ne.extend(process_index_entry(entry, targetid)) 

231 else: 

232 indexnode = None 

233 else: 

234 targetnode = None 

235 indexnode = None 

236 

237 return [a for a in [indexnode, targetnode, todoext] if a is not None] 

238 

239 

240def process_todoexts(app, doctree): 

241 """ 

242 collect all todoexts in the environment 

243 this is not done in the directive itself because it some transformations 

244 must have already been run, e.g. substitutions 

245 """ 

246 env = app.builder.env 

247 if not hasattr(env, 'todoext_all_todosext'): 

248 env.todoext_all_todosext = [] 

249 for node in doctree.traverse(todoext_node): 

250 try: 

251 targetnode = node.parent[node.parent.index(node) - 1] 

252 if not isinstance(targetnode, nodes.target): 

253 raise IndexError 

254 except IndexError: # pragma: no cover 

255 targetnode = None 

256 newnode = node.deepcopy() 

257 todotag = newnode['todotag'] 

258 todotitle = newnode['todotitle'] 

259 todoext_copy = node.get('todoext_copy', None) 

260 del newnode['ids'] 

261 del newnode['todotag'] 

262 if todoext_copy is not None: 

263 del newnode['todoext_copy'] 

264 env.todoext_all_todosext.append({ 

265 'docname': env.docname, 

266 'source': node.source or env.doc2path(env.docname), 

267 'todosource': node.source or env.doc2path(env.docname), 

268 'lineno': node.line, 

269 'todoext': newnode, 

270 'target': targetnode, 

271 'todotag': todotag, 

272 'todocost': newnode['todocost'], 

273 'todoprio': newnode['todoprio'], 

274 'todotitle': todotitle, 

275 'tododate': newnode['tododate'], 

276 'todorelease': newnode['todorelease'], 

277 'todohidden': newnode['todohidden'], 

278 'todoext_copy': todoext_copy}) 

279 

280 

281class TodoExtList(Directive): 

282 """ 

283 A list of all todoext entries, for a specific tag. 

284 

285 * tag: a tag to have several categories of todoext 

286 

287 Example:: 

288 

289 .. todoextlist:: 

290 :tag: issue 

291 """ 

292 

293 has_content = False 

294 required_arguments = 0 

295 optional_arguments = 0 

296 final_argument_whitespace = False 

297 option_spec = { 

298 'tag': directives.unchanged, 

299 'sort': directives.unchanged} 

300 

301 def run(self): 

302 """ 

303 Simply insert an empty todoextlist node which will be replaced later 

304 when process_todoext_nodes is called 

305 """ 

306 env = self.state.document.settings.env if hasattr( 

307 self.state.document.settings, "env") else None 

308 tag = self.options.get('tag', '').strip() 

309 tsort = self.options.get('sort', '').strip() 

310 if env is not None: 

311 targetid = 'indextodoelist-%s' % env.new_serialno('indextodoelist') 

312 targetnode = nodes.target('', '', ids=[targetid]) 

313 n = todoextlist('') 

314 n["todotag"] = tag 

315 n["todosort"] = tsort 

316 return [targetnode, n] 

317 else: # pragma: no cover 

318 n = todoextlist('') 

319 n["todotag"] = tag 

320 n["todosort"] = tsort 

321 return [n] 

322 

323 

324def process_todoext_nodes(app, doctree, fromdocname): 

325 """ 

326 process_todoext_nodes 

327 """ 

328 if not app.config['todoext_include_todosext']: 

329 for node in doctree.traverse(todoext_node): 

330 node.parent.remove(node) 

331 

332 # Replace all todoextlist nodes with a list of the collected todosext. 

333 # Augment each todoext with a backlink to the original location. 

334 env = app.builder.env 

335 if hasattr(env, "settings") and hasattr(env.settings, "language_code"): 

336 lang = env.settings.language_code 

337 else: 

338 lang = "en" 

339 

340 orig_entry = TITLES[lang]["original entry"] 

341 todomes = TITLES[lang]["todomes"] 

342 allowed_tsort = {'date', 'prio', 'title', 'release', 'source'} 

343 

344 if not hasattr(env, 'todoext_all_todosext'): 

345 env.todoext_all_todosext = [] 

346 

347 for ilist, node in enumerate(doctree.traverse(todoextlist)): 

348 if 'ids' in node: 

349 node['ids'] = [] 

350 if not app.config['todoext_include_todosext']: 

351 node.replace_self([]) 

352 continue 

353 

354 nbtodo = 0 

355 fcost = 0 

356 content = [] 

357 todotag = node["todotag"] 

358 tsort = node["todosort"] 

359 if tsort == '': 

360 tsort = 'source' 

361 if tsort not in allowed_tsort: 

362 raise ValueError( 

363 "option sort must in {0}, '{1}' is not".format(allowed_tsort, tsort)) 

364 

365 double_list = [(info.get('todo%s' % tsort, ''), 

366 info.get('todotitle', ''), info) 

367 for info in env.todoext_all_todosext] 

368 double_list.sort(key=lambda x: x[:2]) 

369 for n, todoext_info_ in enumerate(double_list): 

370 todoext_info = todoext_info_[2] 

371 if todoext_info["todotag"] != todotag: 

372 continue 

373 

374 nbtodo += 1 

375 fcost += todoext_info.get("todocost", 0.0) 

376 

377 para = nodes.paragraph(classes=['todoext-source']) 

378 if app.config['todoext_link_only']: 

379 description = locale_('<<%s>>' % orig_entry) 

380 else: 

381 description = ( 

382 locale_(todomes) % 

383 (orig_entry, os.path.split(todoext_info['source'])[-1], 

384 todoext_info['lineno']) 

385 ) 

386 desc1 = description[:description.find('<<')] 

387 desc2 = description[description.find('>>') + 2:] 

388 para += nodes.Text(desc1, desc1) 

389 

390 # Create a reference 

391 newnode = nodes.reference('', '', internal=True) 

392 innernode = nodes.emphasis('', locale_(orig_entry)) 

393 try: 

394 newnode['refuri'] = app.builder.get_relative_uri( 

395 fromdocname, todoext_info['docname']) 

396 try: 

397 newnode['refuri'] += '#' + todoext_info['target']['refid'] 

398 except Exception as e: # pragma: no cover 

399 raise KeyError("refid in not present in '{0}'".format( 

400 todoext_info['target'])) from e 

401 except NoUri: # pragma: no cover 

402 # ignore if no URI can be determined, e.g. for LaTeX output 

403 pass 

404 newnode.append(innernode) 

405 para += newnode 

406 para += nodes.Text(desc2, desc2) 

407 

408 # (Recursively) resolve references in the todoext content 

409 todoext_entry = todoext_info.get('todoext_copy', None) 

410 if todoext_entry is None: 

411 todoext_entry = todoext_info['todoext'] 

412 todoext_entry["ids"] = ["index-todoext-%d-%d" % (ilist, n)] 

413 # it apparently requires an attributes ids 

414 

415 if not hasattr(todoext_entry, "settings"): 

416 todoext_entry.settings = Values() 

417 todoext_entry.settings.env = env 

418 # If an exception happens here, see blog 2017-05-21 from the 

419 # documentation. 

420 env.resolve_references(todoext_entry, todoext_info['docname'], 

421 app.builder) 

422 

423 # Insert into the todoextlist 

424 content.append(todoext_entry) 

425 content.append(para) 

426 

427 if fcost > 0: # pragma: no cover 

428 cost = nodes.paragraph() 

429 lab = "{0} items, cost: {1}".format(nbtodo, fcost) 

430 cost += nodes.Text(lab) 

431 content.append(cost) 

432 else: 

433 cost = nodes.paragraph() 

434 lab = "{0} items".format(nbtodo) 

435 cost += nodes.Text(lab) 

436 content.append(cost) 

437 

438 node.replace_self(content) 

439 

440 

441def purge_todosext(app, env, docname): 

442 """ 

443 purge_todosext 

444 """ 

445 if not hasattr(env, 'todoext_all_todosext'): 

446 return 

447 env.todoext_all_todosext = [todoext for todoext in env.todoext_all_todosext 

448 if todoext['docname'] != docname] 

449 

450 

451def merge_todoext(app, env, docnames, other): 

452 """ 

453 merge_todoext 

454 """ 

455 if not hasattr(other, 'todoext_all_todosext'): 

456 return 

457 if not hasattr(env, 'todoext_all_todosext'): 

458 env.todoext_all_todosext = [] 

459 env.todoext_all_todosext.extend(other.todoext_all_todosext) 

460 

461 

462def visit_todoext_node(self, node): 

463 """ 

464 visit_todoext_node 

465 """ 

466 self.visit_admonition(node) 

467 

468 

469def depart_todoext_node(self, node): 

470 """ 

471 depart_todoext_node, 

472 see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py 

473 """ 

474 self.depart_admonition(node) 

475 

476 

477def visit_todoextlist_node(self, node): 

478 """ 

479 visit_todoextlist_node 

480 see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py 

481 """ 

482 self.visit_admonition(node) 

483 

484 

485def depart_todoextlist_node(self, node): 

486 """ 

487 depart_todoext_node 

488 """ 

489 self.depart_admonition(node) 

490 

491 

492def setup(app): 

493 """ 

494 Setup for ``todoext`` (sphinx). 

495 """ 

496 if hasattr(app, "add_mapping"): 

497 app.add_mapping('todoext', todoext_node) 

498 app.add_mapping('todoextlist', todoextlist) 

499 

500 app.add_config_value('todoext_include_todosext', False, 'html') 

501 app.add_config_value('todoext_link_only', False, 'html') 

502 

503 # The following variable is shared with extension 

504 # `todo <https://www.sphinx-doc.org/en/master/usage/extensions/todo.html>`_. 

505 try_add_config_value(app, 'extlinks', {}, 'env') 

506 

507 app.add_node(todoextlist, 

508 html=(visit_todoextlist_node, depart_todoextlist_node), 

509 epub=(visit_todoextlist_node, depart_todoextlist_node), 

510 elatex=(visit_todoextlist_node, depart_todoextlist_node), 

511 latex=(visit_todoextlist_node, depart_todoextlist_node), 

512 text=(visit_todoextlist_node, depart_todoextlist_node), 

513 md=(visit_todoextlist_node, depart_todoextlist_node), 

514 rst=(visit_todoextlist_node, depart_todoextlist_node)) 

515 app.add_node(todoext_node, 

516 html=(visit_todoext_node, depart_todoext_node), 

517 epub=(visit_todoext_node, depart_todoext_node), 

518 elatex=(visit_todoext_node, depart_todoext_node), 

519 latex=(visit_todoext_node, depart_todoext_node), 

520 text=(visit_todoext_node, depart_todoext_node), 

521 md=(visit_todoext_node, depart_todoext_node), 

522 rst=(visit_todoext_node, depart_todoext_node)) 

523 

524 app.add_directive('todoext', TodoExt) 

525 app.add_directive('todoextlist', TodoExtList) 

526 app.connect('doctree-read', process_todoexts) 

527 app.connect('doctree-resolved', process_todoext_nodes) 

528 app.connect('env-purge-doc', purge_todosext) 

529 app.connect('env-merge-info', merge_todoext) 

530 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}