Coverage for larch/symboltable.py: 72%
314 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-10-16 21:04 +0000
« 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
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
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_')
35 __generic_functions = ('keys', 'values', 'items')
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)
44 def __len__(self):
45 return len(dir(self))
47 def __repr__(self):
48 if self.__name__ is not None:
49 return f'<Group {self.__name__}>'
50 return '<Group>'
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
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
66 def __id__(self):
67 return id(self)
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__)
76 dict_keys = [key for key in self.__dict__ if key not in cls_members]
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)]
86 def __getitem__(self, key):
88 if isinstance(key, int):
89 raise IndexError("Group does not support Integer indexing")
91 return getattr(self, key)
93 def __setitem__(self, key, value):
95 if isinstance(key, int):
96 raise IndexError("Group does not support Integer indexing")
98 return setattr(self, key, value)
100 def __iter__(self):
101 return iter(self.keys())
103 def keys(self):
104 return self.__dir__()
106 def values(self):
107 return [getattr(self, key) for key in self.__dir__()]
109 def items(self):
110 return [(key, getattr(self, key)) for key in self.__dir__()]
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])]
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
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)
139def isgroup(grp, *args):
140 """tests if input is a Group
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
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"""
161GroupDocs = {}
162GroupDocs['_sys'] = """
163Larch system-wide status variables, including
164configuration variables and lists of Groups used
165for finding variables.
166"""
168GroupDocs['_builtin'] = """
169core built-in functions, most taken from Python
170"""
172GroupDocs['_math'] = """
173Mathematical functions, including a host of functtion from numpy and scipy
174"""
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')
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)
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)
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[:])
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()
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)
225 def save_frame(self):
226 " save current local/module group"
227 self._sys.frames.append((self._sys.localGroup, self._sys.moduleGroup))
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
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()
245 def _fix_searchGroups(self, force=False):
246 """resolve list of groups to search for symbol names:
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).
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)
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]
273 if sys.moduleGroup is None:
274 sys.moduleGroup = self.top_group
275 if sys.localGroup is None:
276 sys.localGroup = sys.moduleGroup
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__)
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)
314 self._sys.searchGroups = cache[2] = snames[:]
315 sys.searchGroupObjects = cache[3] = sgroups[:]
316 return sys.searchGroupObjects
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)
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)
342 def public_attr(grp, name):
343 return (hasattr(grp, name) and
344 not (grp is self and name in self._private))
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)
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")
367 if len(parts) == 0:
368 return out
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
385 def has_symbol(self, symname):
386 try:
387 _ = self.get_symbol(symname)
388 return True
389 except (LookupError, NameError, ValueError):
390 return False
392 def has_group(self, gname):
393 try:
394 _ = self.get_group(gname)
395 return True
396 except (NameError, LookupError):
397 return False
399 def isgroup(self, sym):
400 "test if symbol is a group"
401 return isgroup(sym)
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")
410 def create_group(self, **kw):
411 "create a new Group, not placed anywhere in symbol table"
412 return Group(**kw)
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
420 def get_symbol(self, sym, create=False):
421 "lookup and return a symbol by name"
422 return self._lookup(sym, create=create)
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 = []
431 for n in name.split('.'):
432 if not isValidName(n):
433 raise SyntaxError(f"invalid symbol name '{n}'")
434 names.append(n)
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())
446 setattr(grp, child, value)
447 return value
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)
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
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
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))