simpleeval 1.0.6__tar.gz → 1.0.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {simpleeval-1.0.6 → simpleeval-1.0.8}/PKG-INFO +5 -3
  2. {simpleeval-1.0.6 → simpleeval-1.0.8}/README.rst +4 -2
  3. {simpleeval-1.0.6 → simpleeval-1.0.8}/pyproject.toml +24 -5
  4. {simpleeval-1.0.6 → simpleeval-1.0.8}/simpleeval.py +51 -65
  5. simpleeval-1.0.8/tests/__init__.py +0 -0
  6. simpleeval-1.0.8/tests/base.py +18 -0
  7. simpleeval-1.0.8/tests/test_allowed_attributes.py +76 -0
  8. simpleeval-1.0.8/tests/test_basics.py +168 -0
  9. simpleeval-1.0.8/tests/test_compound_types.py +117 -0
  10. simpleeval-1.0.8/tests/test_comprehensions.py +103 -0
  11. simpleeval-1.0.8/tests/test_deliberately_missed_features.py +17 -0
  12. simpleeval-1.0.8/tests/test_disallowed_functions.py +61 -0
  13. simpleeval-1.0.8/tests/test_evaluator.py +48 -0
  14. simpleeval-1.0.8/tests/test_exceptions.py +41 -0
  15. simpleeval-1.0.8/tests/test_extending_the_class.py +28 -0
  16. simpleeval-1.0.8/tests/test_functions.py +123 -0
  17. simpleeval-1.0.8/tests/test_get_item_unhappy_paths.py +33 -0
  18. simpleeval-1.0.8/tests/test_method_chaining.py +32 -0
  19. simpleeval-1.0.8/tests/test_module_wrapper.py +174 -0
  20. simpleeval-1.0.8/tests/test_names.py +230 -0
  21. simpleeval-1.0.8/tests/test_no_entries.py +32 -0
  22. simpleeval-1.0.8/tests/test_operators.py +7 -0
  23. simpleeval-1.0.8/tests/test_reference_cleanup.py +35 -0
  24. simpleeval-1.0.8/tests/test_security.py +637 -0
  25. simpleeval-1.0.8/tests/test_short_circuiting.py +33 -0
  26. simpleeval-1.0.8/tests/test_simple_eval_function.py +16 -0
  27. simpleeval-1.0.8/tests/test_unusual_comparisons.py +51 -0
  28. simpleeval-1.0.8/tests/test_whitespace.py +23 -0
  29. simpleeval-1.0.6/.v2/lib/python3.13/site-packages/docutils/parsers/rst/include/README.rst +0 -17
  30. simpleeval-1.0.6/.v2/lib/python3.13/site-packages/docutils/writers/s5_html/themes/README.rst +0 -6
  31. simpleeval-1.0.6/.v2/lib/python3.13/site-packages/pip/_vendor/README.rst +0 -180
  32. simpleeval-1.0.6/.v314/lib/python3.14/site-packages/simpleeval-1.0.3.dist-info/licenses/LICENCE +0 -21
  33. simpleeval-1.0.6/.v314/lib/python3.14/site-packages/simpleeval.py +0 -768
  34. simpleeval-1.0.6/test_simpleeval.py +0 -1959
  35. {simpleeval-1.0.6 → simpleeval-1.0.8}/.gitignore +0 -0
  36. {simpleeval-1.0.6 → simpleeval-1.0.8}/LICENCE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: simpleeval
3
- Version: 1.0.6
3
+ Version: 1.0.8
4
4
  Summary: A simple, safe single expression evaluator library.
5
5
  Project-URL: Source code, https://github.com/danthedeckie/simpleeval
6
6
  Author-email: Daniel Fairhead <danthedeckie@gmail.com>
