Adding an HTML class to a Sphinx directive with an extension - python

I'm trying to write a basic extension to create a new directive (output) based on the only directive provided by Sphinx. This new directive simply needs to add an HTML class to the standard result of the Only directive.
So for this I have tried the following code based on this collapse-code-block extension.
Since docutils has virtually no documentation and that I'm not a very experienced python developer, I'm struggling to make this work.
Here is what I have tried, amongst other variations that all led to no real indication on the issue:
from docutils import nodes
from sphinx.directives.other import Only
class output_node(nodes.General, nodes.Element):
pass
class output_directive(Only):
option_spec = Only.option_spec
def run(self):
env = self.state.document.settings.env
node = output_node()
output = Only.run(self)
node.setup_child(output)
node.append(output)
return [node]
def html_visit_output_node(self, node):
self.body.append(self.starttag(node, 'div', '', CLASS='internalonly'))
def html_depart_output_node(self, node):
self.body.append('</div>')
def setup(app):
app.add_node(
output_node,
html=(
html_visit_output_node,
html_depart_output_node
)
)
app.add_directive('output', output_directive)
I don't think it should be more complicated than that but this just doesn't cut it.
Any idea?

Related

How to link typing-like nested classes and other urls with Sphinx and RST

Using intersphinx and autodoc, having:
:param stores: Array of objects
:type stores: list[dict[str,int]]
Would result in an entry like:
stores (list[dict[str,int]]) - Array of objects.
Is there a way to convert list[dict[str,int]] outside of the autodoc :param: derivative (or others like :rtype:) with raw RST (within the docstring) or programatically given a 'list[dict[str,int]]' string?
Additionally, is it possible to use external links within the aforementioned example?
Example
Consider a script.py file:
def some_func(arg1):
"""
This is a head description.
:param arg1: The type of this param is hyperlinked.
:type arg1: list[dict[str,int]]
Is it possible to hyperlink this, here: dict[str,list[int]]
Or even add custom references amongst the classes: dict[int,ref]
Where *ref* links to a foreign, external source.
"""
Now in the Sphinx conf.py file add:
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx'
]
intersphinx_mapping = {
'py': ('https://docs.python.org/3', None),
}
In your index.rst, add:
Title
=====
.. toctree::
:maxdepth: 2
.. autofunction:: script.some_func
And now simply make the html for the page.
The list[dict[str,int]] next to :type arg1: will be hyperlinked as shown at the beginning of this question, but dict[str,list[int]] obviously won't. Is there a way to make the latter behave like the former?
I reached a solution after digging around sphinx's code.
Injecting External References (:param:)
I created a custom extension that connects to the missing-reference event and attempts to resolve unknown references.
Code of reflinks.py:
import docutils.nodes as nodes
_cache = {}
def fill_cache(app):
_cache.update(app.config.reflinks)
def missing_reference(app, env, node, contnode):
target = node['reftarget']
try:
uri = _cache[target]
except KeyError:
return
newnode = nodes.reference('', '', internal = False, refuri = uri)
if not node.get('refexplicit'):
name = target.replace('_', ' ') # style
contnode = contnode.__class__(name, name)
newnode.append(contnode)
return newnode
def setup(app):
app.add_config_value('reflinks', None, False)
app.connect('builder-inited', fill_cache)
app.connect('missing-reference', missing_reference, priority = 1000)
Explanation
I consulted intersphinx's methodology for resolving unknown references and connected the function with high priority so it's hopefully only consulted as a last result.
Followup
Include the extenion.
Adding to conf.py:
reflinks = {'google': 'https://google.com'}
Allowed for script.py:
def some_func(arg1):
"""
:param arg1: Google homepages.
:type arg1: dict[str, google]
"""
Where dict[str, google] are now all hyperlinks.
Formatting Nested Types
There were instances where I wanted to use type structures like list[dict[str,myref]] outside of fields like :param:, :rtype:, etc. Another short extension did the trick.
Code of nestlinks.py:
import sphinx.domains.python as domain
import docutils.parsers.rst.roles as roles
_field = domain.PyTypedField('class')
def handle(name, rawtext, text, lineno, inliner, options = {}, content = []):
refs = _field.make_xrefs('class', 'py', text)
return (refs, [])
def setup(app):
roles.register_local_role('nref', handle)
Explanation
After reading this guide on roles, and digging here and here I realised that all I needed was a dummy field to handle the whole reference-making work and pretend like it's trying to reference classes.
Followup
Include the extension.
Now script.py:
def some_func(arg1):
"""
:param arg1: Google homepages.
:type arg1: dict[str, google]
Now this :nref:`list[dict[str,google]]` is hyperlinked!
"""
Notes
I am using intersphinx and autodoc to link to python's types and document my function's docstrings.
I am not well-versed in Sphinx's underlying mechanisms so take my methodology with a grain of salt.
The examples are provided are adjusted for the sake of being re-usable and generic and have not been tested.
The usability of such features is obviously questionable and only necessary when libraries like extlinks don't cover your needs.

