# -*- coding: utf-8 -*-
"""
Defines a sphinx extension to give a title to a todo,
inspired from `todo.py <https://github.com/sphinx-doc/sphinx/blob/master/sphinx/ext/todo.py>`_.
:githublink:`%|py|7`
"""
import os
from docutils import nodes
from docutils.parsers.rst import directives
from docutils.frontend import Values
import sphinx
from sphinx.locale import _ as locale_
from sphinx.errors import NoUri
from docutils.parsers.rst import Directive
from docutils.parsers.rst.directives.admonitions import BaseAdmonition
from sphinx.util.nodes import set_source_info, process_index_entry
from sphinx import addnodes
from ..texthelper.texts_language import TITLES
from .sphinxext_helper import try_add_config_value
[docs]class todoext_node(nodes.admonition):
"""
Defines ``todoext`` node.
:githublink:`%|py|26`
"""
pass
[docs]class todoextlist(nodes.General, nodes.Element):
"""
Defines ``todoextlist`` node.
:githublink:`%|py|33`
"""
pass
[docs]class TodoExt(BaseAdmonition):
"""
A ``todoext`` entry, displayed in the form of an admonition.
It takes the following options:
* *title:* a title for the todo (mandatory)
* *tag:* a tag to have several categories of todo (mandatory)
* *issue:* the issue requires `extlinks <http://www.sphinx-doc.org/en/stable/ext/extlinks.html#confval-extlinks>`_
to be defined and must contain key ``'issue'`` (optional)
* *cost:* a cost if the todo were to be fixed (optional)
* *priority:* to prioritize items (optional)
* *hidden:* if true, the todo does not appear where it is inserted but it
will with a todolist (optional)
* *date:* date (optional)
* *release:* release number (optional)
Example::
.. todoext::
:title: title for the todo
:tag: issue
:issue: issue number
Description of the todo
.. todoext::
:title: add option hidden to hide the item
:tag: done
:date: 2016-06-23
:hidden:
:issue: 17
:release: 1.4
:cost: 0.2
Once an item is done, it can be hidden from the documentation
and show up in a another page.
If the option ``issue`` is filled, the configuration must contain a key in ``extlinks``:
extlinks=dict(issue=('https://link/%s',
'issue {0} on somewhere')))
:githublink:`%|py|78`
"""
node_class = todoext_node
has_content = True
required_arguments = 0
optional_arguments = 0
final_argument_whitespace = False
option_spec = {
'class': directives.class_option,
'title': directives.unchanged,
'tag': directives.unchanged,
'issue': directives.unchanged,
'cost': directives.unchanged,
'priority': directives.unchanged,
'hidden': directives.unchanged,
'date': directives.unchanged,
'release': directives.unchanged,
'index': directives.unchanged,
}
[docs] def run(self):
"""
builds the todo text
:githublink:`%|py|101`
"""
sett = self.state.document.settings
language_code = sett.language_code
lineno = self.lineno
env = self.state.document.settings.env if hasattr(
self.state.document.settings, "env") else None
docname = None if env is None else env.docname
if docname is not None:
docname = docname.replace("\\", "/").split("/")[-1]
legend = "{0}:{1}".format(docname, lineno)
else:
legend = ''
if not self.options.get('class'):
self.options['class'] = ['admonition-todoext']
# link to issue
issue = self.options.get('issue', "").strip()
if issue is not None and len(issue) > 0:
if hasattr(sett, "extlinks"):
extlinks = sett.extlinks
elif env is not None and hasattr(env.config, "extlinks"):
extlinks = env.config.extlinks
else:
available = "\n".join(sorted(sett.__dict__.keys()))
available2 = "\n".join(
sorted(env.config.__dict__.keys())) if env is not None else "-"
mes = "extlinks (wih a key 'issue') is not defined in the documentation settings, available in sett\n{0}\nCONFIG\n{1}"
raise ValueError(mes.format(available, available2))
if "issue" not in extlinks:
raise KeyError("key 'issue' is not present in extlinks")
url, label = extlinks["issue"]
url = url % str(issue)
lab = label.format(issue)
linkin = nodes.reference(lab, locale_(lab), refuri=url)
link = nodes.paragraph()
link += linkin
else:
link = None
# cost
cost = self.options.get('cost', "").strip()
if cost:
try:
fcost = float(cost)
except ValueError:
raise ValueError(
"unable to convert cost '{0}' into float".format(cost))
else:
fcost = 0.0
# priority
prio = self.options.get('priority', "").strip()
# hidden
hidden = self.options.get('hidden', "false").strip().lower() in {
'true', '1', ''}
# body
(todoext,) = super(TodoExt, self).run()
if isinstance(todoext, nodes.system_message):
return [todoext]
# link
if link:
todoext += link
# title
title = self.options.get('title', "").strip()
todotag = self.options.get('tag', '').strip()
if len(title) > 0:
title = ": " + title
# prefix
prefix = TITLES[language_code]["todo"]
tododate = self.options.get('date', "").strip()
todorelease = self.options.get('release', "").strip()
infos = []
if len(todotag) > 0:
infos.append(todotag)
if len(prio) > 0:
infos.append('P=%s' % prio)
if fcost > 0:
if int(fcost) == fcost:
infos.append('C=%d' % int(fcost))
else:
infos.append('C=%1.1f' % fcost)
if todorelease:
infos.append('v{0}'.format(todorelease))
if tododate:
infos.append(tododate)
if infos:
prefix += "({0})".format(" - ".join(infos))
# main node
title = nodes.title(text=locale_(prefix + title))
todoext.insert(0, title)
todoext['todotag'] = todotag
todoext['todocost'] = fcost
todoext['todoprio'] = prio
todoext['todohidden'] = hidden
todoext['tododate'] = tododate
todoext['todorelease'] = todorelease
todoext['todotitle'] = self.options.get('title', "").strip()
set_source_info(self, todoext)
if hidden:
todoext['todoext_copy'] = todoext.deepcopy()
todoext.clear()
if env is not None:
targetid = 'indextodoe-%s' % env.new_serialno('indextodoe')
targetnode = nodes.target(legend, '', ids=[targetid])
set_source_info(self, targetnode)
self.state.add_target(targetid, '', targetnode, lineno)
# index node
index = self.options.get('index', None)
if index is not None:
indexnode = addnodes.index()
indexnode['entries'] = ne = []
indexnode['inline'] = False
set_source_info(self, indexnode)
for entry in index.split(","):
ne.extend(process_index_entry(entry, targetid))
else:
indexnode = None
else:
targetnode = None
indexnode = None
return [a for a in [indexnode, targetnode, todoext] if a is not None]
[docs]def process_todoexts(app, doctree):
"""
collect all todoexts in the environment
this is not done in the directive itself because it some transformations
must have already been run, e.g. substitutions
:githublink:`%|py|242`
"""
env = app.builder.env
if not hasattr(env, 'todoext_all_todosext'):
env.todoext_all_todosext = []
for node in doctree.traverse(todoext_node):
try:
targetnode = node.parent[node.parent.index(node) - 1]
if not isinstance(targetnode, nodes.target):
raise IndexError
except IndexError:
targetnode = None
newnode = node.deepcopy()
todotag = newnode['todotag']
todotitle = newnode['todotitle']
todoext_copy = node.get('todoext_copy', None)
del newnode['ids']
del newnode['todotag']
if todoext_copy is not None:
del newnode['todoext_copy']
env.todoext_all_todosext.append({
'docname': env.docname,
'source': node.source or env.doc2path(env.docname),
'todosource': node.source or env.doc2path(env.docname),
'lineno': node.line,
'todoext': newnode,
'target': targetnode,
'todotag': todotag,
'todocost': newnode['todocost'],
'todoprio': newnode['todoprio'],
'todotitle': todotitle,
'tododate': newnode['tododate'],
'todorelease': newnode['todorelease'],
'todohidden': newnode['todohidden'],
'todoext_copy': todoext_copy,
})
[docs]class TodoExtList(Directive):
"""
A list of all todoext entries, for a specific tag.
* tag: a tag to have several categories of todoext
Example::
.. todoextlist::
:tag: issue
:githublink:`%|py|289`
"""
has_content = False
required_arguments = 0
optional_arguments = 0
final_argument_whitespace = False
option_spec = {
'tag': directives.unchanged,
'sort': directives.unchanged,
}
[docs] def run(self):
"""
Simply insert an empty todoextlist node which will be replaced later
when process_todoext_nodes is called
:githublink:`%|py|304`
"""
env = self.state.document.settings.env if hasattr(
self.state.document.settings, "env") else None
tag = self.options.get('tag', '').strip()
tsort = self.options.get('sort', '').strip()
if env is not None:
targetid = 'indextodoelist-%s' % env.new_serialno('indextodoelist')
targetnode = nodes.target('', '', ids=[targetid])
n = todoextlist('')
n["todotag"] = tag
n["todosort"] = tsort
return [targetnode, n]
else:
n = todoextlist('')
n["todotag"] = tag
n["todosort"] = tsort
return [n]
[docs]def process_todoext_nodes(app, doctree, fromdocname):
"""
process_todoext_nodes
:githublink:`%|py|326`
"""
if not app.config['todoext_include_todosext']:
for node in doctree.traverse(todoext_node):
node.parent.remove(node)
# Replace all todoextlist nodes with a list of the collected todosext.
# Augment each todoext with a backlink to the original location.
env = app.builder.env
if hasattr(env, "settings") and hasattr(env.settings, "language_code"):
lang = env.settings.language_code
else:
lang = "en"
orig_entry = TITLES[lang]["original entry"]
todomes = TITLES[lang]["todomes"]
allowed_tsort = {'date', 'prio', 'title', 'release', 'source'}
if not hasattr(env, 'todoext_all_todosext'):
env.todoext_all_todosext = []
for ilist, node in enumerate(doctree.traverse(todoextlist)):
if 'ids' in node:
node['ids'] = []
if not app.config['todoext_include_todosext']:
node.replace_self([])
continue
nbtodo = 0
fcost = 0
content = []
todotag = node["todotag"]
tsort = node["todosort"]
if tsort == '':
tsort = 'source'
if tsort not in allowed_tsort:
raise ValueError(
"option sort must in {0}, '{1}' is not".format(allowed_tsort, tsort))
double_list = [(info.get('todo%s' % tsort, ''),
info.get('todotitle', ''), info)
for info in env.todoext_all_todosext]
double_list.sort(key=lambda x: x[:2])
for n, todoext_info_ in enumerate(double_list):
todoext_info = todoext_info_[2]
if todoext_info["todotag"] != todotag:
continue
nbtodo += 1
fcost += todoext_info.get("todocost", 0.0)
para = nodes.paragraph(classes=['todoext-source'])
if app.config['todoext_link_only']:
description = locale_('<<%s>>' % orig_entry)
else:
description = (
locale_(todomes) %
(orig_entry, os.path.split(todoext_info['source'])[-1],
todoext_info['lineno'])
)
desc1 = description[:description.find('<<')]
desc2 = description[description.find('>>') + 2:]
para += nodes.Text(desc1, desc1)
# Create a reference
newnode = nodes.reference('', '', internal=True)
innernode = nodes.emphasis('', locale_(orig_entry))
try:
newnode['refuri'] = app.builder.get_relative_uri(
fromdocname, todoext_info['docname'])
try:
newnode['refuri'] += '#' + todoext_info['target']['refid']
except Exception as e:
raise KeyError("refid in not present in '{0}'".format(
todoext_info['target'])) from e
except NoUri:
# ignore if no URI can be determined, e.g. for LaTeX output
pass
newnode.append(innernode)
para += newnode
para += nodes.Text(desc2, desc2)
# (Recursively) resolve references in the todoext content
todoext_entry = todoext_info.get('todoext_copy', None)
if todoext_entry is None:
todoext_entry = todoext_info['todoext']
todoext_entry["ids"] = ["index-todoext-%d-%d" % (ilist, n)]
# it apparently requires an attributes ids
if not hasattr(todoext_entry, "settings"):
todoext_entry.settings = Values()
todoext_entry.settings.env = env
# If an exception happens here, see blog 2017-05-21 from the
# documentation.
env.resolve_references(todoext_entry, todoext_info['docname'],
app.builder)
# Insert into the todoextlist
content.append(todoext_entry)
content.append(para)
if fcost > 0:
cost = nodes.paragraph()
lab = "{0} items, cost: {1}".format(nbtodo, fcost)
cost += nodes.Text(lab)
content.append(cost)
else:
cost = nodes.paragraph()
lab = "{0} items".format(nbtodo)
cost += nodes.Text(lab)
content.append(cost)
node.replace_self(content)
[docs]def purge_todosext(app, env, docname):
"""
purge_todosext
:githublink:`%|py|443`
"""
if not hasattr(env, 'todoext_all_todosext'):
return
env.todoext_all_todosext = [todoext for todoext in env.todoext_all_todosext
if todoext['docname'] != docname]
[docs]def merge_todoext(app, env, docnames, other):
"""
merge_todoext
:githublink:`%|py|453`
"""
if not hasattr(other, 'todoext_all_todosext'):
return
if not hasattr(env, 'todoext_all_todosext'):
env.todoext_all_todosext = []
env.todoext_all_todosext.extend(other.todoext_all_todosext)
[docs]def visit_todoext_node(self, node):
"""
visit_todoext_node
:githublink:`%|py|464`
"""
self.visit_admonition(node)
[docs]def depart_todoext_node(self, node):
"""
depart_todoext_node,
see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
:githublink:`%|py|472`
"""
self.depart_admonition(node)
[docs]def visit_todoextlist_node(self, node):
"""
visit_todoextlist_node
see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
:githublink:`%|py|480`
"""
self.visit_admonition(node)
[docs]def depart_todoextlist_node(self, node):
"""
depart_todoext_node
:githublink:`%|py|487`
"""
self.depart_admonition(node)
[docs]def setup(app):
"""
Setup for ``todoext`` (sphinx).
:githublink:`%|py|494`
"""
if hasattr(app, "add_mapping"):
app.add_mapping('todoext', todoext_node)
app.add_mapping('todoextlist', todoextlist)
app.add_config_value('todoext_include_todosext', False, 'html')
app.add_config_value('todoext_link_only', False, 'html')
# The following variable is shared with extension
# `todo <http://www.sphinx-doc.org/en/stable/ext/todo.html>`_.
try_add_config_value(app, 'extlinks', {}, 'env')
app.add_node(todoextlist,
html=(visit_todoextlist_node, depart_todoextlist_node),
epub=(visit_todoextlist_node, depart_todoextlist_node),
elatex=(visit_todoextlist_node, depart_todoextlist_node),
latex=(visit_todoextlist_node, depart_todoextlist_node),
text=(visit_todoextlist_node, depart_todoextlist_node),
md=(visit_todoextlist_node, depart_todoextlist_node),
rst=(visit_todoextlist_node, depart_todoextlist_node))
app.add_node(todoext_node,
html=(visit_todoext_node, depart_todoext_node),
epub=(visit_todoext_node, depart_todoext_node),
elatex=(visit_todoext_node, depart_todoext_node),
latex=(visit_todoext_node, depart_todoext_node),
text=(visit_todoext_node, depart_todoext_node),
md=(visit_todoext_node, depart_todoext_node),
rst=(visit_todoext_node, depart_todoext_node))
app.add_directive('todoext', TodoExt)
app.add_directive('todoextlist', TodoExtList)
app.connect('doctree-read', process_todoexts)
app.connect('doctree-resolved', process_todoext_nodes)
app.connect('env-purge-doc', purge_todosext)
app.connect('env-merge-info', merge_todoext)
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}