Coverage for gpkit/tools/docstring.py: 85%

142 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-07 22:13 -0500

1"Docstring-parsing methods" 

2import re 

3import inspect 

4import ast 

5import numpy as np 

6 

7 

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}") 

19 

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 

60 

61 

62class parse_variables: # pylint:disable=invalid-name,too-few-public-methods 

63 """decorator for adding local Variables from a string. 

64 

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: 

73 

74 @parse_variables(__doc__, globals()) 

75 def setup(...): 

76 

77""") 

78 

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__] 

111 

112 

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) 

126 

127 

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}, ") 

132 

133 

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)") 

138 

139 

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 

182 

183 

184PARSETIP = ("Is this line following the format `Name (optional Value) [Units]" 

185 " (Optional Description)` without any whitespace in the Name or" 

186 " Value fields?") 

187 

188 

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"