Coverage for gpkit/tools/docstring.py: 85%
142 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-23 21:28 -0400
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-23 21:28 -0400
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
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("multiple instances of %s" % 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:
48 raise AttributeError("`%s` is noted in %s as "
49 "unbounded, but is not "
50 "an attribute of that model."
51 % (var, instance.__class__.__name__))
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
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 for line, index in zip(lines.split("\n"), indexs):
123 out[index] = line + " # from '%s'" % out[index][1:].strip()
124 return "\n".join(out)
127def vv_declare(string, flag, idx2, countstr):
128 "Turns Variable declarations into VectorVariable ones"
129 length = string[len(flag):idx2].strip()
130 return countstr.replace("Variable(", "VectorVariable(%s, " % length)
133# pylint: disable=unused-argument
134def constant_declare(string, flag, idx2, countstr):
135 "Turns Variable declarations into Constant ones"
136 return countstr.replace("')", "', constant=True)")
139# pylint: disable=too-many-locals
140def check_and_parse_flag(string, flag, declaration_func=None):
141 "Checks for instances of flag in string and parses them."
142 overallstr = ""
143 originalstr = string
144 lineidxs = []
145 for _ in range(string.count(flag)):
146 countstr = ""
147 idx = string.index(flag)
148 string = string[idx:]
149 if idx == -1:
150 idx = 0
151 skiplines = 0
152 else:
153 skiplines = 2
154 idx2 = 0 if "\n" in flag else string.index("\n")
155 for line in string[idx2:].split("\n")[skiplines:]:
156 if not line.strip(): # whitespace only
157 break
158 try:
159 units = line[line.index("[")+1:line.index("]")]
160 except ValueError:
161 raise ValueError("A unit declaration bracketed by [] was"
162 " not found on the line reading:\n"
163 " %s" % line)
164 nameval = line[:line.index("[")].split()
165 labelstart = line.index("]") + 1
166 if labelstart >= len(line):
167 label = ""
168 else:
169 while line[labelstart] == " ":
170 labelstart += 1
171 label = line[labelstart:].replace("'", "\\'")
172 countstr += variable_declaration(nameval, units, label, line)
173 # note that this is a 0-based line indexing
174 lineidxs.append(originalstr[:originalstr.index(line)].count("\n"))
175 if declaration_func is None:
176 overallstr += countstr
177 else:
178 overallstr += declaration_func(string, flag, idx2, countstr)
179 string = string[idx2+len(flag):]
180 return overallstr, lineidxs
183PARSETIP = ("Is this line following the format `Name (optional Value) [Units]"
184 " (Optional Description)` without any whitespace in the Name or"
185 " Value fields?")
188def variable_declaration(nameval, units, label, line, errorcatch=True):
189 "Turns parsed output into a Variable declaration"
190 if len(nameval) > 2:
191 raise ValueError("while parsing the line '%s', additional fields"
192 " (separated by whitespace) were found between Value"
193 " '%s' and the Units `%s`. %s"
194 % (line, nameval[1], 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"