simpleeval 1.0.7__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 (31) hide show
  1. {simpleeval-1.0.7 → simpleeval-1.0.8}/PKG-INFO +5 -3
  2. {simpleeval-1.0.7 → simpleeval-1.0.8}/README.rst +4 -2
  3. {simpleeval-1.0.7 → simpleeval-1.0.8}/pyproject.toml +24 -5
  4. {simpleeval-1.0.7 → simpleeval-1.0.8}/simpleeval.py +39 -55
  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.7/test_simpleeval.py +0 -1959
  30. {simpleeval-1.0.7 → simpleeval-1.0.8}/.gitignore +0 -0
  31. {simpleeval-1.0.7 → 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.7
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.7"
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.
@@ -65,6 +65,9 @@ Contributors:
65
65
  - decorator-factory <decorator-factory@protonmail.com> More security fixes
66
66
  - lkruitwagen (Lucas Kruitwagen) Adding support for dict comprehensions
67
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
68
71
 
69
72
  -------------------------------------
70
73
  Basic Usage:
@@ -104,6 +107,8 @@ well:
104
107
 
105
108
  """
106
109
 
110
+ from __future__ import annotations
111
+
107
112
  import ast
108
113
  import operator as op
109
114
  import os
@@ -111,7 +116,6 @@ import sys
111
116
  import types
112
117
  import warnings
113
118
  from random import random
114
- from typing import Type, Dict, Set, Union
115
119
 
116
120
  ########################################
117
121
  # Module wide 'globals'
@@ -162,6 +166,10 @@ DISALLOW_FUNCTIONS = {
162
166
  locals,
163
167
  os.popen,
164
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,
165
173
  }
166
174
  if hasattr(__builtins__, "help") or (
167
175
  hasattr(__builtins__, "__contains__") and "help" in __builtins__ # type: ignore
@@ -169,9 +177,12 @@ if hasattr(__builtins__, "help") or (
169
177
  # PyInstaller environment doesn't include this module.
170
178
  DISALLOW_FUNCTIONS.add(help)
171
179
 
180
+ if hasattr(op, "call"):
181
+ DISALLOW_FUNCTIONS.add(op.call)
182
+
172
183
  # Opt-in type safety experiment. Will be opt-out in 2.x
173
184
 
174
- BASIC_ALLOWED_ATTRS: Dict[Union[Type, None], Set] = {
185
+ BASIC_ALLOWED_ATTRS: dict[type | None, set] = {
175
186
  int: {
176
187
  "as_integer_ratio",
177
188
  "bit_length",
@@ -313,17 +324,13 @@ class TypeNotSpecified(Exception):
313
324
  class InvalidExpression(Exception):
314
325
  """Generic Exception"""
315
326
 
316
- pass
317
-
318
327
 
319
328
  class FunctionNotDefined(InvalidExpression):
320
329
  """sorry! That function isn't defined!"""
321
330
 
322
331
  def __init__(self, func_name, expression):
323
- self.message = "Function '{0}' not defined, for expression '{1}'.".format(
324
- func_name, expression
325
- )
326
- 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.
327
334
  self.expression = expression
328
335
 
329
336
  super(InvalidExpression, self).__init__(self.message)
@@ -334,7 +341,7 @@ class NameNotDefined(InvalidExpression):
334
341
 
335
342
  def __init__(self, name, expression):
336
343
  self.name = name
337
- self.message = "'{0}' is not defined for expression '{1}'".format(name, expression)
344
+ self.message = f"'{name}' is not defined for expression '{expression}'"
338
345
  self.expression = expression
339
346
 
340
347
  super(InvalidExpression, self).__init__(self.message)
@@ -344,9 +351,7 @@ class AttributeDoesNotExist(InvalidExpression):
344
351
  """attribute does not exist"""
345
352
 
346
353
  def __init__(self, attr, expression):
347
- self.message = "Attribute '{0}' does not exist in expression '{1}'".format(
348
- attr, expression
349
- )
354
+ self.message = f"Attribute '{attr}' does not exist in expression '{expression}'"
350
355
  self.attr = attr
351
356
  self.expression = expression
352
357
 
@@ -357,7 +362,7 @@ class OperatorNotDefined(InvalidExpression):
357
362
  """operator does not exist"""
358
363
 
359
364
  def __init__(self, attr, expression):
360
- 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}'"
361
366
  self.attr = attr
