Coverage for pyquickhelper/sphinxext/sphinx_docassert_extension.py: 75%

159 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 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 #L197>`_. 

19 `make_field <https://github.com/sphinx-doc/sphinx/blob/master/sphinx/util/docfields.py 

20 Processes one argument of a function. 

21 

22 @param self from original function 

23 @param types from original function 

24 @param domain from original function 

25 @param items from original function 

26 @param env from original function 

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

28 @param function_name function name these arguments belong to 

29 @param docname document which contains the object 

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

31 

32 Example of warnings it raises: 

33 

34 :: 

35 

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

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

38 

39 """ 

40 if parameters is None: 

41 parameters = None 

42 check_params = {} 

43 else: 

44 parameters = list(parameters) 

45 if kind == "method": 

46 parameters = parameters[1:] 

47 

48 def kg(p): 

49 "local function" 

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

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

52 logger = logging.getLogger("docassert") 

53 

54 def check_item(fieldarg, content, logger): 

55 "local function" 

56 if fieldarg not in check_params: 

57 if function_name is not None: 

58 logger.warning("[docassert] %r has no parameter %r (in %r).", 

59 function_name, fieldarg, docname) 

60 else: 

61 check_params[fieldarg] += 1 

62 if check_params[fieldarg] > 1: 

63 logger.warning("[docassert] %r of %r is duplicated (in %r).", 

64 fieldarg, function_name, docname) 

65 

66 if isinstance(items, list): 

67 for fieldarg, content in items: 

68 check_item(fieldarg, content, logger) 

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

70 if mini == 0: 

71 check_params = list(check_params.items()) 

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

73 if len(nodoc) > 0: 

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

75 # Behavior should be improved. 

76 pass 

77 else: 

78 logger.warning("[docassert] %r has undocumented parameters %r (in %r).", 

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

80 else: 

81 # Documentation related to the return. 

82 pass 

83 

84 

85class OverrideDocFieldTransformer: 

86 """ 

87 Overrides one function with assigning it to a method 

88 """ 

89 

90 def __init__(self, replaced): 

91 """ 

92 Constructor 

93 

94 @param replaced should be *DocFieldTransformer.transform* 

95 """ 

96 self.replaced = replaced 

97 

98 def override_transform(self, other_self, node): 

99 """ 

100 Transform a single field list *node*. 

101 Overwrite function `transform 

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

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

104 It only adds extra verification and returns results from 

105 the replaced function. 

106 

107 @param other_self the builder 

108 @param node node the replaced function changes or replace 

109 

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

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

112 documented arguments. 

113 """ 

114 typemap = other_self.typemap 

115 entries = [] 

116 groupindices = {} 

117 types = {} 

118 

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

120 for field in node: 

121 fieldname, fieldbody = field 

122 try: 

123 # split into field type and argument 

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

125 except ValueError: 

126 # maybe an argument-less field type? 

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

128 if fieldtype == "Parameters": 

129 # numpydoc style 

130 keyfieldtype = 'parameter' 

131 elif fieldtype == "param": 

132 keyfieldtype = 'param' 

133 else: 

134 continue 

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

136 

137 # sort out unknown fields 

138 extracted = [] 

139 if keyfieldtype == 'parameter': 

140 # numpydoc 

141 

142 for child in fieldbody.children: 

143 if isinstance(child, nodes.definition_list): 

144 for child2 in child.children: 

145 extracted.append(child2) 

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

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

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

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

150 if fieldarg: 

151 new_fieldname += ' ' + fieldarg 

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

153 entries.append(field) 

154 continue 

155 

156 typename = typedesc.name 

157 

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

159 if extracted: 

160 content = extracted 

161 elif _is_single_paragraph(fieldbody): 

162 content = fieldbody.children[0].children 

163 else: 

164 content = fieldbody.children 

165 

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

167 if is_typefield: 

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

169 # markup being written out 

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

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

172 if content: 

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

174 continue 

175 

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

177 if typedesc.is_typed: 

178 try: 

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

180 except ValueError: 

181 pass 

182 else: 

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

184 nodes.Text(argtype)] 

185 fieldarg = argname 

186 

187 translatable_content = nodes.inline( 

188 fieldbody.rawsource, translatable=True) 

189 translatable_content.document = fieldbody.parent.document 

190 translatable_content.source = fieldbody.parent.source 

191 translatable_content.line = fieldbody.parent.line 

192 translatable_content += content 

193 

194 # Import object, get the list of parameters 

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

196 

197 myfunc = None 

198 funckind = None 

199 function_name = None 

200 excs = [] 

201 try: 

202 myfunc, function_name, funckind = import_any_object(docs) 

203 except ImportError as e: 

204 excs.append(e) 

205 

206 if myfunc is None: 

207 if len(excs) > 0: 

208 reasons = "\n".join(f" {e}" for e in excs) 

209 else: 

210 reasons = "unknown" 

211 logger = logging.getLogger("docassert") 

212 logger.warning("[docassert] unable to import object %r, reasons:\n%s", 

213 docs, reasons) 

214 myfunc = None 

215 

216 if myfunc is None: 

217 signature = None 

218 parameters = None 

219 else: 

220 try: 

221 signature = inspect.signature(myfunc) 

222 parameters = signature.parameters 

223 except (TypeError, ValueError): 

224 logger = logging.getLogger("docassert") 

225 logger.warning( 

226 "[docassert] unable to get signature of %r.", docs) 

227 signature = None 

228 parameters = None 

229 

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

231 # get one entry per field 

232 if extracted: 

233 # numpydoc 

234 group_entries = [] 

235 for ext in extracted: 

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

237 group_entries.append((name, ext)) 

238 entries.append([typedesc, group_entries]) 

239 elif typedesc.is_grouped: 

240 if typename in groupindices: 

241 group = entries[groupindices[typename]] 

242 else: 

243 groupindices[typename] = len(entries) 

244 group = [typedesc, []] 

245 entries.append(group) 

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

247 group[1].append(entry) 

248 else: 

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

250 entries.append([typedesc, entry]) 

251 

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

253 try: 

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

255 except AttributeError as e: 

256 logger = logging.getLogger("docassert") 

257 logger.warning("[docassert] %s", e) 

258 env = None 

259 

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

261 

262 for entry in entries: 

263 if isinstance(entry, nodes.field): 

264 logger = logging.getLogger("docassert") 

265 logger.warning( 

266 "[docassert] unable to check [nodes.field] %s", entry) 

267 else: 

268 fieldtype, content = entry 

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

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

271 content, env=env, parameters=parameters, 

272 function_name=function_name, docname=docname, 

273 kind=funckind) 

274 

275 return self.replaced(other_self, node) 

276 

277 

278def setup_docassert(app): 

279 """ 

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

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

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

283 extra checking on the list of parameters. 

284 

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

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

287 classes are not properly handled. 

288 """ 

289 inst = OverrideDocFieldTransformer(DocFieldTransformer.transform) 

290 

291 def local_transform(me, node): 

292 "local function" 

293 return inst.override_transform(me, node) 

294 

295 DocFieldTransformer.transform = local_transform 

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

297 

298 

299def setup(app): 

300 "setup for docassert" 

301 return setup_docassert(app)