Coverage for asteval/astutils.py: 92%

357 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-12-05 11:00 +0000

1""" 

2utility functions for asteval 

3 

4 Matthew Newville <newville@cars.uchicago.edu>, 

5 The University of Chicago 

6""" 

7import ast 

8import io 

9import math 

10import numbers 

11import re 

12from sys import exc_info 

13from tokenize import ENCODING as tk_ENCODING 

14from tokenize import NAME as tk_NAME 

15from tokenize import tokenize as generate_tokens 

16 

17builtins = __builtins__ 

18if not isinstance(builtins, dict): 

19 builtins = builtins.__dict__ 

20 

21HAS_NUMPY = False 

22try: 

23 import numpy 

24 numpy_version = numpy.version.version.split('.', 2) 

25 HAS_NUMPY = True 

26except ImportError: 

27 numpy = None 

28 

29HAS_NUMPY_FINANCIAL = False 

30try: 

31 import numpy_financial 

32 HAS_NUMPY_FINANCIAL = True 

33except ImportError: 

34 pass 

35 

36 

37 

38MAX_EXPONENT = 10000 

39MAX_STR_LEN = 2 << 17 # 256KiB 

40MAX_SHIFT = 1000 

41MAX_OPEN_BUFFER = 2 << 17 

42 

43RESERVED_WORDS = ('False', 'None', 'True', 'and', 'as', 'assert', 

44 'async', 'await', 'break', 'class', 'continue', 'def', 

45 'del', 'elif', 'else', 'except', 'finally', 'for', 

46 'from', 'global', 'if', 'import', 'in', 'is', 

47 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 

48 'return', 'try', 'while', 'with', 'yield', 'exec', 

49 'eval', 'execfile', '__import__', '__package__', 

50 '__fstring__') 

51 

52NAME_MATCH = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*$").match 

53 

54# unsafe attributes for all objects: 

55UNSAFE_ATTRS = ('__subclasses__', '__bases__', '__globals__', '__code__', 

56 '__reduce__', '__reduce_ex__', '__mro__', 

57 '__closure__', '__func__', '__self__', '__module__', 

58 '__dict__', '__class__', '__call__', '__get__', 

59 '__getattribute__', '__subclasshook__', '__new__', 

60 '__init__', 'func_globals', 'func_code', 'func_closure', 

61 'im_class', 'im_func', 'im_self', 'gi_code', 'gi_frame', 

62 'f_locals', '__asteval__') 

63 

64# unsafe attributes for particular objects, by type 

65UNSAFE_ATTRS_DTYPES = {str: ('format', 'format_map')} 

66 

67 

68# inherit these from python's __builtins__ 

69FROM_PY = ('ArithmeticError', 'AssertionError', 'AttributeError', 

70 'BaseException', 'BufferError', 'BytesWarning', 

71 'DeprecationWarning', 'EOFError', 'EnvironmentError', 

72 'Exception', 'False', 'FloatingPointError', 'GeneratorExit', 

73 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 

74 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 

75 'MemoryError', 'NameError', 'None', 

76 'NotImplementedError', 'OSError', 'OverflowError', 

77 'ReferenceError', 'RuntimeError', 'RuntimeWarning', 

78 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 

79 'SystemExit', 'True', 'TypeError', 'UnboundLocalError', 

80 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 

81 'UnicodeTranslateError', 'UnicodeWarning', 'ValueError', 

82 'Warning', 'ZeroDivisionError', 'abs', 'all', 'any', 'bin', 

83 'bool', 'bytearray', 'bytes', 'chr', 'complex', 'dict', 'dir', 

84 'divmod', 'enumerate', 'filter', 'float', 'format', 'frozenset', 

85 'hash', 'hex', 'id', 'int', 'isinstance', 'len', 'list', 'map', 

86 'max', 'min', 'oct', 'ord', 'pow', 'range', 'repr', 

87 'reversed', 'round', 'set', 'slice', 'sorted', 'str', 'sum', 

88 'tuple', 'zip') 

89 

90BUILTINS_TABLE = {sym: builtins[sym] for sym in FROM_PY if sym in builtins} 

91 

92# inherit these from python's math 

93FROM_MATH = ('acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 

94 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'exp', 

95 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 

96 'hypot', 'isinf', 'isnan', 'ldexp', 'log', 'log10', 'log1p', 

97 'modf', 'pi', 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 

98 'tanh', 'trunc') 

