Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Defines a way to reference a package or a page in this package. 

5""" 

6 

7import sphinx 

8from docutils import nodes 

9from .import_object_helper import import_any_object 

10 

11 

12class epkg_node(nodes.TextElement): 

13 

14 """ 

15 Defines *epkg* node. 

16 """ 

17 pass 

18 

19 

20class ClassStruct: 

21 """ 

22 Class as struct. 

23 """ 

24 

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) 

31 

32 

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: 

37 

38 :: 

39 

40 `to_html <https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_html.html>`_ 

41 

42 By: 

43 

44 :: 

45 

46 :epkg:`pandas:DataFrame.to_html` 

47 

48 It inserts in the configuration the variable: 

49 

50 :: 

51 

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 } 

58 

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. 

63 

64 :: 

65 

66 def my_custom_links(input): 

67 return "string to display", "url" 

68 

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) 

72 

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. 

80 

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] 

95 

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 

107 

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] 

116 

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 "Empty values for module '{0}' in epkg_dictionary.".format(modname)) 

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) 

146 

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] 

153 

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) 

170 

171 extref = "`{0} <{1}>`__".format(anchor, url) 

172 node = epkg_node(rawtext=rawtext) 

173 node['classes'] += ["epkg"] 

174 

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] 

184 

185 node += processed 

186 return [node], [] 

187 

188 

189def visit_epkg_node(self, node): 

190 """ 

191 What to do when visiting a node *epkg*. 

192 """ 

193 pass 

194 

195 

196def depart_epkg_node(self, node): 

197 """ 

198 What to do when leaving a node *epkg*. 

199 """ 

200 pass 

201 

202 

203def setup(app): 

204 """ 

205 setup for ``bigger`` (sphinx) 

206 """ 

207 if hasattr(app, "add_mapping"): 

208 app.add_mapping('epkg', epkg_node) 

209 

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

219 

220 app.add_role('epkg', epkg_role) 

221 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}