3636 control .tf , # tested separately below
3737]
3838
39- # Checksums to use for checking whether a docstring has changed
40- function_docstring_hash = {
41- control .append : 'fad0bd0766754c3524e0ea27b06bf74a' ,
42- control .describing_function_plot : '95f894706b1d3eeb3b854934596af09f' ,
43- control .dlqe : 'e1e9479310e4e5a6f50f5459fb3d2dfb' ,
44- control .dlqr : '56d7f3a452bc8d7a7256a52d9d1dcb37' ,
45- control .lqe : '0447235d11b685b9dfaf485dd01fdb9a' ,
46- control .lqr : 'a3e0a85f781fc9c0f69a4b7da4f0bd22' ,
47- control .margin : 'f02b3034f5f1d44ce26f916cc3e51600' ,
48- control .parallel : 'bfc470aef75dbb923f9c6fb8bf3c9b43' ,
49- control .series : '9aede1459667738f05cf4fc46603a4f6' ,
50- control .ss2tf : 'e779b8d70205bc1218cc2a4556a66e4b' ,
51- control .tf2ss : '086a3692659b7321c2af126f79f4bc11' ,
52- control .markov : 'a4199c54cb50f07c0163d3790739eafe' ,
53- control .gangof4 : 'f9673ae4c6d26c202060ed4b9ef54800' ,
54- }
55-
5639# List of keywords that we can skip testing (special cases)
5740keyword_skiplist = {
5841 control .input_output_response : ['method' ],
@@ -171,6 +154,9 @@ def test_parameter_docs(module, prefix):
171154 # Get the signature for the function
172155 sig = inspect .signature (obj )
173156
157+ # If first argument is *args, try to use docstring instead
158+ sig = _replace_var_positional_with_docstring (sig , doc )
159+
174160 # Go through each parameter and make sure it is in the docstring
175161 for argname , par in sig .parameters .items ():
176162 # Look for arguments that we can skip
@@ -180,22 +166,11 @@ def test_parameter_docs(module, prefix):
180166
181167 # Check for positional arguments (*arg)
182168 if par .kind == inspect .Parameter .VAR_POSITIONAL :
183- if obj in function_docstring_hash :
184- import hashlib
185- hash = hashlib .md5 (
186- (docstring + source ).encode ('utf-8' )).hexdigest ()
187- if function_docstring_hash [obj ] != hash :
188- _fail (
189- f"source/docstring for { objname } () modified; "
190- f"recheck docstring and update hash to "
191- f"{ hash = } " )
192- continue
193-
194- # Too complicated to check
195169 if f"*{ argname } " not in docstring :
196- _warn (
197- f"{ objname } { argname } has positional arguments; "
198- "docstring not checked" )
170+ _fail (
171+ f"{ objname } has undocumented, unbound positional "
172+ f"argument '{ argname } '; "
173+ "use docstring signature instead" )
199174 continue
200175
201176 # Check for keyword arguments (then look at code for parsing)
@@ -621,7 +596,7 @@ def _check_numpydoc_style(obj, doc):
621596 name = "." .join ([obj .__module__ .removeprefix ("control." ), obj .__name__ ])
622597
623598 # Standard checks for all objects
624- summary = "" .join (doc ["Summary" ])
599+ summary = "\n " .join (doc ["Summary" ])
625600 if len (doc ["Summary" ]) > 1 :
626601 _warn (f"{ name } summary is more than one line" )
627602 if summary and summary [- 1 ] != '.' and re .match (":$" , summary ) is None :
@@ -666,6 +641,55 @@ def _check_param(param, empty_ok=False, noname_ok=False):
666641 raise TypeError ("unknown object type for {obj}" )
667642
668643
644+ # Utility function to replace positional signature with docstring signature
645+ def _replace_var_positional_with_docstring (sig , doc ):
646+ # If no documentation is available, there is nothing we can do...
647+ if doc is None :
648+ return sig
649+
650+ # Check to see if the first argument is positional
651+ parameter_items = iter (sig .parameters .items ())
652+ try :
653+ argname , par = next (parameter_items )
654+ if par .kind != inspect .Parameter .VAR_POSITIONAL or \
655+ (signature := doc ["Signature" ]) == '' :
656+ return sig
657+ except StopIteration :
658+ return sig
659+
660+ # Try parsing the docstring signature
661+ arg_list = []
662+ while (1 ):
663+ if (match_fcn := re .match (
664+ r"^([\s]*\|[\s]*)*[\w]+\(" , signature )) is None :
665+ break
666+ arg_idx = match_fcn .span (0 )[1 ]
667+ while (1 ):
668+ match_arg = re .match (
669+ r"[\s]*([\w]+)(,|,\[|\[,|\)|\]\))(,[\s]*|[\s]*[.]{3},[\s]*)*" ,
670+ signature [arg_idx :])
671+ if match_arg is None :
672+ break
673+ else :
674+ arg_idx += match_arg .span (0 )[1 ]
675+ arg_list .append (match_arg .group (1 ))
676+ signature = signature [arg_idx :]
677+ if arg_list == []:
678+ return sig
679+
680+ # Create the new parameter list
681+ parameter_list = [
682+ inspect .Parameter (arg , inspect .Parameter .POSITIONAL_ONLY )
683+ for arg in arg_list ]
684+
685+ # Add any remaining parameters that were in the original signature
686+ for argname , par in parameter_items :
687+ if argname not in arg_list :
688+ parameter_list .append (par )
689+
690+ # Return the new signature
691+ return sig .replace (parameters = parameter_list )
692+
669693# Utility function to warn with verbose output
670694def _info (str , level ):
671695 if verbose > level :
@@ -693,17 +717,17 @@ def simple_function(arg1, arg2, opt1=None, **kwargs):
693717
694718Failed = pytest .fail .Exception
695719
696- doc_header = simple_class .simple_function .__doc__
720+ doc_header = simple_class .simple_function .__doc__ + " \n "
697721doc_parameters = "\n Parameters\n ----------\n "
698- doc_arg1 = "\n arg1 : int\n Argument 1.\n "
699- doc_arg2 = "\n arg2 : int\n Argument 2.\n "
700- doc_arg2_nospace = "\n arg2 : int\n Argument 2.\n "
701- doc_arg3 = "\n arg3 : int\n Non -existent argument 1.\n "
702- doc_opt1 = "\n opt1 : int\n Keyword argument 1.\n "
703- doc_test = "\n test : int\n Internal keyword argument 1.\n "
722+ doc_arg1 = "arg1 : int\n Argument 1.\n "
723+ doc_arg2 = "arg2 : int\n Argument 2.\n "
724+ doc_arg2_nospace = "arg2 : int\n Argument 2.\n "
725+ doc_arg3 = "arg3 : int\n Non -existent argument 1.\n "
726+ doc_opt1 = "opt1 : int\n Keyword argument 1.\n "
727+ doc_test = "test : int\n Internal keyword argument 1.\n "
704728doc_returns = "\n Returns\n -------\n "
705- doc_ret = "\n out : int\n "
706- doc_ret_nospace = "\n out : int\n "
729+ doc_ret = "out : int\n "
730+ doc_ret_nospace = "out : int\n "
707731
708732@pytest .mark .parametrize ("docstring, exception, match" , [
709733 (None , UserWarning , "missing docstring" ),
@@ -721,8 +745,8 @@ def simple_function(arg1, arg2, opt1=None, **kwargs):
721745 doc_returns + doc_ret , Failed , "'test' not documented" ),
722746 (doc_header + doc_parameters + doc_arg1 + doc_arg2_nospace + doc_opt1 +
723747 doc_test + doc_returns + doc_ret_nospace , UserWarning , "missing space" ),
724- (doc_header + doc_arg1 + doc_arg2_nospace + doc_opt1 + doc_test +
725- doc_returns + doc_ret_nospace , Failed , "missing Parameters section" ),
748+ (doc_header + doc_returns + doc_ret_nospace ,
749+ Failed , "missing Parameters section" ),
726750 (doc_header , None , "" ),
727751 (doc_header + "\n .. deprecated::" , None , "" ),
728752 (doc_header + "\n \n simple_function() is deprecated" ,
0 commit comments