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
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.
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)
31 Example of warnings it raises:
33 ::
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').
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:]
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")
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))
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
84class OverrideDocFieldTransformer:
85 """
86 Overrides one function with assigning it to a method
87 """
89 def __init__(self, replaced):
90 """
91 Constructor
93 @param replaced should be *DocFieldTransformer.transform*
94 """
95 self.replaced = replaced
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.
106 @param other_self the builder
107 @param node node the replaced function changes or replace
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 = {}
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))
136 # sort out unknown fields
137 extracted = []
138 if keyfieldtype == 'parameter':
139 # numpydoc
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
155 typename = typedesc.name
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
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
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
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
193 # Import object, get the list of parameters
194 docs = fieldbody.parent.source.split(":docstring of")[-1].strip()
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)
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
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
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])
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
259 docname = fieldbody.parent.source.split(':docstring')[0]
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)
274 return self.replaced(other_self, node)
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.
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)
290 def local_transform(me, node):
291 "local function"
292 return inst.override_transform(me, node)
294 DocFieldTransformer.transform = local_transform
295 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
298def setup(app):
299 "setup for docassert"
300 return setup_docassert(app)