Coverage for larch/symboltable.py: 72%

314 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2024-10-16 21:04 +0000

1#!/usr/bin/env python 

2''' 

3SymbolTable for Larch interpreter 

4''' 

5import copy 

6import numpy 

7from lmfit.printfuncs import gformat 

8from . import site_config 

9from .utils import fixName, isValidName 

10 

11def repr_value(val): 

12 """render a repr-like value for ndarrays, lists, etc""" 

13 if (isinstance(val, numpy.ndarray) and 

14 (len(val) > 6 or len(val.shape)>1)): 

15 sval = f"shape={val.shape}, type={val.dtype} range=[{gformat(val.min())}:{gformat(val.max())}]" 

16 elif isinstance(val, list) and len(val) > 6: 

17 sval = f"length={len(val)}: [{val[0]}, {val[1]}, ... {val[-2]}, {val[-1]}]" 

18 elif isinstance(val, tuple) and len(val) > 6: 

19 sval = f"length={len(val)}: ({val[0]}, {val[1]}, ... {val[-2]}, {val[-1]})" 

20 else: 

21 try: 

22 sval = repr(val) 

23 except: 

24 sval = val 

25 return sval 

26 

27 

28class Group(): 

29 """ 

30 Generic Group: a container for variables, modules, and subgroups. 

31 """ 

32 __private = ('_main', '_larch', '_parents', '__name__', '__doc__', 

33 '__private', '_subgroups', '_members', '_repr_html_') 

34 

35 __generic_functions = ('keys', 'values', 'items') 

36 

37 def __init__(self, name=None, **kws): 

38 if name is None: 

39 name = hex(id(self)) 

40 self.__name__ = name 

41 for key, val in kws.items(): 

42 setattr(self, key, val) 

43 

44 def __len__(self): 

45 return len(dir(self)) 

46 

47 def __repr__(self): 

48 if self.__name__ is not None: 

49 return f'<Group {self.__name__}>' 

50 return '<Group>' 

51 

52 def __copy__(self): 

53 out = Group() 

54 for key, val in self.__dict__.items(): 

55 if key != '__name__': 

56 setattr(out, key, copy.copy(val)) 

57 return out 

58 

59 def __deepcopy__(self, memo): 

60 out = Group() 

61 for key, val in self.__dict__.items(): 

62 if key != '__name__': 

63 setattr(out, key, copy.deepcopy(val, memo)) 

64 return out 

65 

66 def __id__(self): 

67 return id(self) 

68 

69 def __dir__(self): 

70 "return list of member names" 

71 cls_members = [] 

72 cname = self.__class__.__name__ 

73 if cname != 'SymbolTable' and hasattr(self, '__class__'): 

74 cls_members = dir(self.__class__) 

75 

76 dict_keys = [key for key in self.__dict__ if key not in cls_members] 

77 

78 return [key for key in cls_members + dict_keys 

79 if (not key.startswith('_SymbolTable_') and 

80 not key.startswith('_Group_') and 

81 not key.startswith(f'_{cname}_') and 

82 not (key.startswith('__') and key.endswith('__')) and 

83 key not in self.__generic_functions and 

84 key not in self.__private)] 

85 

86 def __getitem__(self, key): 

87 

88 if isinstance(key, int): 

89 raise IndexError("Group does not support Integer indexing") 

90 

91 return getattr(self, key) 

92 

93 def __setitem__(self, key, value): 

94 

95 if isinstance(key, int): 

96 raise IndexError("Group does not support Integer indexing") 

97 

98 return setattr(self, key, value) 

99 

100 def __iter__(self): 

101 return iter(self.keys()) 

102 

103 def keys(self): 

104 return self.__dir__() 

105 

106 def values(self): 

107 return [getattr(self, key) for key in self.__dir__()] 

108 

109 def items(self): 

110 return [(key, getattr(self, key)) for key in self.__dir__()] 

111 

112 def _subgroups(self): 

113 "return list of names of members that are sub groups" 

114 return [k for k in self._members() if isgroup(self.__dict__[k])] 

115 

116 def _members(self): 

117 "return members" 

118 out = {} 

119 for key in self.__dir__(): 

120 if key in self.__dict__: 

121 out[key] = self.__dict__[key] 

122 return out 

123 