99 

100MATH_TABLE = {sym: getattr(math, sym) for sym in FROM_MATH if hasattr(math, sym)} 

101 

102FROM_NUMPY = ('abs', 'add', 'all', 'amax', 'amin', 'angle', 'any', 'append', 

103 'arange', 'arccos', 'arccosh', 'arcsin', 'arcsinh', 'arctan', 'arctan2', 

104 'arctanh', 'argmax', 'argmin', 'argsort', 'argwhere', 'around', 'array', 

105 'asarray', 'atleast_1d', 'atleast_2d', 'atleast_3d', 'average', 'bartlett', 

106 'bitwise_and', 'bitwise_not', 'bitwise_or', 'bitwise_xor', 'blackman', 

107 'broadcast', 'ceil', 'choose', 'clip', 'column_stack', 'common_type', 

108 'complex128', 'compress', 'concatenate', 'conjugate', 'convolve', 

109 'copysign', 'corrcoef', 'correlate', 'cos', 'cosh', 'cov', 'cross', 

110 'cumprod', 'cumsum', 'datetime_data', 'deg2rad', 'degrees', 'delete', 

111 'diag', 'diag_indices', 'diag_indices_from', 'diagflat', 'diagonal', 

112 'diff', 'digitize', 'divide', 'dot', 'dsplit', 'dstack', 'dtype', 'e', 

113 'ediff1d', 'empty', 'empty_like', 'equal', 'exp', 'exp2', 'expand_dims', 

114 'expm1', 'extract', 'eye', 'fabs', 'fill_diagonal', 'finfo', 'fix', 

115 'flatiter', 'flatnonzero', 'fliplr', 'flipud', 'float64', 'floor', 

116 'floor_divide', 'fmax', 'fmin', 'fmod', 'format_parser', 'frexp', 

117 'frombuffer', 'fromfile', 'fromfunction', 'fromiter', 'frompyfunc', 

118 'fromregex', 'fromstring', 'genfromtxt', 'getbufsize', 'geterr', 

119 'gradient', 'greater', 'greater_equal', 'hamming', 'hanning', 'histogram', 

120 'histogram2d', 'histogramdd', 'hsplit', 'hstack', 'hypot', 'i0', 

121 'identity', 'iinfo', 'imag', 'indices', 'inexact', 'inf', 'info', 'inner', 

122 'insert', 'int32', 'integer', 'interp', 'intersect1d', 'invert', 

123 'iscomplex', 'iscomplexobj', 'isfinite', 'isinf', 'isnan', 'isneginf', 

124 'isposinf', 'isreal', 'isrealobj', 'isscalar', 'iterable', 'kaiser', 

125 'kron', 'ldexp', 'left_shift', 'less', 'less_equal', 'linalg', 'linspace', 

126 'little_endian', 'loadtxt', 'log', 'log10', 'log1p', 'log2', 'logaddexp', 

127 'logaddexp2', 'logical_and', 'logical_not', 'logical_or', 'logical_xor', 

128 'logspace', 'longdouble', 'longlong', 'mask_indices', 'matrix', 'maximum', 

129 'may_share_memory', 'mean', 'median', 'memmap', 'meshgrid', 'minimum', 

130 'mintypecode', 'mod', 'modf', 'msort', 'multiply', 'nan', 'nan_to_num', 

131 'nanargmax', 'nanargmin', 'nanmax', 'nanmin', 'nansum', 'ndarray', 

132 'ndenumerate', 'ndim', 'ndindex', 'negative', 'nextafter', 'nonzero', 

133 'not_equal', 'number', 'ones', 'ones_like', 'outer', 'packbits', 

134 'percentile', 'pi', 'piecewise', 'place', 'poly', 'poly1d', 'polyadd', 

135 'polyder', 'polydiv', 'polyint', 'polymul', 'polysub', 'polyval', 'power', 

136 'prod', 'ptp', 'put', 'putmask', 'rad2deg', 'radians', 'ravel', 'real', 

137 'real_if_close', 'reciprocal', 'record', 'remainder', 'repeat', 'reshape', 

138 'resize', 'right_shift', 'rint', 'roll', 'rollaxis', 'roots', 'rot90', 

139 'round', 'searchsorted', 'select', 'setbufsize', 'setdiff1d', 'seterr', 

140 'setxor1d', 'shape', 'short', 'sign', 'signbit', 'signedinteger', 'sin', 

141 'sinc', 'single', 'sinh', 'size', 'sort', 'sort_complex', 'spacing', 

142 'split', 'sqrt', 'square', 'squeeze', 'std', 'subtract', 'sum', 'swapaxes', 

143 'take', 'tan', 'tanh', 'tensordot', 'tile', 'trace', 'transpose', 'tri', 

144 'tril', 'tril_indices', 'tril_indices_from', 'trim_zeros', 'triu', 

145 'triu_indices', 'triu_indices_from', 'true_divide', 'trunc', 'ubyte', 

146 'uint', 'uint32', 'union1d', 'unique', 'unravel_index', 'unsignedinteger', 

147 'unwrap', 'ushort', 'vander', 'var', 'vdot', 'vectorize', 'vsplit', 

148 'vstack', 'where', 'zeros', 'zeros_like') 

