Coverage for gpkit/tools/docstring.py: 85%
142 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-05 22:33 -0500
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-05 22:33 -0500
1"Docstring-parsing methods"
2import re
3import inspect
4import ast
5import numpy as np
8def expected_unbounded(instance, doc):
9 "Gets expected-unbounded variables from a string"
10 # pylint: disable=too-many-locals,too-many-nested-blocks,too-many-branches
11 exp_unbounded = set()
12 for direction in ["upper", "lower"]:
13 flag = direction[0].upper()+direction[1:]+" Unbounded\n"
14 count = doc.count(flag)
15 if count == 0:
16 continue
17 if count > 1:
18 raise ValueError(f"multiple instances of {flag}")
20 idx = doc.index(flag) + len(flag)
21 idx2 = doc[idx:].index("\n")
22 try:
23 idx3 = doc[idx:][idx2+1:].index("\n\n")
24 except ValueError:
25 idx3 = doc[idx:][idx2+1:].index("\n")
26 varstrs = doc[idx:][idx2+1:][:idx3].strip()
27 varstrs = varstrs.replace("\n", ", ") # cross newlines
28 varstrs = re.sub(" +", " ", varstrs) # multiple-whitespace removal
29 if varstrs:
30 for var in varstrs.split(", "):
31 if " (if " in var: # it's a conditional!
32 var, condition = var.split(" (if ")
33 assert condition[-1] == ")"
34 condition = condition[:-1]
35 invert = condition[:4] == "not "
36 if invert:
37 condition = condition[4:]
38 if getattr(instance, condition):
39 continue
40 elif not getattr(instance, condition):
41 continue
42 try:
43 obj = instance
44 for subdot in var.split("."):
45 obj = getattr(obj, subdot)
46 variables = obj
47 except AttributeError as err:
48 raise AttributeError(
49 f"`{var}` is noted in {instance.__class__.__name__} as"
50 " unbounded, but is not an attribute of that model."
51 ) from err
52 if not hasattr(variables, "shape"):
53 variables = np.array([variables])
54 it = np.nditer(variables, flags=['multi_index', 'refs_ok'])
55 while not it.finished:
56 i = it.multi_index
57 it.iternext()
58 exp_unbounded.add((variables[i].key, direction))
59 return exp_unbounded
62class parse_variables: # pylint:disable=invalid-name,too-few-public-methods
63 """decorator for adding local Variables from a string.
65 Generally called as `@parse_variables(__doc__, globals())`.
66 """
67 def __init__(self, string, scopevars=None):
68 self.string = string
69 self.scopevars = scopevars
70 if scopevars is None:
71 raise DeprecationWarning("""
72parse_variables is no longer used directly with exec, but as a decorator:
74 @parse_variables(__doc__, globals())
75 def setup(...):
77""")
79 def __call__(self, function): # pylint:disable=too-many-locals
80 orig_lines, lineno = inspect.getsourcelines(function)
81 indent_length = 0
82 while orig_lines[1][indent_length] in [" ", "\t"]:
83 indent_length += 1
84 first_indent_length = indent_length
85 setup_lines = 1
86 while "):" not in orig_lines[setup_lines]:
87 setup_lines += 1
88 next_indented_idx = setup_lines + 1
89 # get the next indented line
90 while len(orig_lines[next_indented_idx]) <= indent_length + 1:
91 next_indented_idx += 1
92 while orig_lines[next_indented_idx][indent_length] in [" ", "\t"]:
93 indent_length += 1
94 second_indent = orig_lines[next_indented_idx][:indent_length]
95 parse_lines = [second_indent + line + "\n"
96 for line in parse_varstring(self.string).split("\n")]
97 parse_lines += [second_indent + '# (@parse_variables spacer line)\n']
98 parse_lines += [second_indent + '# (setup spacer line)\n']*setup_lines
99 # make ast of these new lines, insert it into the original ast
100 new_lines = (orig_lines[1:setup_lines+1] + parse_lines
101 + orig_lines[setup_lines+1:])
102 new_src = "\n".join([l[first_indent_length:-1] for l in new_lines
103 if "#" not in l[:first_indent_length]])
104 new_ast = ast.parse(new_src, "<parse_variables>")
105 ast.increment_lineno(new_ast, n=lineno-len(parse_lines))
106 code = compile(new_ast, inspect.getsourcefile(function), "exec",
107 dont_inherit=True) # don't inherit __future__ from here
108 out = {}
109 exec(code, self.scopevars, out) # pylint: disable=exec-used
110 return out[function.__name__]
113def parse_varstring(string):
114 "Parses a string to determine what variables to create from it"
115 consts = check_and_parse_flag(string, "Constants\n", constant_declare)
116 variables = check_and_parse_flag(string, "Variables\n")
117 vecvars = check_and_parse_flag(string, "Variables of length", vv_declare)
118 out = ["# " + line for line in string.split("\n")]
119 # imports, to be updated if more things are parsed above
120 out[0] = "from gpkit import Variable, VectorVariable" + " " + out[0]
121 for lines, indexs in (consts, variables, vecvars):
122 # pylint: disable=no-member # for lines.split()
123 for line, index in zip(lines.split("\n"), indexs):
124 out[index] = line + f" # from '{out[index][1:].strip()}'"
125 return "\n".join(out)
128def vv_declare(string, flag, idx2, countstr):
129 "Turns Variable declarations into VectorVariable ones"
130 length = string[len(flag):idx2].strip()
131 return countstr.replace("Variable(", f"VectorVariable({length}, ")
134# pylint: disable=unused-argument
135def constant_declare(string, flag, idx2, countstr):
136 "Turns Variable declarations into Constant ones"
137 return countstr.replace("')", "', constant=True)")
140# pylint: disable=too-many-locals
141def check_and_parse_flag(string, flag, declaration_func=None):
142 "Checks for instances of flag in string and parses them."
143 overallstr = ""
144 originalstr = string
145 lineidxs = []
146 for _ in range(string.count(flag)):
147 countstr = ""
148 idx = string.index(flag)
149 string = string[idx:]
150 if idx == -1:
151 idx = 0
152 skiplines = 0
153 else:
154 skiplines = 2
155 idx2 = 0 if "\n" in flag else string.index("\n")
156 for line in string[idx2:].split("\n")[skiplines:]:
157 if not line.strip(): # whitespace only
158 break
159 try:
160 units = line[line.index("[")+1:line.index("]")]
161 except ValueError as err:
162 raise ValueError("A unit declaration bracketed by [] was"
163 " not found on the line reading:\n"
164 f" {line}") from err
165 nameval = line[:line.index("[")].split()
166 labelstart = line.index("]") + 1
167 if labelstart >= len(line):
168 label = ""
169 else:
170 while line[labelstart] == " ":
171 labelstart += 1
172 label = line[labelstart:].replace("'", "\\'")
173 countstr += variable_declaration(nameval, units, label, line)
174 # note that this is a 0-based line indexing
175 lineidxs.append(originalstr[:originalstr.index(line)].count("\n"))
176 if declaration_func is None:
177 overallstr += countstr
178 else:
179 overallstr += declaration_func(string, flag, idx2, countstr)
180 string = string[idx2+len(flag):]
181 return overallstr, lineidxs
184PARSETIP = ("Is this line following the format `Name (optional Value) [Units]"
185 " (Optional Description)` without any whitespace in the Name or"
186 " Value fields?")
189def variable_declaration(nameval, units, label, line, errorcatch=True):
190 "Turns parsed output into a Variable declaration"
191 if len(nameval) > 2:
192 raise ValueError(f"while parsing the line '{line}', additional fields"
193 " (separated by whitespace) were found between Value"
194 f" '{nameval[1]}' and the Units `{units}`. {PARSETIP}")
195 if len(nameval) == 2:
196 out = "{0} = self.{0} = Variable('{0}', {1}, '{2}', '{3}')"
197 out = out.format(nameval[0], nameval[1], units, label)
198 elif len(nameval) == 1:
199 out = "{0} = self.{0} = Variable('{0}', '{1}', '{2}')"
200 out = out.format(nameval[0], units, label)
201 return out + "\n"