362
367
  self.expression = expression
363
368
 
@@ -367,33 +372,23 @@ class OperatorNotDefined(InvalidExpression):
367
372
  class FeatureNotAvailable(InvalidExpression):
368
373
  """What you're trying to do is not allowed."""
369
374
 
370
- pass
371
-
372
375
 
373
376
  class NumberTooHigh(InvalidExpression):
374
377
  """Sorry! That number is too high. I don't want to spend the
375
378
  next 10 years evaluating this expression!"""
376
379
 
377
- pass
378
-
379
380
 
380
381
  class IterableTooLong(InvalidExpression):
381
382
  """That iterable is **way** too long, baby."""
382
383
 
383
- pass
384
-
385
384
 
386
385
  class AssignmentAttempted(UserWarning):
387
386
  """Assignment not allowed in SimpleEval"""
388
387
 
389
- pass
390
-
391
388
 
392
389
  class MultipleExpressions(UserWarning):
393
390
  """Only the first expression parsed will be used"""
394
391
 
395
- pass
396
-
397
392
 
398
393
  # Sentinal used during attr access
399
394
  _ATTR_NOT_FOUND = object()
@@ -457,7 +452,7 @@ def safe_power(a, b): # pylint: disable=invalid-name
457
452
  """a limited exponent/to-the-power-of function, for safety reasons"""
458
453
 
459
454
  if abs(a) > MAX_POWER or abs(b) > MAX_POWER:
460
- 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}")
461
456
  return a**b
462
457
 
463
458
 
@@ -475,25 +470,22 @@ def safe_mult(a, b): # pylint: disable=invalid-name
475
470
  def safe_add(a, b): # pylint: disable=invalid-name
476
471
  """iterable length limit again"""
477
472
 
478
- if hasattr(a, "__len__") and hasattr(b, "__len__"):
479
- if len(a) + len(b) > MAX_STRING_LENGTH:
480
- raise IterableTooLong(
481
- "Sorry, adding those two together would make something too long."
482
- )
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.")
483
475
  return a + b
484
476
 
485
477
 
486
478
  def safe_rshift(a, b): # pylint: disable=invalid-name
487
479
  """rshift, but with input limits"""
488
480
  if abs(b) > MAX_SHIFT or abs(a) > MAX_SHIFT_BASE:
489
- 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}")
490
482
  return a >> b
491
483
 
492
484
 
493
485
  def safe_lshift(a, b): # pylint: disable=invalid-name
494
486
  """lshift, but with input limits"""
495
487
  if abs(b) > MAX_SHIFT or abs(a) > MAX_SHIFT_BASE:
496
- 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}")
497
489
  return a << b
498
490
 
499
491
 
@@ -546,7 +538,7 @@ ATTR_INDEX_FALLBACK = True
546
538
  # And the actual evaluator:
547
539
 
548
540
 