149 

150 

151FROM_NUMPY_FINANCIAL = ('fv', 'ipmt', 'irr', 'mirr', 'nper', 'npv', 

152 'pmt', 'ppmt', 'pv', 'rate') 

153 

154NUMPY_RENAMES = {'ln': 'log', 'asin': 'arcsin', 'acos': 'arccos', 

155 'atan': 'arctan', 'atan2': 'arctan2', 'atanh': 

156 'arctanh', 'acosh': 'arccosh', 'asinh': 'arcsinh'} 

157 

158if HAS_NUMPY: 

159 FROM_NUMPY = tuple(set(FROM_NUMPY)) 

160 FROM_NUMPY = tuple(sym for sym in FROM_NUMPY if hasattr(numpy, sym)) 

161 NUMPY_RENAMES = {sym: value for sym, value in NUMPY_RENAMES.items() if hasattr(numpy, value)} 

162 

163 NUMPY_TABLE = {} 

164 for sym in FROM_NUMPY: 

165 obj = getattr(numpy, sym, None) 

166 if obj is not None: 

167 NUMPY_TABLE[sym] = obj 

168 

169 for sname, sym in NUMPY_RENAMES.items(): 

170 obj = getattr(numpy, sym, None) 

171 if obj is not None: 

172 NUMPY_TABLE[sname] = obj 

173 

174 if HAS_NUMPY_FINANCIAL: 

175 for sym in FROM_NUMPY_FINANCIAL: 

176 obj = getattr(numpy_financial, sym, None) 

177 if obj is not None: 

178 NUMPY_TABLE[sym] = obj 

179 

180else: 

181 NUMPY_TABLE = {} 

182 

183 

184def _open(filename, mode='r', buffering=-1, encoding=None): 

185 """read only version of open()""" 

186 if mode not in ('r', 'rb', 'rU'): 

187 raise RuntimeError("Invalid open file mode, must be 'r', 'rb', or 'rU'") 

188 if buffering > MAX_OPEN_BUFFER: 

189 raise RuntimeError(f"Invalid buffering value, max buffer size is {MAX_OPEN_BUFFER}") 

190 return open(filename, mode, buffering, encoding=encoding) 

191 

192 

193def _type(x): 

194 """type that prevents varargs and varkws""" 

195 return type(x).__name__ 

196 

197 

198LOCALFUNCS = {'open': _open, 'type': _type} 

199 

200 

201# Safe versions of functions to prevent denial of service issues 

202 

203def safe_pow(base, exp): 

204 """safe version of pow""" 

205 if isinstance(exp, numbers.Number): 

206 if exp > MAX_EXPONENT: 

207 raise RuntimeError(f"Invalid exponent, max exponent is {MAX_EXPONENT}") 

208 elif HAS_NUMPY and isinstance(exp, numpy.ndarray): 

209 if numpy.nanmax(exp) > MAX_EXPONENT: 

210 raise RuntimeError(f"Invalid exponent, max exponent is {MAX_EXPONENT}") 

211 return base ** exp 

212 

213 

214def safe_mult(arg1, arg2): 

215 """safe version of multiply""" 

216 if isinstance(arg1, str) and isinstance(arg2, int) and len(arg1) * arg2 > MAX_STR_LEN: 

217 raise RuntimeError(f"String length exceeded, max string length is {MAX_STR_LEN}") 