Can I call sphinx.parsers.Parser() directly and parse a fragment of reST?

I need to parse a stand-alone fragment of reST content to a doctree (for later processing). I can do it via docutils easily enough, e.g.:
# ref: http://stackoverflow.com/questions/12883428/
import docutils.nodes
import docutils.parsers.rst
import docutils.utils
def parse_rst(text: str) -> docutils.nodes.document:
parser = docutils.parsers.rst.Parser()
components = (docutils.parsers.rst.Parser,)
settings = docutils.frontend.OptionParser(
components=components).get_default_values()
document = docutils.utils.new_document('<rst-doc>', settings=settings)
parser.parse(text, document)
return document
class MyVisitor(docutils.nodes.NodeVisitor):
def visit_reference(self, node: docutils.nodes.reference) -> None:
"""Called for "reference" nodes."""
print(node)
def unknown_visit(self, node: docutils.nodes.Node) -> None:
"""Called for all other node types."""
print(node)
if __name__ == '__main__':
doc = parse_rst('spam spam lovely spam')
visitor = MyVisitor(doc)
doc.walk(visitor)
That works unless the reST content includes Sphinx-specific directives/roles (e.g. .. glossary::, :term:, etc).
So I need the Sphinx parser rather than the docutils one.
I tried subclassing sphinx.parsers.Parser; using from sphinx.application import Sphinx then app.build() to fake a sphinx docs project.
But references and workarounds get complicated so I suspect I'm approaching it the wrong way. Can I use the sphinx parser outside of the full-blown sphinx-build workflow?

How do you change the style of pybtex references in sphinx

