|
17 | 17 | importcontrol.flatsys
|
18 | 18 |
|
19 | 19 | # List of functions that we can skip testing (special cases)
|
20 |
| -skiplist= [ |
| 20 | +function_skiplist= [ |
21 | 21 | control.ControlPlot.reshape,# needed for legacy interface
|
| 22 | +control.phase_plot,# legacy function |
22 | 23 | ]
|
23 | 24 |
|
| 25 | +# List of keywords that we can skip testing (special cases) |
| 26 | +keyword_skiplist= { |
| 27 | +control.input_output_response: ['method'], |
| 28 | +control.nyquist_plot: ['color'],# checked separately |
| 29 | +control.optimal.solve_ocp: ['method'],# deprecated |
| 30 | +control.sisotool: ['kvect'],# deprecated |
| 31 | +} |
| 32 | + |
| 33 | +# Decide on the level of verbosity (use -rP when running pytest) |
| 34 | +verbose=1 |
| 35 | + |
24 | 36 | @pytest.mark.parametrize("module, prefix", [
|
25 | 37 | (control,""), (control.flatsys,"flatsys."),
|
26 | 38 | (control.optimal,"optimal."), (control.phaseplot,"phaseplot.")
|
27 | 39 | ])
|
28 | 40 | deftest_docstrings(module,prefix):
|
29 | 41 | # Look through every object in the package
|
30 |
| -print(f"Checking module{module}") |
| 42 | +ifverbose>1: |
| 43 | +print(f"Checking module{module}") |
31 | 44 | forname,objininspect.getmembers(module):
|
32 | 45 | # Skip anything that is outside of this module
|
33 |
| -ifinspect.getmodule(obj)isnotNoneand \ |
34 |
| -notinspect.getmodule(obj).__name__.startswith('control'): |
| 46 | +ifinspect.getmodule(obj)isnotNoneand ( |
| 47 | +notinspect.getmodule(obj).__name__.startswith('control') |
| 48 | +orprefix!=""andinspect.getmodule(obj)!=module): |
35 | 49 | # Skip anything that isn't part of the control package
|
36 | 50 | continue
|
37 | 51 |
|
38 | 52 | ifinspect.isclass(obj):
|
39 |
| -print(f" Checking class{name}") |
| 53 | +ifverbose>1: |
| 54 | +print(f" Checking class{name}") |
40 | 55 | # Check member functions within the class
|
41 |
| -test_docstrings(obj,prefix+obj.__name__+'.') |
| 56 | +test_docstrings(obj,prefix+name+'.') |
42 | 57 |
|
43 | 58 | ifinspect.isfunction(obj):
|
44 |
| -# Skip anything that is inheritedorhidden |
45 |
| -ifinspect.isclass(module)andobj.__name__notinmodule.__dict__ \ |
46 |
| -orobj.__name__.startswith('_')orobjinskiplist: |
| 59 | +# Skip anything that is inherited, hidden,ordeprecated |
| 60 | +ifinspect.isclass(module)andnamenotinmodule.__dict__ \ |
| 61 | +orname.startswith('_')orobjinfunction_skiplist: |
47 | 62 | continue
|
48 | 63 |
|
49 |
| -# Make sure there is a docstring |
50 |
| -print(f" Checking function{name}") |
| 64 | +# Get the docstring (skip w/ warning if there isn't one) |
| 65 | +ifverbose>1: |
| 66 | +print(f" Checking function{name}") |
51 | 67 | ifobj.__doc__isNone:
|
52 | 68 | warnings.warn(
|
53 |
| -f"{module.__name__}.{obj.__name__} is missing docstring") |
| 69 | +f"{module.__name__}.{name} is missing docstring") |
| 70 | +continue |
| 71 | +else: |
| 72 | +docstring=inspect.getdoc(obj) |
| 73 | +source=inspect.getsource(obj) |
| 74 | + |
| 75 | +# Skip deprecated functions |
| 76 | +iff"{name} is deprecated"indocstringor \ |
| 77 | +"function is deprecated"indocstringor \ |
| 78 | +".. deprecated::"indocstring: |
| 79 | +ifverbose>1: |
| 80 | +print(" [deprecated]") |
54 | 81 | continue
|
55 |
| - |
| 82 | + |
| 83 | +eliff"{name} is deprecated"insource: |
| 84 | +ifverbose: |
| 85 | +print(f"{name} is deprecated, but not documented") |
| 86 | +warnings.warn(f"{name} deprecated, but not documented") |
| 87 | +continue |
| 88 | + |
56 | 89 | # Get the signature for the function
|
57 | 90 | sig=inspect.signature(obj)
|
58 | 91 |
|
59 | 92 | # Go through each parameter and make sure it is in the docstring
|
60 | 93 | forargname,parinsig.parameters.items():
|
61 |
| -ifargname=='self'orargname[0]=='_': |
| 94 | + |
| 95 | +# Look for arguments that we can skip |
| 96 | +ifargname=='self'orargname[0]=='_'or \ |
| 97 | +objinkeyword_skiplistandargnameinkeyword_skiplist[obj]: |
62 | 98 | continue
|
63 |
| - |
64 |
| -ifpar.kind==inspect.Parameter.VAR_KEYWORD: |
65 |
| -# Found a keyword argument; look at code for parsing |
66 |
| -warnings.warn("keyword argument checks not yet implemented") |
| 99 | + |
| 100 | +# Check for positional arguments |
| 101 | +ifpar.kind==inspect.Parameter.VAR_POSITIONAL: |
| 102 | +# Too complicated to check |
| 103 | +iff"*{argname}"notindocstringandverbose: |
| 104 | +print(f"{name} has positional arguments; " |
| 105 | +"check manually") |
| 106 | +continue |
| 107 | + |
| 108 | +# Check for keyword arguments (then look at code for parsing) |
| 109 | +elifpar.kind==inspect.Parameter.VAR_KEYWORD: |
| 110 | +# See if we documented the keyward argumnt directly |
| 111 | +iff"**{argname}"indocstring: |
| 112 | +continue |
| 113 | + |
| 114 | +# Look for direct kwargs argument access |
| 115 | +kwargnames=set() |
| 116 | +for_,kwargnameinre.findall( |
| 117 | +argname+r"(\[|\.pop\(|\.get\()'([\w]+)'", |
| 118 | +source): |
| 119 | +ifverbose>2: |
| 120 | +print(" Found direct keyword argument", |
| 121 | +kwargname) |
| 122 | +kwargnames.add(kwargname) |
| 123 | + |
| 124 | +# Look for kwargs access via _process_legacy_keyword |
| 125 | +forkwargnameinre.findall( |
| 126 | +r"_process_legacy_keyword\([\s]*"+argname+ |
| 127 | +r",[\s]*'[\w]+',[\s]*'([\w]+)'",source): |
| 128 | +ifverbose>2: |
| 129 | +print(" Found legacy keyword argument", |
| 130 | + {kwargname}) |
| 131 | +kwargnames.add(kwargname) |
| 132 | + |
| 133 | +forkwargnameinkwargnames: |
| 134 | +ifobjinkeyword_skiplistand \ |
| 135 | +kwargnameinkeyword_skiplist[obj]: |
| 136 | +continue |
| 137 | +ifverbose>3: |
| 138 | +print(f" Checking keyword argument{kwargname}") |
| 139 | +assert_check_docstring( |
| 140 | +name,kwargname,inspect.getdoc(obj), |
| 141 | +prefix=prefix) |
67 | 142 |
|
68 | 143 | # Make sure this argument is documented properly in docstring
|
69 | 144 | else:
|
70 |
| -assert_check_docstring(obj.__name__,argname,obj.__doc__) |
| 145 | +ifverbose>3: |
| 146 | +print(f" Checking argument{argname}") |
| 147 | +assert_check_docstring( |
| 148 | +name,argname,docstring,prefix=prefix) |
71 | 149 |
|
72 | 150 |
|
73 | 151 | # Utility function to check for an argument in a docstring
|
74 |
| -def_check_docstring(funcname,argname,docstring): |
75 |
| -ifre.search(f" ([\\w]+, )*{argname}(,[\\w]+)*[^ ]:",docstring): |
| 152 | +def_check_docstring(funcname,argname,docstring,prefix=""): |
| 153 | +funcname=prefix+funcname |
| 154 | +ifre.search( |
| 155 | +"\n"+r"((\w+|\.{3}), )*"+argname+r"(, (\w+|\.{3}))*:", |
| 156 | +docstring): |
76 | 157 | # Found the string, but not in numpydoc form
|
| 158 | +ifverbose: |
| 159 | +print(f"{funcname}:{argname} docstring missing space") |
77 | 160 | warnings.warn(f"{funcname} '{argname}' docstring missing space")
|
78 | 161 | returnTrue
|
79 |
| - |
80 |
| -elifnotre.search(f" ([\\w]+, )*{argname}(,[\\w]+)* :",docstring): |
| 162 | + |
| 163 | +elifnotre.search( |
| 164 | +"\n"+r"((\w+|\.{3}), )*"+argname+r"(, (\w+|\.{3}))* :", |
| 165 | +docstring): |
81 | 166 | # return False
|
82 | 167 | #
|
83 | 168 | # Just issue a warning for now
|
| 169 | +ifverbose: |
| 170 | +print(f"{funcname}:{argname} not documented") |
84 | 171 | warnings.warn(f"{funcname} '{argname}' not documented")
|
85 | 172 | returnTrue
|
86 |
| -
|
| 173 | + |
87 | 174 | returnTrue
|