Navigation

Source code for pyquickhelper.sphinxext.sphinx_docassert_extension

# -*- coding: utf-8 -*-"""Defines a :epkg:`sphinx` extension which if all parameters are documented.:githublink:`%|py|6`"""importinspectfromdocutilsimportnodesimportsphinxfromsphinx.utilimportloggingfromsphinx.util.docfieldsimportDocFieldTransformer,_is_single_paragraphfrom.import_object_helperimportimport_any_object

[docs]defcheck_typed_make_field(self,types,domain,items,env=None,parameters=None,function_name=None,docname=None,kind=None):""" Overwrites function `make_field <https://github.com/sphinx-doc/sphinx/blob/master/sphinx/util/docfields.py#L197>`_. Processes one argument of a function. :param self: from original function :param types: from original function :param domain: from original function :param items: from original function :param env: from original function :param parameters: list of known arguments for the function or method :param function_name: function name these arguments belong to :param docname: document which contains the object :param kind: tells which kind of object *function_name* is (function, method or class) Example of warnings it raises: :: [docassert] 'onefunction' has no parameter 'a' (in '...project_name\\subproject\\myexampleb.py'). [docassert] 'onefunction' has undocumented parameters 'a, b' (...project_name\\subproject\\myexampleb.py'). :githublink:`%|py|46` """ifparametersisNone:parameters=Nonecheck_params={}else:parameters=list(parameters)ifkind=="method":parameters=parameters[1:]defkg(p):"local function"returnpifisinstance(p,str)elsep.namecheck_params={kg(p):0forpinparameters}logger=logging.getLogger("docassert")defcheck_item(fieldarg,content,logger):"local function"iffieldargnotincheck_params:iffunction_nameisnotNone:logger.warning("[docassert] '{0}' has no parameter '{1}' (in '{2}').".format(function_name,fieldarg,docname))else:check_params[fieldarg]+=1ifcheck_params[fieldarg]>1:logger.warning("[docassert] '{1}' of '{0}' is duplicated (in '{2}').".format(function_name,fieldarg,docname))ifisinstance(items,list):forfieldarg,contentinitems:check_item(fieldarg,content,logger)mini=Noneiflen(check_params)==0elsemin(check_params.values())ifmini==0:check_params=list(check_params.items())nodoc=list(sorted(kfork,vincheck_paramsifv==0))iflen(nodoc)>0:iflen(nodoc)==1andnodoc[0]=='self':# Behavior should be improved.passelse:logger.warning("[docassert] '{0}' has undocumented parameters '{1}' (in '{2}').".format(function_name,", ".join(nodoc),docname))else:# Documentation related to the return.pass

[docs]classOverrideDocFieldTransformer:""" Overrides one function with assigning it to a method :githublink:`%|py|95` """

