Coverage for pyquickhelper/sphinxext/sphinx_githublink_extension.py: 92%
103 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 :epkg:`sphinx` extension to display a link on github.
5"""
6import os
7import sphinx
8from docutils import nodes
9from docutils.parsers.rst.roles import set_classes
12class githublink_node(nodes.Element):
14 """
15 defines *githublink* node
16 """
17 pass
20def make_link_node(rawtext, app, path, anchor, lineno, options, settings):
21 """
22 Create a link to a github file.
24 :param rawtext: Text being replaced with link node.
25 :param app: Sphinx application context
26 :param path: path to filename
27 :param lineno: line number
28 :param anchor: anchor
29 :param options: Options dictionary passed to role func.
30 :param settings: settings
32 The configuration of the documentation must contain
33 a member ``githublink_options`` (a dictionary) which contains the following fields
34 either the pair:
36 * *user*, *project*: to form the url
37 ``https://github.com/<user>/<project>``
39 Or the field:
41 * *processor*: function with takes a path and a line number and returns an url and an anchor name
43 Example:
45 ::
47 def processor_github(path, lineno):
48 url = "https://github.com/{0}/{1}/blob/master/{2}".format(user, project, path)
49 if lineno:
50 url += "#L{0}".format(lineno)
51 return url, "source on GitHub"
52 """
53 try:
54 exc = []
55 try:
56 config = app.config
57 except AttributeError as e: # pragma: no cover
58 exc.append(e)
59 config = None
60 if config is not None:
61 try:
62 opt = config.githublink_options
63 except AttributeError as ee: # pragma: no cover
64 exc.append(ee)
65 opt = None
66 else:
67 opt = None # pragma: no cover
68 if not opt:
69 try:
70 opt = settings.githublink_options
71 except AttributeError as eee:
72 exc.append(eee)
73 opt = None
74 if not opt:
75 lines = "\n".join(f"## {str(e)} ##" for e in exc)
76 raise AttributeError(
77 f"settings does not have a key githublink_options, app does not have a member config.\n{lines}")
78 except AttributeError:
79 # it just means the role will be ignored
80 return None
81 if "processor" not in opt:
82 user = opt["user"]
83 project = opt["project"]
84 ll = 'x' if '.cpython' in path else ''
85 ref = f"https://github.com/{user}/{project}/blob/master/{path}{ll}"
86 if lineno:
87 ref += f"#L{lineno}"
88 else:
89 ref, anchor = opt["processor"](path, lineno)
90 if anchor == "%" and 'anchor' in opt:
91 anchor = opt['anchor']
92 set_classes(options)
93 node = nodes.reference(rawtext, anchor, refuri=ref, **options)
94 return node
97def githublink_role(role, rawtext, text, lineno, inliner,
98 options=None, content=None):
99 """
100 Defines custom role *githublink*. The following instruction add
101 a link to the documentation on github.
103 :githublink:`source on GitHub|py`
105 :param role: The role name used in the document.
106 :param rawtext: The entire markup snippet, with role.
107 :param text: The text marked with the role.
108 :param lineno: The line number where rawtext appears in the input.
109 :param inliner: The inliner instance that called us.
110 :param options: Directive options for customization (dictionary)
111 :param content: The directive content for customization (list)
112 :return: ``[node], []``
114 The pipe ``|`` indicates that an extension must be added to
115 *docname* to get the true url.
117 Different formats handled by the role:
119 * ``anchor``: anchor = filename, line number is guess form the position in the file
120 * ``anchor|py|*``: extension *.py* is added to the anchor, no line number
121 * ``anchor|py|45``: extension *.py* is added to the anchor, line number is 45
122 * ``%|py|45``: the anchor name comes from the variable ``githublink_options['anchor']`` in the configuration file.
124 A suffix can be added to the extension ``rst-doc`` to tell the extension
125 the source comes from the subfolder ``_doc/sphinx/source`` and not from
126 a subfolder like ``src``.
127 """
128 if options is None:
129 options = {}
130 if content is None:
131 content = []
132 if not rawtext or len(rawtext) == 0:
133 rawtext = "source" # pragma: no cover
135 app = inliner.document.settings.env.app
136 docname = inliner.document.settings.env.docname
137 folder = docname
139 # Retrieves extension and path.
140 text0 = text
141 path = None
142 if "|" in text:
143 # No extension to the url, it adds one.
144 spl = text.split("|")
145 if len(spl) == 3:
146 text, ext, no = spl
147 if len(ext) > 7 or "." in ext:
148 path = ext
149 ext = None
150 else:
151 ext = "." + ext
152 lineno = int(no) if no != "*" else None
153 elif len(spl) != 2:
154 raise ValueError( # pragma: no cover
155 f"Unable to interpret '{text}'.")
156 else:
157 text, ext = spl
158 ext = "." + ext
159 else:
160 ext = None
162 # -
163 if ext is not None and "-" in ext:
164 spl = ext.split("-")
165 if len(spl) != 2:
166 raise ValueError( # pragma: no cover
167 f"Unable to interpret extension in '{text0}'")
168 ext, doc = spl
169 else:
170 doc = "src"
172 # Get path to source.
173 if path is None:
174 git = os.path.join(folder, ".git")
175 while len(folder) > 0 and not os.path.exists(git):
176 folder = os.path.split(folder)[0]
177 git = os.path.join(folder, ".git")
179 if len(folder) > 0:
180 path = docname[len(folder):]
181 elif doc == "src":
182 path = docname
183 source_doc = inliner.document.settings._source
184 if source_doc is not None:
185 source_doc = source_doc.replace("\\", "/")
186 spl = source_doc.split('/')
187 if '_doc' in spl:
188 sub_doc = spl[:spl.index('_doc')]
189 root_doc = "/".join(sub_doc)
190 root_doc_src = os.path.join(root_doc, 'src')
191 if os.path.exists(root_doc_src):
192 path = os.path.join('src', docname) # pragma: no cover
193 elif doc == "doc":
194 path = os.path.join('_doc', 'sphinxdoc', 'source', docname)
195 else:
196 raise ValueError( # pragma: no cover
197 f"Unable to interpret subfolder in '{text0}'.")
199 # Path with extension.
200 if ext is not None:
201 path += ext
202 path = path.replace("\\", "/")
204 # Get rid of binaries (.pyd, .so) --> add a link to the root.
205 if path.endswith(".pyd") or path.endswith(".so"):
206 path = "/".join(path.split("/")[:-1]).rstrip('/') + '/'
208 # Add node.
209 try:
210 node = make_link_node(rawtext=rawtext, app=app, path=path, lineno=lineno,
211 options=options, anchor=text, settings=inliner.document.settings)
212 except (ValueError, AttributeError) as e: # pragma: no cover
213 msg = inliner.reporter.error(
214 'githublink_options must be set to a dictionary with keys '
215 '(user, project)\n%s' % str(e), line=lineno)
216 prb = inliner.problematic(rawtext, rawtext, msg)
217 return [prb], [msg]
218 if node is None:
219 return [], []
220 return [node], []
223def setup(app):
224 """
225 setup for ``githublink`` (:epkg:`sphinx`)
226 """
227 app.add_role('githublink', githublink_role)
228 app.add_role('gitlink', githublink_role)
229 app.add_config_value('githublink_options', None, 'env')
230 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}