549
- class SimpleEval(object): # pylint: disable=too-few-public-methods
541
+ class SimpleEval: # pylint: disable=too-few-public-methods
550
542
  """A very simple expression parser.
551
543
  >>> s = SimpleEval()
552
544
  >>> s.eval("20 + 30 - ( 10 * 5)")
@@ -615,7 +607,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
615
607
 
616
608
  for f in self.functions.values():
617
609
  if f in DISALLOW_FUNCTIONS:
618
- raise FeatureNotAvailable("This function {} is a really bad idea.".format(f))
610
+ raise FeatureNotAvailable(f"This function {f} is a really bad idea.")
619
611
 
620
612
  def __del__(self):
621
613
  self.nodes = None
@@ -637,7 +629,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
637
629
  if isinstance(item, types.ModuleType):
638
630
  raise FeatureNotAvailable("Sorry, modules are not allowed")
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):
@@ -656,7 +648,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
656
648
  raise InvalidExpression("Sorry, cannot evaluate empty string")
657
649
  if len(parsed.body) > 1:
658
650
  warnings.warn(
659
- "'{}' contains multiple expressions. Only the first will be used.".format(expr),
651
+ f"'{expr}' contains multiple expressions. Only the first will be used.",
660
652
  MultipleExpressions,
661
653
  )
662
654
  return parsed.body[0]
@@ -677,7 +669,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
677
669
  handler = self.nodes[type(node)]
678
670
  except KeyError:
679
671
  raise FeatureNotAvailable(
680
- "Sorry, {0} is not available in this evaluator".format(type(node).__name__)
672
+ f"Sorry, {type(node).__name__} is not available in this evaluator"
681
673
  )
682
674
 
683
675
  result = handler(node)
@@ -689,13 +681,13 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
689
681
 
690
682
  def _eval_assign(self, node):
691
683
  warnings.warn(
692
- "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted
684
+ f"Assignment ({self.expr}) attempted, but this is ignored", AssignmentAttempted
693
685
  )
694
686
  return self._eval(node.value)
695
687
 
696
688
  def _eval_aug_assign(self, node):
697
689
  warnings.warn(
698
- "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted
690
+ f"Assignment ({self.expr}) attempted, but this is ignored", AssignmentAttempted
699
691
  )
700
692
  return self._eval(node.value)
701
693
 
@@ -711,9 +703,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
711
703
  def _eval_str(node):
712
704
  if len(node.s) > MAX_STRING_LENGTH:
713
705
  raise IterableTooLong(
714
- "String Literal in statement is too long! ({0}, when {1} is max)".format(
715
- len(node.s), MAX_STRING_LENGTH
716
- )
706
+ f"String Literal in statement is too long! ({len(node.s)}, when {MAX_STRING_LENGTH} is max)"
717
707
  )
718
708
  return node.s
719
709
 
@@ -721,9 +711,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
721
711
  def _eval_constant(node):
722
712
  if hasattr(node.value, "__len__") and len(node.value) > MAX_STRING_LENGTH:
723
713
  raise IterableTooLong(
724
- "Literal in statement is too long! ({0}, when {1} is max)".format(
725
- len(node.value), MAX_STRING_LENGTH
726
- )
714
+ f"Literal in statement is too long! ({len(node.value)}, when {MAX_STRING_LENGTH} is max)"
727
715
  )
728
716
  return node.value
729
717
 
@@ -809,9 +797,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
809
797
  pass
810
798
  elif not hasattr(self.names, "__getitem__"):
811
799
  raise InvalidExpression(
812
- 'Trying to use name (variable) "{0}" when no "names" defined for evaluator'.format(
813
- node.id
814
- )
800
+ f'Trying to use name (variable) "{node.id}" when no "names" defined for evaluator'
815
801
  )
816
802
 
817
803
  if node.id in self.functions:
@@ -834,12 +820,10 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
834
820
  raise FeatureNotAvailable(
835
821
  "Sorry, access to __attributes "
836
822
  " or func_ attributes is not available. "
837
- "({0})".format(node.attr)
823
+ f"({node.attr})"
838
824
  )
839
825
  if node.attr in DISALLOW_METHODS:
840
- raise FeatureNotAvailable(
841
- "Sorry, this method is not available. ({0})".format(node.attr)
842
- )
826
+ raise FeatureNotAvailable(f"Sorry, this method is not available. ({node.attr})")
843
827
 
844
828
  # Evaluate "node" - the thing that we're trying to access an attr of first:
845
829
  node_evaluated = self._eval(node.value)
@@ -923,7 +907,7 @@ class EvalWithCompoundTypes(SimpleEval):
923
907
  _max_count = 0
924
908
 
925
909
  def __init__(self, operators=None, functions=None, names=None, allowed_attrs=None):
926
- super(EvalWithCompoundTypes, self).__init__(operators, functions, names, allowed_attrs)
910
+ super().__init__(operators, functions, names, allowed_attrs)
927
911
 
928
912
  self.functions.update(list=list, tuple=tuple, dict=dict, set=set)
929
913
 
@@ -942,7 +926,7 @@ class EvalWithCompoundTypes(SimpleEval):
942
926
  def eval(self, expr, previously_parsed=None):
943
927
  # reset _max_count for each eval run
944
928
  self._max_count = 0
945
- return super(EvalWithCompoundTypes, self).eval(expr, previously_parsed)
929
+ return super().eval(expr, previously_parsed)
946
930
 
947
931
  def _eval_dict(self, node):
948
932
  result = {}
@@ -971,7 +955,7 @@ class EvalWithCompoundTypes(SimpleEval):
971
955
  return tuple(self._eval(x) for x in node.elts)
972
956
 
973
957
  def _eval_set(self, node):
974
- return set(self._eval(x) for x in node.elts)
958
+ return {self._eval(x) for x in node.elts}
975
959
 
976
960
  def _eval_comprehension(self, node):
977
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)