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.
- {simpleeval-1.0.7 → simpleeval-1.0.8}/PKG-INFO +5 -3
- {simpleeval-1.0.7 → simpleeval-1.0.8}/README.rst +4 -2
- {simpleeval-1.0.7 → simpleeval-1.0.8}/pyproject.toml +24 -5
- {simpleeval-1.0.7 → simpleeval-1.0.8}/simpleeval.py +39 -55
- simpleeval-1.0.8/tests/__init__.py +0 -0
- simpleeval-1.0.8/tests/base.py +18 -0
- simpleeval-1.0.8/tests/test_allowed_attributes.py +76 -0
- simpleeval-1.0.8/tests/test_basics.py +168 -0
- simpleeval-1.0.8/tests/test_compound_types.py +117 -0
- simpleeval-1.0.8/tests/test_comprehensions.py +103 -0
- simpleeval-1.0.8/tests/test_deliberately_missed_features.py +17 -0
- simpleeval-1.0.8/tests/test_disallowed_functions.py +61 -0
- simpleeval-1.0.8/tests/test_evaluator.py +48 -0
- simpleeval-1.0.8/tests/test_exceptions.py +41 -0
- simpleeval-1.0.8/tests/test_extending_the_class.py +28 -0
- simpleeval-1.0.8/tests/test_functions.py +123 -0
- simpleeval-1.0.8/tests/test_get_item_unhappy_paths.py +33 -0
- simpleeval-1.0.8/tests/test_method_chaining.py +32 -0
- simpleeval-1.0.8/tests/test_module_wrapper.py +174 -0
- simpleeval-1.0.8/tests/test_names.py +230 -0
- simpleeval-1.0.8/tests/test_no_entries.py +32 -0
- simpleeval-1.0.8/tests/test_operators.py +7 -0
- simpleeval-1.0.8/tests/test_reference_cleanup.py +35 -0
- simpleeval-1.0.8/tests/test_security.py +637 -0
- simpleeval-1.0.8/tests/test_short_circuiting.py +33 -0
- simpleeval-1.0.8/tests/test_simple_eval_function.py +16 -0
- simpleeval-1.0.8/tests/test_unusual_comparisons.py +51 -0
- simpleeval-1.0.8/tests/test_whitespace.py +23 -0
- simpleeval-1.0.7/test_simpleeval.py +0 -1959
- {simpleeval-1.0.7 → simpleeval-1.0.8}/.gitignore +0 -0
- {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.
|
|
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 ``
|
|
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
|
|
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 ``
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
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:
|
|
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 '{
|
|
324
|
-
|
|
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 = "'{
|
|
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 '{
|
|
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 '{
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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."
|
|
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."
|
|
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, {
|
|
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"
|
|
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"
|
|
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! ({
|
|
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! ({
|
|
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) "{
|
|
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
|
-
"({
|
|
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(
|
|
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(
|
|
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
|
|
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)
|