Coverage for pyquickhelper/sphinxext/sphinx_epkg_extension.py: 99%
68 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Defines a way to reference a package or a page in this package.
5"""
7import sphinx
8from docutils import nodes
9from .import_object_helper import import_any_object
12class epkg_node(nodes.TextElement):
14 """
15 Defines *epkg* node.
16 """
17 pass
20class ClassStruct:
21 """
22 Class as struct.
23 """
25 def __init__(self, **kwargs):
26 """
27 All arguments are added to the class.
28 """
29 for k, v in kwargs.items():
30 setattr(self, k, v)
33def epkg_role(role, rawtext, text, lineno, inliner, options=None, content=None):
34 """
35 Defines custom role *epkg*. A list of supported urls must be defined in the
36 configuration file. It wants to replace something like:
38 ::
40 `to_html <https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_html.html>`_
42 By:
44 ::
46 :epkg:`pandas:DataFrame.to_html`
48 It inserts in the configuration the variable:
50 ::
52 epkg_dictionary = {'pandas': ('http://pandas.pydata.org/pandas-docs/stable/generated/',
53 ('http://pandas.pydata.org/pandas-docs/stable/generated/{0}.html', 1))
54 # 1 for one paraemter
55 '*py': ('https://docs.python.org/3/',
56 ('https://docs.python.org/3/library/{0}.html#{0}.{1}', 2))
57 }
59 If the module name starts with a '*', the anchor does not contain it.
60 See also :ref:`l-sphinx-epkg`.
61 If no template is found, the role will look into the list of options
62 to see if there is one function. It must be the last one.
64 ::
66 def my_custom_links(input):
67 return "string to display", "url"
69 epkg_dictionary = {'weird_package': ('http://pandas.pydata.org/pandas-docs/stable/generated/',
70 ('http://pandas.pydata.org/pandas-docs/stable/generated/{0}.html', 1),
71 my_custom_links)
73 However, it is impossible to use a function as a value
74 in the configuration because :epkg:`*py:pickle` does not handle
75 this scenario (see `PicklingError on environment when config option
76 value is a callable <https://github.com/sphinx-doc/sphinx/issues/1424>`_),
77 ``my_custom_links`` needs to be replaced by:
78 ``("module_where_it_is_defined.function_name", None)``.
79 The role *epkg* will import it based on its name.
81 :param role: The role name used in the document.
82 :param rawtext: The entire markup snippet, with role.
83 :param text: The text marked with the role.
84 :param lineno: The line number where rawtext appears in the input.
85 :param inliner: The inliner instance that called us.
86 :param options: Directive options for customization.
87 :param content: The directive content for customization.
88 """
89 # It extracts the pieces of the text.
90 spl = text.split(":")
91 if len(spl) == 0: # pragma: no cover
92 msg = inliner.reporter.error("empty value for role epkg", line=lineno)
93 prb = inliner.problematic(rawtext, rawtext, msg)
94 return [prb], [msg]
96 # Configuration.
97 env = inliner.document.settings.env
98 app = env.app
99 config = app.config
100 try:
101 epkg_dictionary = config.epkg_dictionary
102 except AttributeError as e: # pragma: no cover
103 ma = "\n".join(sorted(str(_) for _ in app.config))
104 raise AttributeError(
105 "unable to find 'epkg_dictionary' in configuration. Available:\n{0}"
106 "".format(ma)) from e
108 # Supported module?
109 modname = spl[0]
110 if modname not in epkg_dictionary:
111 msg = inliner.reporter.error(
112 "Unable to find module '{0}' in epkg_dictionary, existing={1}".format(
113 modname, ", ".join(sorted(epkg_dictionary.keys())), line=lineno))
114 prb = inliner.problematic(rawtext, rawtext, msg)
115 return [prb], [msg]
117 if len(spl) == 1:
118 value = epkg_dictionary[modname]
119 if isinstance(value, tuple):
120 if len(value) == 0: # pragma: no cover
121 msg = inliner.reporter.error(
122 f"Empty values for module '{modname}' in epkg_dictionary.")
123 prb = inliner.problematic(rawtext, rawtext, msg)
124 return [prb], [msg]
125 value = value[0]
126 anchor, url = modname, value
127 else:
128 value = epkg_dictionary[modname]
129 expected = len(spl) - 1
130 found = None
131 for tu in value:
132 if isinstance(tu, tuple) and len(tu) == 2 and tu[1] == expected:
133 found = tu[0]
134 if found is None:
135 if callable(value[-1]):
136 found = value[-1]
137 elif isinstance(value[-1], tuple) and len(value[-1]) == 2 and value[-1][-1] is None:
138 # It assumes the first parameter is a name of a function.
139 namef = value[-1][0]
140 if not hasattr(config, namef):
141 # It assumes its name is defined in a package.
142 found = import_any_object(namef)[0]
143 else:
144 # Defined in the configuration.
145 found = getattr(config, namef)
147 if found is None: # pragma: no cover
148 msg = inliner.reporter.error(
149 "Unable to find a tuple with '{0}' parameters in epkg_dictionary['{1}']"
150 "".format(expected, modname))
151 prb = inliner.problematic(rawtext, rawtext, msg)
152 return [prb], [msg]
154 if callable(found):
155 try:
156 anchor, url = found(text)
157 except TypeError:
158 try:
159 anchor, url = found()(text)
160 except Exception as e: # pragma: no cover
161 raise ValueError(
162 "epkg accepts function or classes with __call__ overloaded. "
163 "Found '{0}'".format(found)) from e
164 else:
165 url = found.format(*tuple(spl[1:]))
166 if spl[0].startswith("*"):
167 anchor = ".".join(spl[1:]) # pragma: no cover
168 else:
169 anchor = ".".join(spl)
171 extref = f"`{anchor} <{url}>`__"
172 node = epkg_node(rawtext=rawtext)
173 node['classes'] += ["epkg"]
175 memo = ClassStruct(document=inliner.document, reporter=inliner.reporter,
176 language=inliner.language)
177 processed, messages = inliner.parse(extref, lineno, memo, node)
178 if len(messages) > 0: # pragma: no cover
179 msg = inliner.reporter.error(
180 "unable to interpret '{0}', messages={1}".format(
181 text, ", ".join(str(_) for _ in messages)), line=lineno)
182 prb = inliner.problematic(rawtext, rawtext, msg)
183 return [prb], [msg]
185 node += processed
186 return [node], []
189def visit_epkg_node(self, node):
190 """
191 What to do when visiting a node *epkg*.
192 """
193 pass
196def depart_epkg_node(self, node):
197 """
198 What to do when leaving a node *epkg*.
199 """
200 pass
203def setup(app):
204 """
205 setup for ``bigger`` (sphinx)
206 """
207 if hasattr(app, "add_mapping"):
208 app.add_mapping('epkg', epkg_node)
210 app.add_config_value('epkg_dictionary', {}, 'env')
211 app.add_node(epkg_node,
212 html=(visit_epkg_node, depart_epkg_node),
213 epub=(visit_epkg_node, depart_epkg_node),
214 elatex=(visit_epkg_node, depart_epkg_node),
215 latex=(visit_epkg_node, depart_epkg_node),
216 rst=(visit_epkg_node, depart_epkg_node),
217 md=(visit_epkg_node, depart_epkg_node),
218 text=(visit_epkg_node, depart_epkg_node))
220 app.add_role('epkg', epkg_role)
221 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}