218 return arg1 * arg2 

219 

220 

221def safe_add(arg1, arg2): 

222 """safe version of add""" 

223 if isinstance(arg1, str) and isinstance(arg2, str) and len(arg1) + len(arg2) > MAX_STR_LEN: 

224 raise RuntimeError(f"String length exceeded, max string length is {MAX_STR_LEN}") 

225 return arg1 + arg2 

226 

227 

228def safe_lshift(arg1, arg2): 

229 """safe version of lshift""" 

230 if isinstance(arg2, numbers.Number): 

231 if arg2 > MAX_SHIFT: 

232 raise RuntimeError(f"Invalid left shift, max left shift is {MAX_SHIFT}") 

233 elif HAS_NUMPY and isinstance(arg2, numpy.ndarray): 

234 if numpy.nanmax(arg2) > MAX_SHIFT: 

235 raise RuntimeError(f"Invalid left shift, max left shift is {MAX_SHIFT}") 

236 return arg1 << arg2 

237 

238 

239OPERATORS = {ast.Is: lambda a, b: a is b, 

240 ast.IsNot: lambda a, b: a is not b, 

241 ast.In: lambda a, b: a in b, 

242 ast.NotIn: lambda a, b: a not in b, 

243 ast.Add: safe_add, 

244 ast.BitAnd: lambda a, b: a & b, 

245 ast.BitOr: lambda a, b: a | b, 

246 ast.BitXor: lambda a, b: a ^ b, 

247 ast.Div: lambda a, b: a / b, 

248 ast.FloorDiv: lambda a, b: a // b, 

249 ast.LShift: safe_lshift, 

250 ast.RShift: lambda a, b: a >> b, 

251 ast.Mult: safe_mult, 

252 ast.Pow: safe_pow, 

253 ast.MatMult: lambda a, b: a @ b, 

254 ast.Sub: lambda a, b: a - b, 

255 ast.Mod: lambda a, b: a % b, 

256 ast.And: lambda a, b: a and b, 

257 ast.Or: lambda a, b: a or b, 

258 ast.Eq: lambda a, b: a == b, 

259 ast.Gt: lambda a, b: a > b, 

260 ast.GtE: lambda a, b: a >= b, 

261 ast.Lt: lambda a, b: a < b, 

262 ast.LtE: lambda a, b: a <= b, 

263 ast.NotEq: lambda a, b: a != b, 

264 ast.Invert: lambda a: ~a, 

265 ast.Not: lambda a: not a, 

266 ast.UAdd: lambda a: +a, 

267 ast.USub: lambda a: -a} 

268 

269 

270def valid_symbol_name(name): 

271 """Determine whether the input symbol name is a valid name. 

272 

273 Arguments 

274 --------- 

275 name : str 

276 name to check for validity. 

277 

278 Returns 

279 -------- 

280 valid : bool 

281 whether name is a a valid symbol name 

282 

283 This checks for Python reserved words and that the name matches 

284 the regular expression ``[a-zA-Z_][a-zA-Z0-9_]`` 

285 """ 

286 if name in RESERVED_WORDS: 

287 return False 

288 

289 gen = generate_tokens(io.BytesIO(name.encode('utf-8')).readline) 

290 typ, _, start, end, _ = next(gen) 

291 if typ == tk_ENCODING: 

292 typ, _, start, end, _ = next(gen) 

293 return typ == tk_NAME and start == (1, 0) and end == (1, len(name)) 

294 

295 

296def op2func(oper): 

297 """Return function for operator nodes.""" 

298 return OPERATORS[oper.__class__] 

299 

300 

301class Empty: 

302 """Empty class.""" 

303 def __init__(self): 

304 """TODO: docstring in public method.""" 

305 return 

306 

307 def __nonzero__(self): 

308 """Empty is TODO: docstring in magic method.""" 

309 return False 

310 

311 def __repr__(self): 

312 """Empty is TODO: docstring in magic method.""" 

313 return "Empty" 

314 

315ReturnedNone = Empty() 

316 

317class ExceptionHolder: 

318 """Basic exception handler.""" 

319 def __init__(self, node, exc=None, msg='', expr=None, lineno=None): 

320 """TODO: docstring in public method.""" 

321 self.node = node 

322 self.expr = expr 

323 self.msg = msg 

324 self.exc = exc 

