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 :epkg:`sphinx` extension which if all parameters are documented. 

5""" 

6import inspect 

7from docutils import nodes 

8import sphinx 

9from sphinx.util import logging 

10from sphinx.util.docfields import DocFieldTransformer, _is_single_paragraph 

11from .import_object_helper import import_any_object 

12 

13 

14def check_typed_make_field(self, types, domain, items, env=None, parameters=None, 

15 function_name=None, docname=None, kind=None): 

16 """ 

17 Overwrites function 

18 `make_field <https://github.com/sphinx-doc/sphinx/blob/master/sphinx/util/docfields.py#L197>`_. 

19 Processes one argument of a function. 

20 

21 @param self from original function 

22 @param types from original function 

23 @param domain from original function 

24 @param items from original function 

25 @param env from original function 

26 @param parameters list of known arguments for the function or method 

27 @param function_name function name these arguments belong to 

28 @param docname document which contains the object 

29 @param kind tells which kind of object *function_name* is (function, method or class) 

30 

31 Example of warnings it raises: 

32 

33 :: 

34 

35 [docassert] 'onefunction' has no parameter 'a' (in '...project_name\\subproject\\myexampleb.py'). 

36 [docassert] 'onefunction' has undocumented parameters 'a, b' (...project_name\\subproject\\myexampleb.py'). 

37 

38 """ 

39 if parameters is None: 

40 parameters = None 

41 check_params = {} 

42 else: 

43 parameters = list(parameters) 

44 if kind == "method": 

45 parameters = parameters[1:] 

46 

47 def kg(p): 

48 "local function" 

49 return p if isinstance(p, str) else p.name 

50 check_params = {kg(p): 0 for p in parameters} 

51 logger = logging.getLogger("docassert") 

52 

53 def check_item(fieldarg, content, logger): 

54 "local function" 

55 if fieldarg not in check_params: 

56 if function_name is not None: 

57 logger.warning("[docassert] '{0}' has no parameter '{1}' (in '{2}').".format( 

58 function_name, fieldarg, docname)) 

59 else: 

60 check_params[fieldarg] += 1 

61 if check_params[fieldarg] > 1: 

62 logger.warning("[docassert] '{1}' of '{0}' is duplicated (in '{2}').".format( 

63 function_name, fieldarg, docname)) 

64 

65 if isinstance(items, list): 

66 for fieldarg, content in items: 

67 check_item(fieldarg, content, logger) 

68 mini = None if len(check_params) == 0 else min(check_params.values()) 

69 if mini == 0: 

70 check_params = list(check_params.items()) 

71 nodoc = list(sorted(k for k, v in check_params if v == 0)) 

72 if len(nodoc) > 0: 

73 if len(nodoc) == 1 and nodoc[0] == 'self': 

74 # Behavior should be improved. 

75 pass 

76 else: 

77 logger.warning("[docassert] '{0}' has undocumented parameters '{1}' (in '{2}').".format( 

78 function_name, ", ".join(nodoc), docname)) 

79 else: 

80 # Documentation related to the return. 

81 pass 

82 

83 

84class OverrideDocFieldTransformer: 

85 """ 

86 Overrides one function with assigning it to a method 

87 """ 

88 

89 def __init__(self, replaced): 

90 """ 

91 Constructor 

92 

93 @param replaced should be *DocFieldTransformer.transform* 

94 """ 

95 self.replaced = replaced 

96 

97 def override_transform(self, other_self, node): 

98 """ 

99 Transform a single field list *node*. 

100 Overwrite function `transform 

101 <https://github.com/sphinx-doc/sphinx/blob/ 

102 master/sphinx/util/docfields.py#L271>`_. 

103 It only adds extra verification and returns results from 

104 the replaced function. 

105 

106 @param other_self the builder 

107 @param node node the replaced function changes or replace 

108 

109 The function parses the original function and checks that the list 

110 of arguments declared by the function is the same the list of 

111 documented arguments. 

112 """ 

113 typemap = other_self.typemap 

114 entries = [] 

115 groupindices = {} 

116 types = {} 

117 

118 # step 1: traverse all fields and collect field types and content 

119 for field in node: 

120 fieldname, fieldbody = field 

121 try: 

122 # split into field type and argument 

123 fieldtype, fieldarg = fieldname.astext().split(None, 1) 

124 except ValueError: 

125 # maybe an argument-less field type? 

126 fieldtype, fieldarg = fieldname.astext(), '' 

127 if fieldtype == "Parameters": 

128 # numpydoc style 

129 keyfieldtype = 'parameter' 

130 elif fieldtype == "param": 

131 keyfieldtype = 'param' 

132 else: 

133 continue 

134 typedesc, is_typefield = typemap.get(keyfieldtype, (None, None)) 

135 

136 # sort out unknown fields 

137 extracted = [] 

138 if keyfieldtype == 'parameter': 

139 # numpydoc 

140 

141 for child in fieldbody.children: 

142 if isinstance(child, nodes.definition_list): 

143 for child2 in child.children: 

144 extracted.append(child2) 

145 elif typedesc is None or typedesc.has_arg != bool(fieldarg): 

146 # either the field name is unknown, or the argument doesn't 

147 # match the spec; capitalize field name and be done with it 

148 new_fieldname = fieldtype[0:1].upper() + fieldtype[1:] 

149 if fieldarg: 

150 new_fieldname += ' ' + fieldarg 

151 fieldname[0] = nodes.Text(new_fieldname) 

152 entries.append(field) 

153 continue 

154 

155 typename = typedesc.name 

156 

157 # collect the content, trying not to keep unnecessary paragraphs 

158 if extracted: 

159 content = extracted 

160 elif _is_single_paragraph(fieldbody): 

161 content = fieldbody.children[0].children 

162 else: 

163 content = fieldbody.children 

164 

165 # if the field specifies a type, put it in the types collection 

166 if is_typefield: 

167 # filter out only inline nodes; others will result in invalid 

168 # markup being written out 

169 content = [n for n in content if isinstance( 

170 n, (nodes.Inline, nodes.Text))] 

171 if content: 

172 types.setdefault(typename, {})[fieldarg] = content 

173 continue 

174 

175 # also support syntax like ``:param type name:`` 

176 if typedesc.is_typed: 

177 try: 

178 argtype, argname = fieldarg.split(None, 1) 

179 except ValueError: 

180 pass 

181 else: 

182 types.setdefault(typename, {})[argname] = [ 

183 nodes.Text(argtype)] 

184 fieldarg = argname 

185 

186 translatable_content = nodes.inline( 

187 fieldbody.rawsource, translatable=True) 

188 translatable_content.document = fieldbody.parent.document 

189 translatable_content.source = fieldbody.parent.source 

190 translatable_content.line = fieldbody.parent.line 

191 translatable_content += content 

192 

193 # Import object, get the list of parameters 

194 docs = fieldbody.parent.source.split(":docstring of")[-1].strip() 

195 

196 myfunc = None 

197 funckind = None 

198 function_name = None 

199 excs = [] 

200 try: 

201 myfunc, function_name, funckind = import_any_object(docs) 

202 except ImportError as e: 

203 excs.append(e) 

204 

205 if myfunc is None: 

206 if len(excs) > 0: 

207 reasons = "\n".join(" {0}".format(e) for e in excs) 

208 else: 

209 reasons = "unknown" 

210 logger = logging.getLogger("docassert") 

211 logger.warning( 

212 "[docassert] unable to import object '{0}', reasons:\n{1}".format(docs, reasons)) 

213 myfunc = None 

214 

215 if myfunc is None: 

216 signature = None 

217 parameters = None 

218 else: 

219 try: 

220 signature = inspect.signature(myfunc) 

221 parameters = signature.parameters 

222 except (TypeError, ValueError): 

223 logger = logging.getLogger("docassert") 

224 logger.warning( 

225 "[docassert] unable to get signature of '{0}'.".format(docs)) 

226 signature = None 

227 parameters = None 

228 

229 # grouped entries need to be collected in one entry, while others 

230 # get one entry per field 

231 if extracted: 

232 # numpydoc 

233 group_entries = [] 

234 for ext in extracted: 

235 name = ext.astext().split('\n')[0].split()[0] 

236 group_entries.append((name, ext)) 

237 entries.append([typedesc, group_entries]) 

238 elif typedesc.is_grouped: 

239 if typename in groupindices: 

240 group = entries[groupindices[typename]] 

241 else: 

242 groupindices[typename] = len(entries) 

243 group = [typedesc, []] 

244 entries.append(group) 

245 entry = typedesc.make_entry(fieldarg, [translatable_content]) 

246 group[1].append(entry) 

247 else: 

248 entry = typedesc.make_entry(fieldarg, [translatable_content]) 

249 entries.append([typedesc, entry]) 

250 

251 # step 2: all entries are collected, check the parameters list. 

252 try: 

253 env = other_self.directive.state.document.settings.env 

254 except AttributeError as e: 

255 logger = logging.getLogger("docassert") 

256 logger.warning("[docassert] {0}".format(e)) 

257 env = None 

258 

259 docname = fieldbody.parent.source.split(':docstring')[0] 

260 

261 for entry in entries: 

262 if isinstance(entry, nodes.field): 

263 logger = logging.getLogger("docassert") 

264 logger.warning( 

265 "[docassert] unable to check [nodes.field] {0}".format(entry)) 

266 else: 

267 fieldtype, content = entry 

268 fieldtypes = types.get(fieldtype.name, {}) 

269 check_typed_make_field(other_self, fieldtypes, other_self.directive.domain, 

270 content, env=env, parameters=parameters, 

271 function_name=function_name, docname=docname, 

272 kind=funckind) 

273 

274 return self.replaced(other_self, node) 

275 

276 

277def setup_docassert(app): 

278 """ 

279 Setup for ``docassert`` extension (sphinx). 

280 This changes ``DocFieldTransformer.transform`` and replaces 

281 it by a function which calls the current function and does 

282 extra checking on the list of parameters. 

283 

284 .. warning:: This class does not handle methods if the parameter name 

285 for the class is different from *self*. Classes included in other 

286 classes are not properly handled. 

287 """ 

288 inst = OverrideDocFieldTransformer(DocFieldTransformer.transform) 

289 

290 def local_transform(me, node): 

291 "local function" 

292 return inst.override_transform(me, node) 

293 

294 DocFieldTransformer.transform = local_transform 

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

296 

297 

298def setup(app): 

299 "setup for docassert" 

300 return setup_docassert(app)