I'm trying to use the sphinxcontrib.bibtex to include citations/references in some documentation. It says that you can set the style with:
.. bibliography: refs.bib
:style: plain
And it comes with the following styles: alpha, plain which create reference labels that look like this:
alpha: [ZieglerBenderSchreiber+14] or [TMT14] or [PCY+16a]
plain: [1], [2], [3]
I find that alpha is inconsistent but I think that the just-numbers of plain lack context. To customize formatting it provides this example:
from pybtex.style.formatting.unsrt import Style as UnsrtStyle
from pybtex.style.template import toplevel # ... and anything else needed
from pybtex.plugin import register_plugin
class MyStyle(UnsrtStyle):
def format_XXX(self, e):
template = toplevel [
# etc.
]
return template.format_data(e)
register_plugin('pybtex.style.formatting', 'mystyle', MyStyle)
which sphinx fails out on with:
sphinx-build -M html . out
Running Sphinx v1.8.3
Configuration error:
There is a syntax error in your configuration file: bad input (conf.py, line 92)
Did you change the syntax from 2.x to 3.x?
It seems to use pybtex under the hood so following an example from that documentation:
from pybtex.style.formatting import BaseStyle
from pybtex.richtext import Text, Tag
class MyStyle(BaseStyle):
def format_article(self, entry):
return Text('Article ', Tag('em', entry.fields['title']))
from pybtex.plugin import register_plugin
register_plugin('pybtex.style.formatting', 'mystyle', MyStyle)
and sphinx fails out with:
Exception occurred:
File "/home/josh/codes/docs/conf.py", line 88, in format_article
return Text('Article ', Tag('em', entry.fields['title']))
AttributeError: 'dict' object has no attribute 'fields'
The full traceback has been saved in /tmp/sphinx-err-dftwlzvr.log, if you want to report the issue to the developers.
... and I think that just customizes the bibliography entry text and not the label text anyway. How do I customize the label text? I would prefer one of the following:
just bracket numerals ([1]) like plain, but with labels in superscript
something like [ref.12] similar to eq.12 or fig.12
something like alpha but with a consistent number of characters
I hate to answer my own question, but here's what I figured out. I added the following to my conf.py and it uses the bibtex key for the label text:
from pybtex.style.formatting.unsrt import Style as UnsrtStyle
from pybtex.style.labels.alpha import LabelStyle as AlphaLabelStyle
class KeyLabelStyle(AlphaLabelStyle):
def format_label(self, entry):
label = entry.key
return label
class CustomStyle(UnsrtStyle):
default_sorting_style = 'author_year_title'
def __init__(self, *args, **kwargs):
super(CustomStyle, self).__init__(*args, **kwargs)
self.label_style = KeyLabelStyle()
self.format_labels = self.label_style.format_labels
from pybtex.plugin import register_plugin
register_plugin('pybtex.style.formatting', 'custom', CustomStyle)
To activate in RST:
.. bibliography:: refs.bib
:style: custom
This isn't a complete answer however as it doesn't really generate custom labels... but overrides some internals of an existing style. So kind of a hacky almost-answer... I hope it helps someone else.

How to parse restructuredtext in python?

Is there any module that can parse restructuredtext into a tree model?
Can docutils or sphinx do this?
I'd like to extend upon the answer from Gareth Latty. "What you probably want is the parser at docutils.parsers.rst" is a good starting point of the answer, but what's next? Namely:
How to parse restructuredtext in python?
Below is the exact answer for Python 3.6 and docutils 0.14:
import docutils.nodes
import docutils.parsers.rst
import docutils.utils
import docutils.frontend
def parse_rst(text: str) -> docutils.nodes.document:
parser = docutils.parsers.rst.Parser()
components = (docutils.parsers.rst.Parser,)
settings = docutils.frontend.OptionParser(components=components).get_default_values()
document = docutils.utils.new_document('<rst-doc>', settings=settings)
parser.parse(text, document)
return document
And the resulting document can be processed using, for example, below, which will print all references in the document:
class MyVisitor(docutils.nodes.NodeVisitor):
def visit_reference(self, node: docutils.nodes.reference) -> None:
"""Called for "reference" nodes."""
print(node)
def unknown_visit(self, node: docutils.nodes.Node) -> None:
"""Called for all other node types."""
pass
Here's how to run it:
doc = parse_rst('spam spam lovely spam')
visitor = MyVisitor(doc)
doc.walk(visitor)
Docutils does indeed contain the tools to do this.
What you probably want is the parser at docutils.parsers.rst
See this page for details on what is involved. There are also some examples at docutils/examples.py - particularly check out the internals() function, which is probably of interest.

Substitutions inside links in reST / Sphinx