124 def _repr_html_(self): 

125 """HTML representation for Jupyter notebook""" 

126 html = [f"Group {self.__name__}", "<table>", 

127 "<tr><td><b>Attribute</b></td><td><b>Type</b></td>", 

128 "<td><b>Value</b></td></tr>"] 

129 attrs = self.__dir__() 

130 for attr in self.__dir__(): 

131 obj = getattr(self, attr) 

132 atype = type(obj).__name__ 

133 sval = repr_value(obj) 

134 html.append(f"<tr><td>{attr}</td><td><i>{atype}</i></td><td>{sval}</td></tr>") 

135 html.append("</table>") 

136 return '\n'.join(html) 

137 

138 

139def isgroup(grp, *args): 

140 """tests if input is a Group 

141 

142 With additional arguments (all must be strings), it also tests 

143 that the group has an an attribute named for each argument. This 

144 can be used to test not only if a object is a Group, but whether 

145 it a group with expected arguments. 

146 """ 

147 ret = isinstance(grp, Group) 

148 if ret and len(args) > 0: 

149 try: 

150 ret = all([hasattr(grp, a) for a in args]) 

151 except TypeError: 

152 return False 

153 return ret 

154 

155 

156class InvalidName: 

157 """ used to create a value that will NEVER be a useful symbol. 

158 symboltable._lookup() uses this to check for invalid names""" 

159 

160 

161GroupDocs = {} 

162GroupDocs['_sys'] = """ 

163Larch system-wide status variables, including 

164configuration variables and lists of Groups used 

165for finding variables. 

166""" 

167 

168GroupDocs['_builtin'] = """ 

169core built-in functions, most taken from Python 

170""" 

171 

172GroupDocs['_math'] = """ 

173Mathematical functions, including a host of functtion from numpy and scipy 

174""" 

175 

176 

177class SymbolTable(Group): 

178 """Main Symbol Table for Larch. 

179 """ 

180 top_group = '_main' 

181 core_groups = ('_sys', '_builtin', '_math') 

182 __invalid_name = InvalidName() 

183 _private = ('save_frame', 'restore_frame', 'set_frame', 

184 'has_symbol', 'has_group', 'get_group', 

185 'create_group', 'new_group', 'isgroup', 

186 'get_symbol', 'set_symbol', 'del_symbol', 

187 'get_parent', '_path', '__parents') 

188 

189 def __init__(self, larch=None): 

190 Group.__init__(self, name=self.top_group) 

191 self._larch = larch 

192 self._sys = None 

193 setattr(self, self.top_group, self) 

194 

195 for gname in self.core_groups: 

196 thisgroup = Group(name=gname) 

197 if gname in GroupDocs: 

198 thisgroup.__doc__ = GroupDocs[gname] 

199 setattr(self, gname, thisgroup) 

200 

201 self._sys.frames = [] 

202 self._sys.searchGroups = [self.top_group] 

203 self._sys.path = ['.'] 

204 self._sys.localGroup = self 

205 self._sys.valid_commands = [] 

206 self._sys.moduleGroup = self 

207 self._sys.__cache__ = [None]*4 

208 self._sys.saverestore_groups = [] 

209 for grp in self.core_groups: 

210 self._sys.searchGroups.append(grp) 

211 self._sys.core_groups = tuple(self._sys.searchGroups[:]) 

212 

213 self._sys.modules = {'_main':self} 

214 for gname in self.core_groups: 

215 self._sys.modules[gname] = getattr(self, gname) 

216 self._fix_searchGroups() 

217 

218 self._sys.config = Group(home_dir = site_config.home_dir, 

219 history_file= site_config.history_file, 

220 init_files = site_config.init_files, 

221 user_larchdir= site_config.user_larchdir, 

222 larch_version= site_config.larch_version, 

223 release_version = site_config.larch_release_version) 

224 

225 def save_frame(self): 

226 " save current local/module group" 

227 self._sys.frames.append((self._sys.localGroup, self._sys.moduleGroup)) 

228 

229 def restore_frame(self): 

230 "restore last saved local/module group" 

231 try: 

232 lgrp, mgrp = self._sys.frames.pop() 

233 self._sys.localGroup = lgrp 

234 self._sys.moduleGroup = mgrp 