[docs]defoverride_transform(self,other_self,node):""" Transform a single field list *node*. Overwrite function `transform <https://github.com/sphinx-doc/sphinx/blob/master/sphinx/util/docfields.py#L271>`_. It only adds extra verification and returns results from the replaced function. :param other_self: the builder :param node: node the replaced function changes or replace The function parses the original function and checks that the list of arguments declared by the function is the same the list of documented arguments. :githublink:`%|py|116` """typemap=other_self.typemapentries=[]groupindices={}types={}# step 1: traverse all fields and collect field types and contentforfieldinnode:fieldname,fieldbody=fieldtry:# split into field type and argumentfieldtype,fieldarg=fieldname.astext().split(None,1)exceptValueError:# maybe an argument-less field type?fieldtype,fieldarg=fieldname.astext(),''iffieldtype!="param":continuetypedesc,is_typefield=typemap.get(fieldtype,(None,None))# sort out unknown fieldsiftypedescisNoneortypedesc.has_arg!=bool(fieldarg):# either the field name is unknown, or the argument doesn't# match the spec; capitalize field name and be done with itnew_fieldname=fieldtype[0:1].upper()+fieldtype[1:]iffieldarg:new_fieldname+=' '+fieldargfieldname[0]=nodes.Text(new_fieldname)entries.append(field)continuetypename=typedesc.name# collect the content, trying not to keep unnecessary paragraphsif_is_single_paragraph(fieldbody):content=fieldbody.children[0].childrenelse:content=fieldbody.children# if the field specifies a type, put it in the types collectionifis_typefield:# filter out only inline nodes; others will result in invalid# markup being written outcontent=[nfornincontentifisinstance(n,(nodes.Inline,nodes.Text))]ifcontent:types.setdefault(typename,{})[fieldarg]=contentcontinue# also support syntax like ``:param type name:``iftypedesc.is_typed:try:argtype,argname=fieldarg.split(None,1)exceptValueError:passelse:types.setdefault(typename,{})[argname]=[nodes.Text(argtype)]fieldarg=argnametranslatable_content=nodes.inline(fieldbody.rawsource,translatable=True)translatable_content.document=fieldbody.parent.documenttranslatable_content.source=fieldbody.parent.sourcetranslatable_content.line=fieldbody.parent.linetranslatable_content+=content# Import object, get the list of parametersdocs=fieldbody.parent.source.split(":docstring of")[-1].strip()myfunc=Nonefunckind=Nonefunction_name=Noneexcs=[]try:myfunc,function_name,funckind=import_any_object(docs)exceptImportErrorase:excs.append(e)ifmyfuncisNone:iflen(excs)>0:reasons="\n".join(" {0}".format(e)foreinexcs)else:reasons="unknown"logger=logging.getLogger("docassert")logger.warning("[docassert] unable to import object '{0}', reasons:\n{1}".format(docs,reasons))myfunc=NoneifmyfuncisNone:signature=Noneparameters=Noneelse:try:signature=inspect.signature(myfunc)parameters=signature.parametersexcept(TypeError,ValueError):logger=logging.getLogger("docassert")logger.warning("[docassert] unable to get signature of '{0}'.".format(docs))signature=Noneparameters=None# grouped entries need to be collected in one entry, while others# get one entry per fieldiftypedesc.is_grouped:iftypenameingroupindices:group=entries[groupindices[typename]]else:groupindices[typename]=len(entries)group=[typedesc,[]]entries.append(group)entry=typedesc.make_entry(fieldarg,[translatable_content])group[1].append(entry)else:entry=typedesc.make_entry(fieldarg,[translatable_content])entries.append([typedesc,entry])# step 2: all entries are collected, check the parameters list.try:env=other_self.directive.state.document.settings.envexceptAttributeErrorase:logger=logging.getLogger("docassert")logger.warning("[docassert] {0}".format(e))env=Nonedocname=fieldbody.parent.source.split(':docstring')[0]forentryinentries:ifisinstance(entry,nodes.field):# raise NotImplementedError()logger=logging.getLogger("docassert")logger.warning("[docassert] unable to checl [nodes.field] {0}".format(entry))else:fieldtype,content=entryfieldtypes=types.get(fieldtype.name,{})check_typed_make_field(other_self,fieldtypes,other_self.directive.domain,content,env=env,parameters=parameters,function_name=function_name,docname=docname,kind=funckind)returnself.replaced(other_self,node)

[docs]defsetup_docassert(app):""" Setup for ``docassert`` extension (sphinx). This changes ``DocFieldTransformer.transform`` and replaces it by a function which calls the current function and does extra checking on the list of parameters. .. warning:: This class does not handle methods if the parameter name for the class is different from *self*. Classes included in other classes are not properly handled. :githublink:`%|py|270` """inst=OverrideDocFieldTransformer(DocFieldTransformer.transform)deflocal_transform(me,node):"local function"returninst.override_transform(me,node)DocFieldTransformer.transform=local_transformreturn{'version':sphinx.__display_version__,'parallel_read_safe':True}