325 self.lineno = lineno 

326 self.exc_info = exc_info() 

327 if self.exc is None and self.exc_info[0] is not None: 

328 self.exc = self.exc_info[0] 

329 if self.msg == '' and self.exc_info[1] is not None: 

330 self.msg = str(self.exc_info[1]) 

331 

332 def get_error(self): 

333 """Retrieve error data.""" 

334 col_offset = -1 

335 if self.node is not None: 

336 try: 

337 col_offset = self.node.col_offset 

338 except AttributeError: 

339 pass 

340 try: 

341 exc_name = self.exc.__name__ 

342 except AttributeError: 

343 exc_name = str(self.exc) 

344 if exc_name in (None, 'None'): 

345 exc_name = 'UnknownError' 

346 

347 out = [f" {self.expr}"] 

348 if col_offset > 0: 

349 out.append(f" {col_offset*' '}^^^^") 

350 out.append(f"{exc_name}: {self.msg}") 

351 return (exc_name, '\n'.join(out)) 

352 

353 def __repr__(self): 

354 return f"ExceptionHolder({self.exc}, {self.msg})" 

355 

356class NameFinder(ast.NodeVisitor): 

357 """Find all symbol names used by a parsed node.""" 

358 

359 def __init__(self): 

360 """TODO: docstring in public method.""" 

361 self.names = [] 

362 ast.NodeVisitor.__init__(self) 

363 

364 def generic_visit(self, node): 

365 """TODO: docstring in public method.""" 

366 if node.__class__.__name__ == 'Name': 

367 if node.id not in self.names: 

368 self.names.append(node.id) 

369 ast.NodeVisitor.generic_visit(self, node) 

370 

371 

372def get_ast_names(astnode): 

373 """Return symbol Names from an AST node.""" 

374 finder = NameFinder() 

375 finder.generic_visit(astnode) 

376 return finder.names 

377 

378 

379def valid_varname(name): 

380 "is this a valid variable name" 

381 return name.isidentifier() and name not in RESERVED_WORDS 

382 

383 

384class Group(dict): 

385 """ 

386 Group: a container of objects that can be accessed either as an object attributes 

387 or dictionary key/value. Attribute names must follow Python naming conventions. 

388 """ 

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

390 if name is None: 

391 name = hex(id(self)) 

392 self.__name__ = name 

393 dict.__init__(self, **kws) 

394 self._searchgroups = searchgroups 

395 

396 def __setattr__(self, name, value): 

397 if not valid_varname(name): 

398 raise SyntaxError(f"invalid attribute name '{name}'") 

399 self[name] = value 

400 

401 def __getattr__(self, name, default=None): 

402 if name in self: 

403 return self[name] 

404 if default is not None: 

405 return default 

406 raise KeyError(f"no attribute named '{name}'") 

407 

408 def __setitem__(self, name, value): 

409 if valid_varname(name): 

410 dict.__setitem__(self, name, value) 

411 else: # raise SyntaxError(f"invalid attribute name '{name}'") 

412 return setattr(self, name, value) 

413 

414 def get(self, key, default=None): 

415 val = self.__getattr__(key, ReturnedNone) 

416 if not isinstance(val, Empty): 

417 return val 

418 searchgroups = self._searchgroups 

419 if searchgroups is not None: 

420 for sgroup in searchgroups: 

421 grp = self.__getattr__(sgroup, None) 

422 if isinstance(grp, (Group, dict)): 

423 val = grp.__getattr__(key, ReturnedNone) 

424 if not isinstance(val, Empty): 

425 return val 

426 return default 

427 

428 

429 def __repr__(self): 

430 keys = [a for a in self.keys() if a != '__name__'] 

431 return f"Group('{self.__name__}', {len(keys)} symbols)" 

432 

433 def _repr_html_(self): 

434 """HTML representation for Jupyter notebook""" 

435 html = [f"<table><caption>Group('{self.__name__}')</caption>", 

436 "<tr><th>Attribute</th><th>DataType</th><th><b>Value</b></th></tr>"] 

437 for key, val in self.items(): 

438 html.append(f""" 

439<tr><td>{key}</td><td><i>{type(val).__name__}</i></td> 

440 <td>{repr(val):.75s}</td> 

441</tr>""") 

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

443 return '\n'.join(html) 

444 

445 

