# -*- coding: utf-8 -*-
"""
@file
@brief Defines a way to reference a package or a page in this package.
"""
import sphinx
from docutils import nodes
from ..import_object_helper import import_any_object
class epkg_node(nodes.TextElement):
"""
Defines *epkg* node.
"""
pass
class ClassStruct:
"""
Class as struct.
"""
def __init__(self, **kwargs):
"""
All arguments are added to the class.
"""
for k, v in kwargs.items():
setattr(self, k, v)
[docs]
def epkg_role(role, rawtext, text, lineno, inliner, options=None, content=None):
"""
Defines custom role *epkg*. A list of supported urls must be defined in the
configuration file. It wants to replace something like:
::
`to_html <https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_html.html>`_
By:
::
:epkg:`pandas:DataFrame.to_html`
It inserts in the configuration the variable:
::
epkg_dictionary = {
'pandas': (
'http://pandas.pydata.org/pandas-docs/stable/generated/',
('http://pandas.pydata.org/pandas-docs/stable/generated/{0}.html', 1)
),
# 1 for one parameter
'*py': (
'https://docs.python.org/3/',
('https://docs.python.org/3/library/{0}.html#{0}.{1}', 2)
),
}
If the module name starts with a '*', the anchor does not contain it.
See also :ref:`l-sphinx-epkg`.
If no template is found, the role will look into the list of options
to see if there is one function. It must be the last one.
::
def my_custom_links(input):
return "string to display", "url"
epkg_dictionary = {
'weird_package': (
'http://pandas.pydata.org/pandas-docs/stable/generated/',
('http://pandas.pydata.org/pandas-docs/stable/generated/{0}.html', 1),
my_custom_links
)
However, it is impossible to use a function as a value
in the configuration because :epkg:`*py:pickle` does not handle
this scenario (see `PicklingError on environment when config option
value is a callable <https://github.com/sphinx-doc/sphinx/issues/1424>`_),
``my_custom_links`` needs to be replaced by:
``("module_where_it_is_defined.function_name", None)``.
The role *epkg* will import it based on its name.
:param role: The role name used in the document.
:param rawtext: The entire markup snippet, with role.
:param text: The text marked with the role.
:param lineno: The line number where rawtext appears in the input.
:param inliner: The inliner instance that called us.
:param options: Directive options for customization.
:param content: The directive content for customization.
"""
# It extracts the pieces of the text.
spl = text.split(":")
if len(spl) == 0:
msg = inliner.reporter.error("empty value for role epkg", line=lineno)
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
# Configuration.
env = inliner.document.settings.env
app = env.app
config = app.config
try:
epkg_dictionary = config.epkg_dictionary
except AttributeError as e:
ma = "\n".join(sorted(str(_) for _ in app.config))
raise AttributeError(
"unable to find 'epkg_dictionary' in configuration. Available:\n{0}"
"".format(ma)
) from e
# Supported module?
modname = spl[0]
if modname not in epkg_dictionary:
txt = ", ".join(sorted(epkg_dictionary.keys()))
msg = inliner.reporter.error(
f"Unable to find {modname!r} in epkg_dictionary, existing={txt}",
line=lineno,
)
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
if len(spl) == 1:
value = epkg_dictionary[modname]
if isinstance(value, tuple):
if len(value) == 0:
msg = inliner.reporter.error(
f"Empty values for module '{modname}' in epkg_dictionary."
)
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
value = value[0]
anchor, url = modname, value
else:
value = epkg_dictionary[modname]
expected = len(spl) - 1
found = None
for tu in value:
if isinstance(tu, tuple) and len(tu) == 2 and tu[1] == expected:
found = tu[0]
if found is None:
if callable(value[-1]):
found = value[-1]
elif (
isinstance(value[-1], tuple)
and len(value[-1]) == 2
and value[-1][-1] is None
):
# It assumes the first parameter is a name of a function.
namef = value[-1][0]
if not hasattr(config, namef):
# It assumes its name is defined in a package.
found = import_any_object(namef)[0]
else:
# Defined in the configuration.
found = getattr(config, namef)
if found is None:
msg = inliner.reporter.error(
"Unable to find a tuple with '{0}' parameters in epkg_dictionary['{1}']"
"".format(expected, modname)
)
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
if callable(found):
try:
anchor, url = found(text)
except TypeError:
try:
anchor, url = found()(text)
except Exception as e:
raise ValueError(
"epkg accepts function or classes with __call__ overloaded. "
"Found '{0}'".format(found)
) from e
else:
url = found.format(*tuple(spl[1:]))
if spl[0].startswith("*"):
anchor = ".".join(spl[1:])
else:
anchor = ".".join(spl)
extref = f"`{anchor} <{url}>`__"
node = epkg_node(rawtext=rawtext)
node["classes"] += ["epkg"]
memo = ClassStruct(
document=inliner.document, reporter=inliner.reporter, language=inliner.language
)
processed, messages = inliner.parse(extref, lineno, memo, node)
if len(messages) > 0:
msg = inliner.reporter.error(
"unable to interpret '{0}', messages={1}".format(
text, ", ".join(str(_) for _ in messages)
),
line=lineno,
)
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
node += processed
return [node], []
def visit_epkg_node(self, node):
"""
What to do when visiting a node *epkg*.
"""
pass
def depart_epkg_node(self, node):
"""
What to do when leaving a node *epkg*.
"""
pass
def setup(app):
"""
setup for ``bigger`` (sphinx)
"""
if hasattr(app, "add_mapping"):
app.add_mapping("epkg", epkg_node)
app.add_config_value("epkg_dictionary", {}, "env")
app.add_node(
epkg_node,
html=(visit_epkg_node, depart_epkg_node),
epub=(visit_epkg_node, depart_epkg_node),
elatex=(visit_epkg_node, depart_epkg_node),
latex=(visit_epkg_node, depart_epkg_node),
rst=(visit_epkg_node, depart_epkg_node),
md=(visit_epkg_node, depart_epkg_node),
text=(visit_epkg_node, depart_epkg_node),
)
app.add_role("epkg", epkg_role)
return {"version": sphinx.__display_version__, "parallel_read_safe": True}