235 self._fix_searchGroups() 

236 except: 

237 pass 

238 

239 def set_frame(self, groups): 

240 "set current execution frame (localGroup, moduleGroup)" 

241 self._sys.localGroup, self._sys.moduleGroup = groups 

242 self._fix_searchGroups() 

243 

244 

245 def _fix_searchGroups(self, force=False): 

246 """resolve list of groups to search for symbol names: 

247 

248 The variable self._sys.searchGroups holds the list of group 

249 names for searching for symbol names. A user can set this 

250 dynamically. The names need to be absolute (that is, relative to 

251 _main, and can omit the _main prefix). 

252 

253 This calclutes and returns self._sys.searchGroupObjects, 

254 which is the list of actual group objects (not names) resolved from 

255 the list of names in _sys.searchGroups) 

256 

257 _sys.localGroup,_sys.moduleGroup come first in the search list, 

258 followed by any search path associated with that module (from 

259 imports for that module) 

260 """ 

261 ## 

262 # check (and cache) whether searchGroups needs to be changed. 

263 sys = self._sys 

264 cache = sys.__cache__ 

265 if len(cache) < 4: 

266 cache = [None]*4 

267 if (sys.localGroup == cache[0] and 

268 sys.moduleGroup == cache[1] and 

269 sys.searchGroups == cache[2] and 

270 cache[3] is not None and not force): 

271 return cache[3] 

272 

273 if sys.moduleGroup is None: 

274 sys.moduleGroup = self.top_group 

275 if sys.localGroup is None: 

276 sys.localGroup = sys.moduleGroup 

277 

278 cache[0] = sys.localGroup 

279 cache[1] = sys.moduleGroup 

280 snames = [] 

281 sgroups = [] 

282 for grp in (sys.localGroup, sys.moduleGroup): 

283 if grp is not None and grp not in sgroups: 

284 sgroups.append(grp) 

285 snames.append(grp.__name__) 

286 

287 sysmods = list(self._sys.modules.values()) 

288 searchGroups = sys.searchGroups[:] 

289 searchGroups.extend(self._sys.core_groups) 

290 for name in searchGroups: 

291 grp = None 

292 if name in self._sys.modules: 

293 grp = self._sys.modules[name] 

294 elif hasattr(self, name): 

295 gtest = getattr(self, name) 

296 if isinstance(gtest, Group): 

297 grp = gtest 

298 elif '.' in name: 

299 parent, child= name.split('.') 

300 for sgrp in sysmods: 

301 if (parent == sgrp.__name__ and 

302 hasattr(sgrp, child)): 

303 grp = getattr(sgrp, child) 

304 break 

305 else: 

306 for sgrp in sysmods: 

307 if hasattr(sgrp, name): 

308 grp = getattr(sgrp, name) 

309 break 

310 if grp is not None and grp not in sgroups: 

311 sgroups.append(grp) 

312 snames.append(name) 

313 

314 self._sys.searchGroups = cache[2] = snames[:] 

315 sys.searchGroupObjects = cache[3] = sgroups[:] 

316 return sys.searchGroupObjects 

317 

318 def get_parentpath(self, sym): 

319 """ get parent path for a symbol""" 

320 obj = self._lookup(sym) 

321 if obj is None: 

322 return 

323 out = [] 

324 for s in reversed(self.__parents): 

325 if s.__name__ != '_main' or '_main' not in out: 

326 out.append(s.__name__) 

327 out.reverse() 

328 return '.'.join(out) 

329 

330 def _lookup(self, name=None, create=False): 

331 """looks up symbol in search path 

332 returns symbol given symbol name, 

333 creating symbol if needed (and create=True)""" 

334 debug = False # not ('force'in name) 

335 if debug: 

336 print( '====\nLOOKUP ', name) 

337 searchGroups = self._fix_searchGroups() 

338 self.__parents = [] 

339 if self not in searchGroups: 

340 searchGroups.append(self) 

341 

342 def public_attr(grp, name): 

343 return (hasattr(grp, name) and 

344 not (grp is self and name in self._private)) 

345 

346 parts = name.split('.') 

347 if len(parts) == 1: 

348 for grp in searchGroups: 

349 if public_attr(grp, name): 

350 self.__parents.append(grp) 