446def make_symbol_table(use_numpy=True, nested=False, top=True, **kws): 

447 """Create a default symboltable, taking dict of user-defined symbols. 

448 

449 Arguments 

450 --------- 

451 numpy : bool, optional 

452 whether to include symbols from numpy [True] 

453 nested : bool, optional 

454 whether to make a "new-style" nested table instead of a plain dict [False] 

455 top : bool, optional 

456 whether this is the top-level table in a nested-table [True] 

457 kws : optional 

458 additional symbol name, value pairs to include in symbol table 

459 

460 Returns 

461 -------- 

462 symbol_table : dict or nested Group 

463 a symbol table that can be used in `asteval.Interpereter` 

464 

465 """ 

466 if nested: 

467 name = '_' 

468 if top: 

469 name = '_main' 

470 if 'name' in kws: 

471 name = kws.pop('name') 

472 symtable = Group(name=name, Group=Group) 

473 else: 

474 symtable = {} 

475 

476 symtable.update(BUILTINS_TABLE) 

477 symtable.update(LOCALFUNCS) 

478 symtable.update(kws) 

479 math_functions = dict(MATH_TABLE.items()) 

480 if use_numpy: 

481 math_functions.update(NUMPY_TABLE) 

482 

483 if nested: 

484 symtable['math'] = Group(name='math', **math_functions) 

485 symtable['Group'] = Group 

486 symtable._searchgroups = ('math',) 

487 else: 

488 symtable.update(math_functions) 

489 symtable.update(**kws) 

490 return symtable 

491 

492 

493class Procedure: 

494 """Procedure: user-defined function for asteval. 

495 

496 This stores the parsed ast nodes as from the 'functiondef' ast node 

497 for later evaluation. 

498 

499 """ 

500 

501 def __init__(self, name, interp, doc=None, lineno=0, 

502 body=None, args=None, kwargs=None, 

503 vararg=None, varkws=None): 

504 """TODO: docstring in public method.""" 

505 self.__ininit__ = True 

506 self.name = name 

507 self.__name__ = self.name 

508 self.__asteval__ = interp 

509 self.raise_exc = self.__asteval__.raise_exception 

510 self.__doc__ = doc 

511 self.body = body 

512 self.argnames = args 

513 self.kwargs = kwargs 

514 self.vararg = vararg 

515 self.varkws = varkws 

516 self.lineno = lineno 

517 self.__ininit__ = False 

518 

519 def __setattr__(self, attr, val): 

520 if not getattr(self, '__ininit__', True): 

521 self.raise_exc(None, exc=TypeError, 

522 msg="procedure is read-only") 

523 self.__dict__[attr] = val 

524 

525 def __dir__(self): 

526 return ['_getdoc', 'argnames', 'kwargs', 'name', 'vararg', 'varkws'] 

527 

528 def _getdoc(self): 

529 doc = self.__doc__ 

530 if isinstance(doc, ast.Constant): 

531 doc = doc.value 

532 return doc 

533 

534 def __repr__(self): 

535 """TODO: docstring in magic method.""" 

536 sig = self._signature() 

537 rep = f"<Procedure {sig}>" 

538 doc = self._getdoc() 

539 if doc is not None: 

540 rep = f"{rep}\n {doc}" 

541 return rep 

542 

543 def _signature(self): 

544 "call signature" 

545 sig = "" 

546 if len(self.argnames) > 0: 

547 sig = sig + ', '.join(self.argnames) 

548 if self.vararg is not None: 

549 sig = sig + f"*{self.vararg}" 

550 if len(self.kwargs) > 0: 

551 if len(sig) > 0: 

552 sig = f"{sig}, " 

553 _kw = [f"{k}={v}" for k, v in self.kwargs] 

554 sig = f"{sig}{', '.join(_kw)}" 

555 

556 if self.varkws is not None: 

557 sig = f"{sig}, **{self.varkws}" 

558 return f"{self.name}({sig})" 

559 

560 def __call__(self, *args, **kwargs): 

561 """TODO: docstring in public method.""" 

562 topsym = self.__asteval__.symtable 

563 if self.__asteval__.config.get('nested_symtable', False): 

564 sargs = {'_main': topsym} 

565 sgroups = topsym.get('_searchgroups', None) 

566 if sgroups is not None: 

567 for sxname in sgroups: 