I am using Sphinx to document a webservice that will be deployed in different servers. The documentation is full of URL examples for the user to click and they should just work. My problem is that the host, port and deployment root will vary and the documentation will have to be re-generated for every deployment.
I tried defining substitutions like this:
|base_url|/path
.. |base_url| replace:: http://localhost:8080
But the generated HTML is not what I want (doesn't include "/path" in the generated link):
http://localhost:8080/path
Does anybody know how to work around this?
New in Sphinx v1.0:
sphinx.ext.extlinks – Markup to shorten external links
https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html
The extension adds one new config value:
extlinks
This config value must be a dictionary of external sites, mapping unique short alias names to a base URL and a prefix. For example, to create an alias for the above mentioned issues, you would add
extlinks = {'issue':
('http://bitbucket.org/birkenfeld/sphinx/issue/%s', 'issue ')}
Now, you can use the alias name as a new role, e.g. :issue:`123`. This then inserts a link to http://bitbucket.org/birkenfeld/sphinx/issue/123. As you can see, the target given in the role is substituted in the base URL in the place of %s.
The link caption depends on the second item in the tuple, the prefix:
If the prefix is None, the link caption is the full URL.
If the prefix is the empty string, the link caption is the partial URL given in the role content (123 in this case.)
If the prefix is a non-empty string, the link caption is the partial URL, prepended by the prefix – in the above example, the link caption would be issue 123.
You can also use the usual “explicit title” syntax supported by other roles that generate links, i.e. :issue:`this issue <123>`. In this case, the prefix is not relevant.
I had a similar problem where I needed to substitute also URLs in image targets.
The extlinks do not expand when used as a value of image :target: attribute.
Eventually I wrote a custom sphinx transformation that rewrites URLs that start with a given prefix, in my case, http://mybase/. Here is a relevant code for conf.py:
from sphinx.transforms import SphinxTransform
class ReplaceMyBase(SphinxTransform):
default_priority = 750
prefix = 'http://mybase/'
def apply(self):
from docutils.nodes import reference, Text
baseref = lambda o: (
isinstance(o, reference) and
o.get('refuri', '').startswith(self.prefix))
basetext = lambda o: (
isinstance(o, Text) and o.startswith(self.prefix))
base = self.config.mybase.rstrip('/') + '/'
for node in self.document.traverse(baseref):
target = node['refuri'].replace(self.prefix, base, 1)
node.replace_attr('refuri', target)
for t in node.traverse(basetext):
t1 = Text(t.replace(self.prefix, base, 1), t.rawsource)
t.parent.replace(t, t1)
return
# end of class
def setup(app):
app.add_config_value('mybase', 'https://en.wikipedia.org/wiki', 'env')
app.add_transform(ReplaceMyBase)
return
This expands the following rst source to point to English wikipedia.
When conf.py sets mybase="https://es.wikipedia.org/wiki" the links would point to the Spanish wiki.
* inline link http://mybase/Helianthus
* `link with text <http://mybase/Helianthus>`_
* `link with separate definition`_
* image link |flowerimage|
.. _link with separate definition: http://mybase/Helianthus
.. |flowerimage| image:: https://upload.wikimedia.org/wikipedia/commons/f/f1/Tournesol.png
:target: http://mybase/Helianthus
Ok, here's how I did it. First, apilinks.py (the Sphinx extension):
from docutils import nodes, utils
def setup(app):
def api_link_role(role, rawtext, text, lineno, inliner, options={},
content=[]):
ref = app.config.apilinks_base + text
node = nodes.reference(rawtext, utils.unescape(ref), refuri=ref,
**options)
return [node], []
app.add_config_value('apilinks_base', 'http://localhost/', False)
app.add_role('apilink', api_link_role)
Now, in conf.py, add 'apilinks' to the extensions list and set an appropriate value for 'apilinks_base' (otherwise, it will default to 'http://localhost/'). My file looks like this:
extensions = ['sphinx.ext.autodoc', 'apilinks']
# lots of other stuff
apilinks_base = 'http://host:88/base/'
Usage:
:apilink:`path`
Output:
http://host:88/base/path
You can write a Sphinx extension that creates a role like
:apilink:`path`
and generates the link from that. I never did this, so I can't help more than giving this pointer, sorry. You should try to look at how the various roles are implemented. Many are very similar to what you need, I think.

Categories