@@ -556,7 +556,7 @@ The initial idea came from J.F. Sebastian on Stack Overflow
556
556
  ( http://stackoverflow.com/a/9558001/1973500 ) with modifications and many improvements,
557
557
  see the head of the main file for contributors list.
558
558
 
559
- Please read the ``test_simpleeval.py`` file for other potential gotchas or
559
+ Please read the ``tests/`` for other potential gotchas or
560
560
  details. I'm very happy to accept pull requests, suggestions, or other issues.
561
561
  Enjoy!
562
562
 
@@ -573,7 +573,7 @@ Or to set the tests running on every file change:
573
573
 
574
574
  (requires ``entr``)
575
575
 
576
- I'm trying to keep the codebase relatively clean with Black, isort, pylint & mypy.
576
+ I'm trying to keep the codebase relatively clean with ruff, & mypy.
577
577
  See::
578
578
 
579
579
  $ make format
@@ -582,6 +582,8 @@ and::
582
582
 
583
583
  $ make lint
584
584
 
585
+ You can run tests against python versions 3.9-3.14 via `tox` (if you have them installed)
586
+
585
587
  BEWARE
586
588
  ------
587
589
 
@@ -539,7 +539,7 @@ The initial idea came from J.F. Sebastian on Stack Overflow
539
539
  ( http://stackoverflow.com/a/9558001/1973500 ) with modifications and many improvements,
540
540
  see the head of the main file for contributors list.
541
541
 
542
- Please read the ``test_simpleeval.py`` file for other potential gotchas or
542
+ Please read the ``tests/`` for other potential gotchas or
543
543
  details. I'm very happy to accept pull requests, suggestions, or other issues.
544
544
  Enjoy!
545
545
 
@@ -556,7 +556,7 @@ Or to set the tests running on every file change:
556
556
 
557
557
  (requires ``entr``)
558
558
 
559
- I'm trying to keep the codebase relatively clean with Black, isort, pylint & mypy.
559
+ I'm trying to keep the codebase relatively clean with ruff, & mypy.
560
560
  See::
561
561
 
562
562
  $ make format
@@ -565,6 +565,8 @@ and::
565
565
 
566
566
  $ make lint
567
567
 
568
+ You can run tests against python versions 3.9-3.14 via `tox` (if you have them installed)
569
+
568
570
  BEWARE
569
571
  ------
570
572
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simpleeval"
3
- version = "1.0.6"
3
+ version = "1.0.8"
4
4
  requires-python = ">=3.9"
5
5
  readme = "README.rst"
6
6
  description = "A simple, safe single expression evaluator library."
@@ -27,6 +27,15 @@ dependencies = []
27
27
  [project.urls]
28
28
  "Source code" = "https://github.com/danthedeckie/simpleeval"
29
29
 
30
+ [dependency-groups]
31
+ dev = [
32
+ "hatch>=1.15.1",
33
+ "mypy>=1.19.1",
34
+ "ruff>=0.16.1",
35
+ "tox>=4.30.3",
36
+ "twine>=6.2.0",
37
+ ]
38
+
30
39
  [pycodestyle]
31
40
  max_line_length = 99
32
41
 
@@ -37,7 +46,7 @@ build-backend = "hatchling.build"
37
46
  [tool.hatch.build.targets.sdist]
38
47
  include = [
39
48
  "simpleeval.py",
40
- "test_simpleeval.py",
49
+ "tests/*.py",
41
50
  "README.rst",
42
51
  "LICENCE",
43
52
  ]
@@ -50,9 +59,6 @@ path = "simpleeval.py"
50
59
  packages = ["simpleeval.py"]
51
60
  core-metadata-version = "2.3"
52
61
 
53
- [tool.hatch.envs.hatch-test]
54
- default-args = ["test_simpleeval.py"]
55
-
56
62
  [tool.black]
57
63
  line-length = 99
58
64
  target-version = ['py310']
@@ -81,3 +87,16 @@ disable = [
81
87
  "unnecessary-pass",
82
88
  "bad-super-call",
83
89
  ]
90
+
91
+ [tool.uv]
92
+ required-version = ">=0.12.1"
93
+
94
+ [tool.tox]
95
+ env_list = ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "lint"]
96
+
97
+ [tool.tox.env_run_base]
98
+ commands = [["python", "-Werror", "-m", "unittest"]]
99
+
100
+ [tool.tox.env.lint]
101
+ commands = [["make", "lint"]]
102
+ allowlist_externals = ["make"]
@@ -2,7 +2,7 @@
2
2
  SimpleEval - (C) 2013-2026 Daniel Fairhead
3
3
  -------------------------------------
4
4
 
5
- An short, easy to use, safe and reasonably extensible expression evaluator.
5
+ A short, easy to use, safe and reasonably extensible expression evaluator.
6
6
  Designed for things like in a website where you want to allow the user to
7
7
  generate a string, or a number from some other input, without allowing full
8
8
  eval() or other unsafe or needlessly complex linguistics.
@@ -59,11 +59,15 @@ Contributors:
59
59
  - smurfix (Matthias Urlichs) Allow clearing functions / operators / etc completely
60
60
  - koenigsley (Mikhail Yeremeyev) documentation typos correction.
61
61
  - kurtmckee (Kurt McKee) Infrastructure updates
62
- - edgarrmondragon (Edgar Ramírez-Mondragón) Address Python 3.12+ deprecation warnings
62
+ - edgarrmondragon (Edgar Ramírez-Mondragón) Address Python 3.12+ deprecation warnings,
63
+ performance fixes
63
64
  - cedk (Cédric Krier) <ced@b2ck.com> Allow running tests with Werror
64
65
  - decorator-factory <decorator-factory@protonmail.com> More security fixes
65
66
  - lkruitwagen (Lucas Kruitwagen) Adding support for dict comprehensions
66
67
  - ByamB4 (Byambadalai) Reported breakout via module & disallowed functions as object attrs
68
+ - Charles Vosburgh Reported & tested dangerous functions in `os`
69
+ - Tradi3 (github.com/krutftw) Reported escape via operator module functions
70
+ - arpitjain099 (Arpit Jain) Reported missing set & frozenset for names
67
71
 
68
72
  -------------------------------------
69
73
  Basic Usage:
@@ -103,6 +107,8 @@ well:
103
107
 
104
108
  """
105
109
 
110
+ from __future__ import annotations
111
+
106
112
  import ast
107
113
  import operator as op
108
114
  import os
@@ -110,7 +116,6 @@ import sys
110
116
  import types
111
117
  import warnings
112
118
  from random import random
113
- from typing import Type, Dict, Set, Union
114
119
 
115
120
  ########################################
116
121
  # Module wide 'globals'
@@ -135,12 +140,9 @@ DISALLOW_METHODS = [
135
140
  ########################################
136
141
  # Tiny helpers:
137
142
 
138
-
139
- def is_hashable(value):
140
- try:
141
- return hash(value)
142
- except TypeError:
143
- return False
143
+ # Primitive types that are always safe: can't be modules, can't be in DISALLOW_FUNCTIONS,
144
+ # and don't need recursive container checks. Used as a fast path in _check_disallowed_items.
145
+ _PRIMITIVE_TYPES = frozenset({int, float, str, bool, type(None), bytes, complex})
144
146
 
145
147
 
146
148
  # Disallow functions:
@@ -164,6 +166,10 @@ DISALLOW_FUNCTIONS = {
164
166
  locals,
165
167
  os.popen,
166
168
  os.system,
169
+ *(getattr(os, func) for func in dir(os) if func.startswith(("exec", "spawn", "posix_spawn"))),
170
+ op.attrgetter,
171
+ op.itemgetter,
172
+ op.methodcaller,
167
173
  }
168
174
  if hasattr(__builtins__, "help") or (
169
175
  hasattr(__builtins__, "__contains__") and "help" in __builtins__ # type: ignore
@@ -171,9 +177,12 @@ if hasattr(__builtins__, "help") or (
171
177
  # PyInstaller environment doesn't include this module.
172
178
  DISALLOW_FUNCTIONS.add(help)
173
179
 
180
+ if hasattr(op, "call"):
181
+ DISALLOW_FUNCTIONS.add(op.call)
182
+
174
183
  # Opt-in type safety experiment. Will be opt-out in 2.x
175
184
 
176
- BASIC_ALLOWED_ATTRS: Dict[Union[Type, None], Set] = {
185
+ BASIC_ALLOWED_ATTRS: dict[type | None, set] = {
177
186
  int: {
178
187
  "as_integer_ratio",
179
188
  "bit_length",
@@ -315,17 +324,13 @@ class TypeNotSpecified(Exception):
315
324
  class InvalidExpression(Exception):
316
325
  """Generic Exception"""
317
326
 
318
- pass
319
-
320
327
 
321
328
  class FunctionNotDefined(InvalidExpression):
322
329
  """sorry! That function isn't defined!"""
323
330
 
324
331
  def __init__(self, func_name, expression):
325
- self.message = "Function '{0}' not defined, for expression '{1}'.".format(
326
- func_name, expression
327
- )
328
- setattr(self, "func_name", func_name) # bypass 2to3 confusion.
332
+ self.message = f"Function '{func_name}' not defined, for expression '{expression}'."
333
+ self.func_name = func_name # bypass 2to3 confusion.
329
334
  self.expression = expression
330
335
 
331
336
  super(InvalidExpression, self).__init__(self.message)
@@ -336,7 +341,7 @@ class NameNotDefined(InvalidExpression):
336
341
 
337
342
  def __init__(self, name, expression):
338
343
  self.name = name
339
- self.message = "'{0}' is not defined for expression '{1}'".format(name, expression)
344
+ self.message = f"'{name}' is not defined for expression '{expression}'"
340
345
  self.expression = expression
341
346
 
342
347
  super(InvalidExpression, self).__init__(self.message)
@@ -346,9 +351,7 @@ class AttributeDoesNotExist(InvalidExpression):
346
351
  """attribute does not exist"""
347
352
 
348
353
  def __init__(self, attr, expression):
349
- self.message = "Attribute '{0}' does not exist in expression '{1}'".format(
350
- attr, expression
351
- )
354
+ self.message = f"Attribute '{attr}' does not exist in expression '{expression}'"
352
355
  self.attr = attr
353
356
  self.expression = expression
354
357
 
@@ -359,7 +362,7 @@ class OperatorNotDefined(InvalidExpression):
359
362
  """operator does not exist"""
360
363
 
361
364
  def __init__(self, attr, expression):
362
- self.message = "Operator '{0}' does not exist in expression '{1}'".format(attr, expression)
365
+ self.message = f"Operator '{attr}' does not exist in expression '{expression}'"
363
366
  self.attr = attr
364
367
  self.expression = expression
365
368
 
@@ -369,33 +372,23 @@ class OperatorNotDefined(InvalidExpression):
369
372
  class FeatureNotAvailable(InvalidExpression):
370
373
  """What you're trying to do is not allowed."""
371
374
 
372
- pass
373
-
374
375
 
375
376
  class NumberTooHigh(InvalidExpression):
376
377
  """Sorry! That number is too high. I don't want to spend the
377
378
  next 10 years evaluating this expression!"""
378
379
 
379
- pass
380
-
381
380
 
382
381
  class IterableTooLong(InvalidExpression):
383
382
  """That iterable is **way** too long, baby."""
384
383
 
385
- pass
386
-
387
384
 
388
385
  class AssignmentAttempted(UserWarning):
389
386
  """Assignment not allowed in SimpleEval"""
390
387
 
391
- pass
392
-
393
388
 
394
389
  class MultipleExpressions(UserWarning):
395
390
  """Only the first expression parsed will be used"""
396
391
 
397
- pass
398
-
399
392
 
400
393
  # Sentinal used during attr access
401
394
  _ATTR_NOT_FOUND = object()
@@ -459,7 +452,7 @@ def safe_power(a, b): # pylint: disable=invalid-name
459
452
  """a limited exponent/to-the-power-of function, for safety reasons"""
460
453
 
461
454
  if abs(a) > MAX_POWER or abs(b) > MAX_POWER:
462
- raise NumberTooHigh("Sorry! I don't want to evaluate {0} ** {1}".format(a, b))
455
+ raise NumberTooHigh(f"Sorry! I don't want to evaluate {a} ** {b}")
463
456
  return a**b
464
457
 
465
458
 
@@ -477,25 +470,22 @@ def safe_mult(a, b): # pylint: disable=invalid-name
477
470
  def safe_add(a, b): # pylint: disable=invalid-name
478
471
  """iterable length limit again"""
479
472
 
480
- if hasattr(a, "__len__") and hasattr(b, "__len__"):
481
- if len(a) + len(b) > MAX_STRING_LENGTH:
482
- raise IterableTooLong(
483
- "Sorry, adding those two together would make something too long."
484
- )
473
+ if hasattr(a, "__len__") and hasattr(b, "__len__") and len(a) + len(b) > MAX_STRING_LENGTH:
474
+ raise IterableTooLong("Sorry, adding those two together would make something too long.")
485
475
  return a + b
486
476
 
487
477
 
488
478
  def safe_rshift(a, b): # pylint: disable=invalid-name
489
479
  """rshift, but with input limits"""
490
480
  if abs(b) > MAX_SHIFT or abs(a) > MAX_SHIFT_BASE:
491
- raise NumberTooHigh("Sorry! I don't want to evaluate {0} >> {1}".format(a, b))
481
+ raise NumberTooHigh(f"Sorry! I don't want to evaluate {a} >> {b}")
492
482
  return a >> b
493
483
 
494
484
 
495
485
  def safe_lshift(a, b): # pylint: disable=invalid-name
496
486
  """lshift, but with input limits"""
497
487
  if abs(b) > MAX_SHIFT or abs(a) > MAX_SHIFT_BASE:
498
- raise NumberTooHigh("Sorry! I don't want to evaluate {0} << {1}".format(a, b))
488
+ raise NumberTooHigh(f"Sorry! I don't want to evaluate {a} << {b}")
499
489
  return a << b
500
490
 
501
491
 
@@ -548,7 +538,7 @@ ATTR_INDEX_FALLBACK = True
548
538
  # And the actual evaluator:
549
539
 
550
540
 
551
- class SimpleEval(object): # pylint: disable=too-few-public-methods
541
+ class SimpleEval: # pylint: disable=too-few-public-methods
552
542
  """A very simple expression parser.
553
543
  >>> s = SimpleEval()
554
544
  >>> s.eval("20 + 30 - ( 10 * 5)")
@@ -617,7 +607,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
617
607
 
618
608
  for f in self.functions.values():
619
609
  if f in DISALLOW_FUNCTIONS:
620
- raise FeatureNotAvailable("This function {} is a really bad idea.".format(f))
610
+ raise FeatureNotAvailable(f"This function {f} is a really bad idea.")
621
611
 
622
612
  def __del__(self):
623
613
  self.nodes = None
@@ -628,21 +618,25 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
628
618
  Raises FeatureNotAvailable if forbidden content found.
629
619
  ModuleWrapper instances are allowed (explicit opt-in to module access).
630
620
  """
621
+ # Fast path: primitive scalars are always safe (most common case)
622
+ if type(item) in _PRIMITIVE_TYPES:
623
+ return
624
+
631
625
  # Allow ModuleWrapper (explicit opt-in to module access)
632
626
  if isinstance(item, ModuleWrapper):
633
627
  return
634
628
 
635
629
  if isinstance(item, types.ModuleType):
636
630
  raise FeatureNotAvailable("Sorry, modules are not allowed")
637
- if is_hashable(item) and item in DISALLOW_FUNCTIONS:
638
- raise FeatureNotAvailable("This function is forbidden")
639
631
 
640
- if isinstance(item, (list, tuple)):
632
+ if isinstance(item, (list, tuple, set, frozenset)):
641
633
  for element in item:
642
634
  self._check_disallowed_items(element)
643
635
  elif isinstance(item, dict):
644
636
  for value in item.values():
645
637
  self._check_disallowed_items(value)
638
+ elif callable(item) and item in DISALLOW_FUNCTIONS:
639
+ raise FeatureNotAvailable("This function is forbidden")
646
640
 
647
641
  @staticmethod
648
642
  def parse(expr):
@@ -654,7 +648,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
654
648
  raise InvalidExpression("Sorry, cannot evaluate empty string")
655
649
  if len(parsed.body) > 1:
656
650
  warnings.warn(
657
- "'{}' contains multiple expressions. Only the first will be used.".format(expr),
651
+ f"'{expr}' contains multiple expressions. Only the first will be used.",
658
652
  MultipleExpressions,
659
653
  )
660
654
  return parsed.body[0]
@@ -675,7 +669,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
675
669
  handler = self.nodes[type(node)]
676
670
  except KeyError:
677
671
  raise FeatureNotAvailable(
678
- "Sorry, {0} is not available in this evaluator".format(type(node).__name__)
672
+ f"Sorry, {type(node).__name__} is not available in this evaluator"
679
673
  )
680
674
 
681
675
  result = handler(node)
@@ -687,13 +681,13 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
687
681
 
688
682
  def _eval_assign(self, node):
689
683
  warnings.warn(
690
- "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted
684
+ f"Assignment ({self.expr}) attempted, but this is ignored", AssignmentAttempted
691
685
  )
692
686
  return self._eval(node.value)
693
687
 
694
688
  def _eval_aug_assign(self, node):
695
689
  warnings.warn(
696
- "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted
690
+ f"Assignment ({self.expr}) attempted, but this is ignored", AssignmentAttempted
697
691
  )
698
692
  return self._eval(node.value)
699
693
 
@@ -709,9 +703,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
709
703
  def _eval_str(node):
710
704
  if len(node.s) > MAX_STRING_LENGTH:
711
705
  raise IterableTooLong(
712
- "String Literal in statement is too long! ({0}, when {1} is max)".format(
713
- len(node.s), MAX_STRING_LENGTH
714
- )
706
+ f"String Literal in statement is too long! ({len(node.s)}, when {MAX_STRING_LENGTH} is max)"
715
707
  )
716
708
  return node.s
717
709
 
@@ -719,9 +711,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
719
711
  def _eval_constant(node):
720
712
  if hasattr(node.value, "__len__") and len(node.value) > MAX_STRING_LENGTH:
721
713
  raise IterableTooLong(
722
- "Literal in statement is too long! ({0}, when {1} is max)".format(
723
- len(node.value), MAX_STRING_LENGTH
724
- )
714
+ f"Literal in statement is too long! ({len(node.value)}, when {MAX_STRING_LENGTH} is max)"
725
715
  )
726
716
  return node.value
727
717
 
@@ -807,9 +797,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
807
797
  pass
808
798
  elif not hasattr(self.names, "__getitem__"):
809
799
  raise InvalidExpression(
810
- 'Trying to use name (variable) "{0}" when no "names" defined for evaluator'.format(
811
- node.id
812
- )
800
+ f'Trying to use name (variable) "{node.id}" when no "names" defined for evaluator'
813
801
  )
814
802
 
815
803
  if node.id in self.functions:
@@ -832,12 +820,10 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
832
820
  raise FeatureNotAvailable(
833
821
  "Sorry, access to __attributes "
834
822
  " or func_ attributes is not available. "
835
- "({0})".format(node.attr)
823
+ f"({node.attr})"
836
824
  )
837
825
  if node.attr in DISALLOW_METHODS:
838
- raise FeatureNotAvailable(
839
- "Sorry, this method is not available. ({0})".format(node.attr)
840
- )
826
+ raise FeatureNotAvailable(f"Sorry, this method is not available. ({node.attr})")
841
827
 
842
828
  # Evaluate "node" - the thing that we're trying to access an attr of first:
843
829
  node_evaluated = self._eval(node.value)
@@ -875,7 +861,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
875
861
  if item is not _ATTR_NOT_FOUND:
876
862
  if isinstance(item, types.ModuleType):
877
863
  raise FeatureNotAvailable("Sorry, modules are not allowed in attribute access")
878
- if is_hashable(item) and item in DISALLOW_FUNCTIONS:
864
+ if callable(item) and item in DISALLOW_FUNCTIONS:
879
865
  raise FeatureNotAvailable("This function is forbidden")
880
866
  return item
881
867
 
@@ -921,7 +907,7 @@ class EvalWithCompoundTypes(SimpleEval):
921
907
  _max_count = 0
922
908
 
923
909
  def __init__(self, operators=None, functions=None, names=None, allowed_attrs=None):
924
- super(EvalWithCompoundTypes, self).__init__(operators, functions, names, allowed_attrs)
910
+ super().__init__(operators, functions, names, allowed_attrs)
925
911
 
926
912
  self.functions.update(list=list, tuple=tuple, dict=dict, set=set)
927
913
 
@@ -940,7 +926,7 @@ class EvalWithCompoundTypes(SimpleEval):
940
926
  def eval(self, expr, previously_parsed=None):
941
927
  # reset _max_count for each eval run
942
928
  self._max_count = 0
943
- return super(EvalWithCompoundTypes, self).eval(expr, previously_parsed)
929
+ return super().eval(expr, previously_parsed)
944
930
 
945
931
  def _eval_dict(self, node):
946
932
  result = {}
@@ -969,7 +955,7 @@ class EvalWithCompoundTypes(SimpleEval):
969
955
  return tuple(self._eval(x) for x in node.elts)
970
956
 
971
957
  def _eval_set(self, node):
972
- return set(self._eval(x) for x in node.elts)
958
+ return {self._eval(x) for x in node.elts}
973
959
 
974
960
  def _eval_comprehension(self, node):
975
961
  if isinstance(node, ast.DictComp):
File without changes
@@ -0,0 +1,18 @@
1
+ import unittest
2
+
3
+ from simpleeval import (
4
+ SimpleEval,
5
+ )
6
+
7
+
8
+ class DRYTest(unittest.TestCase):
9
+ """Stuff we need to do every test, let's do here instead..
10
+ Don't Repeat Yourself."""
11
+
12
+ def setUp(self):
13
+ """initialize a SimpleEval"""
14
+ self.s = SimpleEval()
15
+
16
+ def t(self, expr, should_be): # pylint: disable=invalid-name
17
+ """test an evaluation of an expression against an expected answer"""
18
+ return self.assertEqual(self.s.eval(expr), should_be)
@@ -0,0 +1,76 @@
1
+ import simpleeval
2
+ from simpleeval import BASIC_ALLOWED_ATTRS, FeatureNotAvailable, simple_eval
3
+
4
+ from .base import DRYTest
5
+
6
+
7
+ class TestAllowedAttributes(DRYTest):
8
+ def setUp(self):
9
+ self.saved_disallow_methods = simpleeval.DISALLOW_METHODS
10
+ simpleeval.DISALLOW_METHODS = []
11
+ super().setUp()
12
+
13
+ def tearDown(self) -> None:
14
+ simpleeval.DISALLOW_METHODS = self.saved_disallow_methods
15
+ return super().tearDown()
16
+
17
+ def test_allowed_attrs_(self):
18
+ self.s.allowed_attrs = BASIC_ALLOWED_ATTRS
19
+ self.t("5 + 5", 10)
20
+ self.t('" hello ".strip()', "hello")
21
+
22
+ def test_allowed_extra_attr(self):
23
+ class Foo:
24
+ def bar(self):
25
+ return 42
26
+
27
+ assert Foo().bar() == 42
28
+
29
+ extended_attrs = BASIC_ALLOWED_ATTRS.copy()
30
+ extended_attrs[Foo] = {"bar"}
31
+
32
+ simple_eval("foo.bar()", names={"foo": Foo()}, allowed_attrs=extended_attrs)
33
+
34
+ def test_disallowed_extra_attr(self):
35
+ class Foo:
36
+ bar = 42
37
+ hidden = 100
38
+
39
+ assert Foo().bar == 42
40
+
41
+ extended_attrs = BASIC_ALLOWED_ATTRS.copy()
42
+ extended_attrs[Foo] = {"bar"}
43
+
44
+ self.assertEqual(
45
+ simple_eval("foo.bar", names={"foo": Foo()}, allowed_attrs=extended_attrs), 42
46
+ )
47
+ with self.assertRaisesRegex(FeatureNotAvailable, r".*'\.hidden' access not allowed.*"):
48
+ self.assertEqual(
49
+ simple_eval("foo.hidden", names={"foo": Foo()}, allowed_attrs=extended_attrs), 42
50
+ )
51
+
52
+ def test_disallowed_types(self):
53
+ class Foo:
54
+ bar = 42
55
+
56
+ assert Foo().bar == 42
57
+
58
+ with self.assertRaises(FeatureNotAvailable):
59
+ simple_eval("foo.bar", names={"foo": Foo()}, allowed_attrs=BASIC_ALLOWED_ATTRS)
60
+
61
+ def test_breakout_via_generator(self):
62
+ # Thanks decorator-factory
63
+ class Foo:
64
+ def bar(self):
65
+ yield "Hello, world!"
66
+
67
+ # Test the generator does work - also adds the `yield` to codecov...
68
+ assert list(Foo().bar()) == ["Hello, world!"]
69
+
70
+ evil = "foo.bar().gi_frame.f_globals['__builtins__'].exec('raise RuntimeError(\"Oh no\")')"
71
+
72
+ extended_attrs = BASIC_ALLOWED_ATTRS.copy()
73
+ extended_attrs[Foo] = {"bar"}
74
+
75
+ with self.assertRaisesRegex(FeatureNotAvailable, r".*attempted to access `\.gi_frame`.*"):
76
+ simple_eval(evil, names={"foo": Foo()}, allowed_attrs=extended_attrs)