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.
- {simpleeval-1.0.6 → simpleeval-1.0.8}/PKG-INFO +5 -3
- {simpleeval-1.0.6 → simpleeval-1.0.8}/README.rst +4 -2
- {simpleeval-1.0.6 → simpleeval-1.0.8}/pyproject.toml +24 -5
- {simpleeval-1.0.6 → simpleeval-1.0.8}/simpleeval.py +51 -65
- 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.6/.v2/lib/python3.13/site-packages/docutils/parsers/rst/include/README.rst +0 -17
- simpleeval-1.0.6/.v2/lib/python3.13/site-packages/docutils/writers/s5_html/themes/README.rst +0 -6
- simpleeval-1.0.6/.v2/lib/python3.13/site-packages/pip/_vendor/README.rst +0 -180
- simpleeval-1.0.6/.v314/lib/python3.14/site-packages/simpleeval-1.0.3.dist-info/licenses/LICENCE +0 -21
- simpleeval-1.0.6/.v314/lib/python3.14/site-packages/simpleeval.py +0 -768
- simpleeval-1.0.6/test_simpleeval.py +0 -1959
- {simpleeval-1.0.6 → simpleeval-1.0.8}/.gitignore +0 -0
- {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.
|
|
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.
|
|
@@ -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
|
-
|
|
140
|
-
|
|
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:
|
|
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 '{
|
|
326
|
-
|
|
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 = "'{
|
|
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 '{
|
|
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 '{
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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."
|
|
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."
|
|
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, {
|
|
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"
|
|
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"
|
|
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! ({
|
|
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! ({
|
|
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) "{
|
|
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
|
-
"({
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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)
|