568 sargs[sxname] = topsym.get(sxname) 

569 

570 

571 symlocals = Group(name=f'symtable_{self.name}_', **sargs) 

572 symlocals._searchgroups = list(sargs.keys()) 

573 else: 

574 symlocals = {} 

575 

576 args = list(args) 

577 nargs = len(args) 

578 nkws = len(kwargs) 

579 nargs_expected = len(self.argnames) 

580 

581 # check for too few arguments, but the correct keyword given 

582 if (nargs < nargs_expected) and nkws > 0: 

583 for name in self.argnames[nargs:]: 

584 if name in kwargs: 

585 args.append(kwargs.pop(name)) 

586 nargs = len(args) 

587 nargs_expected = len(self.argnames) 

588 nkws = len(kwargs) 

589 if nargs < nargs_expected: 

590 msg = f"{self.name}() takes at least" 

591 msg = f"{msg} {nargs_expected} arguments, got {nargs}" 

592 self.raise_exc(None, exc=TypeError, msg=msg) 

593 # check for multiple values for named argument 

594 if len(self.argnames) > 0 and kwargs is not None: 

595 msg = "multiple values for keyword argument" 

596 for targ in self.argnames: 

597 if targ in kwargs: 

598 msg = f"{msg} '{targ}' in Procedure {self.name}" 

599 self.raise_exc(None, exc=TypeError, msg=msg, lineno=self.lineno) 

600 

601 # check more args given than expected, varargs not given 

602 if nargs != nargs_expected: 

603 msg = None 

604 if nargs < nargs_expected: 

605 msg = f"not enough arguments for Procedure {self.name}()" 

606 msg = f"{msg} (expected {nargs_expected}, got {nargs}" 

607 self.raise_exc(None, exc=TypeError, msg=msg) 

608 

609 if nargs > nargs_expected and self.vararg is None: 

610 if nargs - nargs_expected > len(self.kwargs): 

611 msg = f"too many arguments for {self.name}() expected at most" 

612 msg = f"{msg} {len(self.kwargs)+nargs_expected}, got {nargs}" 

613 self.raise_exc(None, exc=TypeError, msg=msg) 

614 

615 for i, xarg in enumerate(args[nargs_expected:]): 

616 kw_name = self.kwargs[i][0] 

617 if kw_name not in kwargs: 

618 kwargs[kw_name] = xarg 

619 

620 for argname in self.argnames: 

621 symlocals[argname] = args.pop(0) 

622 

623 try: 

624 if self.vararg is not None: 

625 symlocals[self.vararg] = tuple(args) 

626 

627 for key, val in self.kwargs: 

628 if key in kwargs: 

629 val = kwargs.pop(key) 

630 symlocals[key] = val 

631 

632 if self.varkws is not None: 

633 symlocals[self.varkws] = kwargs 

634 

635 elif len(kwargs) > 0: 

636 msg = f"extra keyword arguments for Procedure {self.name}: " 

637 msg = msg + ','.join(list(kwargs.keys())) 

638 self.raise_exc(None, msg=msg, exc=TypeError, 

639 lineno=self.lineno) 

640 

641 except (ValueError, LookupError, TypeError, 

642 NameError, AttributeError): 

643 msg = f"incorrect arguments for Procedure {self.name}" 

644 self.raise_exc(None, msg=msg, lineno=self.lineno) 

645 

646 if self.__asteval__.config.get('nested_symtable', False): 

647 save_symtable = self.__asteval__.symtable 

648 self.__asteval__.symtable = symlocals 

649 else: 

650 save_symtable = self.__asteval__.symtable.copy() 

651 self.__asteval__.symtable.update(symlocals) 

652 

653 self.__asteval__.retval = None 

654 self.__asteval__._calldepth += 1 

655 retval = None 

656 

657 # evaluate script of function 

658 for node in self.body: 

659 self.__asteval__.run(node, expr='<>', lineno=self.lineno) 

660 if len(self.__asteval__.error) > 0: 

661 break 

662 if self.__asteval__.retval is not None: 

663 retval = self.__asteval__.retval 

664 self.__asteval__.retval = None 

665 if retval is ReturnedNone: 

666 retval = None 

667 break 

668 

669 self.__asteval__.symtable = save_symtable 

670 self.__asteval__._calldepth -= 1 

671 symlocals = None 

672 return retval