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

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 

10 

11 

12class githublink_node(nodes.Element): 

13 

14 """ 

15 defines *githublink* node 

16 """ 

17 pass 

18 

19 

20def make_link_node(rawtext, app, path, anchor, lineno, options, settings): 

21 """ 

22 Create a link to a github file. 

23 

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 

31 

32 The configuration of the documentation must contain 

33 a member ``githublink_options`` (a dictionary) which contains the following fields 

34 either the pair: 

35 

36 * *user*, *project*: to form the url 

37 ``https://github.com/<user>/<project>`` 

38 

39 Or the field: 

40 

41 * *processor*: function with takes a path and a line number and returns an url and an anchor name 

42 

43 Example: 

44 

45 :: 

46 

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 

95 

96 

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. 

102 

103 :githublink:`source on GitHub|py` 

104 

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], []`` 

113 

114 The pipe ``|`` indicates that an extension must be added to 

115 *docname* to get the true url. 

116 

117 Different formats handled by the role: 

118 

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. 

123 

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 

134 

135 app = inliner.document.settings.env.app 

136 docname = inliner.document.settings.env.docname 

137 folder = docname 

138 

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 

161 

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" 

171 

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") 

178 

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}'.") 

198 

199 # Path with extension. 

200 if ext is not None: 

201 path += ext 

202 path = path.replace("\\", "/") 

203 

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('/') + '/' 

207 

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], [] 

221 

222 

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}