351 return getattr(grp, name) 

352 

353 # more complex case: not immediately found in Local or Module Group 

354 parts.reverse() 

355 top = parts.pop() 

356 out = self.__invalid_name 

357 if top == self.top_group: 

358 out = self 

359 else: 

360 for grp in searchGroups: 

361 if public_attr(grp, top): 

362 self.__parents.append(grp) 

363 out = getattr(grp, top) 

364 if out is self.__invalid_name: 

365 raise NameError(f"'{name}' is not defined") 

366 

367 if len(parts) == 0: 

368 return out 

369 

370 while parts: 

371 prt = parts.pop() 

372 if hasattr(out, prt): 

373 out = getattr(out, prt) 

374 elif create: 

375 val = None 

376 if len(parts) > 0: 

377 val = Group(name=prt) 

378 setattr(out, prt, val) 

379 out = getattr(out, prt) 

380 else: 

381 raise LookupError( 

382 f"cannot locate member '{prt}' of '{out}'") 

383 return out 

384 

385 def has_symbol(self, symname): 

386 try: 

387 _ = self.get_symbol(symname) 

388 return True 

389 except (LookupError, NameError, ValueError): 

390 return False 

391 

392 def has_group(self, gname): 

393 try: 

394 _ = self.get_group(gname) 

395 return True 

396 except (NameError, LookupError): 

397 return False 

398 

399 def isgroup(self, sym): 

400 "test if symbol is a group" 

401 return isgroup(sym) 

402 

403 def get_group(self, gname): 

404 "find group by name" 

405 sym = self._lookup(gname, create=False) 

406 if isgroup(sym): 

407 return sym 

408 raise LookupError(f"symbol '{gname}' found, but not a group") 

409 

410 def create_group(self, **kw): 

411 "create a new Group, not placed anywhere in symbol table" 

412 return Group(**kw) 

413 

414 def new_group(self, name, **kws): 

415 name = fixName(name) 

416 grp = Group(__name__ = name, **kws) 

417 self.set_symbol(name, value=grp) 

418 return grp 

419 

420 def get_symbol(self, sym, create=False): 

421 "lookup and return a symbol by name" 

422 return self._lookup(sym, create=create) 

423 

424 def set_symbol(self, name, value=None, group=None): 

425 "set a symbol in the table" 

426 grp = self._sys.localGroup 

427 if group is not None: 

428 grp = self.get_group(group) 

429 names = [] 

430 

431 for n in name.split('.'): 

432 if not isValidName(n): 

433 raise SyntaxError(f"invalid symbol name '{n}'") 

434 names.append(n) 

435 

436 child = names.pop() 

437 for nam in names: 

438 if hasattr(grp, nam): 

439 grp = getattr(grp, nam) 

440 if not isgroup(grp): 

441 raise ValueError( 

442 f"cannot create subgroup of non-group '{grp}'") 

443 else: 

444 setattr(grp, nam, Group()) 

445 

446 setattr(grp, child, value) 

447 return value 

448 

449 def del_symbol(self, name): 

450 "delete a symbol" 

451 sym = self._lookup(name, create=False) 

452 parent, child = self.get_parent(name) 

453 delattr(parent, child) 

454 

455 def get_parent(self, name): 

456 """return parent group, child name for an absolute symbol name 

457 (as from _lookup) that is, a pair suitable for hasattr, 

458 getattr, or delattr 

459 """ 

460 tnam = name.split('.') 

461 if len(tnam) < 1 or name == self.top_group: 

462 return (self, None) 

463 child = tnam.pop() 

464 sym = self 

465 if len(tnam) > 0: 

466 sym = self._lookup('.'.join(tnam)) 

467 return sym, child 

468 

469 def show_group(self, groupname): 

470 """display group members --- simple version for tests""" 

471 out = [] 

472 try: 

473 group = self.get_group(groupname) 

474 except (NameError, LookupError): 

475 return 'Group %s not found' % groupname 

476 

477 members = dir(group) 

478 out = ['f== {group.__name__}: {len(members)} symbols =='] 

479 for item in members: 

480 obj = getattr(group, item) 

481 dval = repr_value(obj) 

482 out.append(f' {item}: {dval}') 

483 out.append('\n') 

484 self._larch.writer.write('\n'.join(out))