summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--python-evalidate.spec516
-rw-r--r--sources2
3 files changed, 206 insertions, 313 deletions
diff --git a/.gitignore b/.gitignore
index 63ef0df..924a334 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/evalidate-1.0.2.tar.gz
+/evalidate-1.0.3.tar.gz
diff --git a/python-evalidate.spec b/python-evalidate.spec
index 23fb839..5388288 100644
--- a/python-evalidate.spec
+++ b/python-evalidate.spec
@@ -1,11 +1,11 @@
%global _empty_manifest_terminate_build 0
Name: python-evalidate
-Version: 1.0.2
+Version: 1.0.3
Release: 1
Summary: Validation and secure evaluation of untrusted python expressions
License: MIT
URL: http://github.com/yaroslaff/evalidate
-Source0: https://mirrors.nju.edu.cn/pypi/web/packages/a8/d1/c2988bc432070280af5453cb65849c6b9689ba032fab0d4fc1b3b0a87997/evalidate-1.0.2.tar.gz
+Source0: https://mirrors.aliyun.com/pypi/web/packages/96/ca/060aeb3b919dd1b91fa11546cb6552b1c61bd8c30971f2e4594805870dfb/evalidate-1.0.3.tar.gz
BuildArch: noarch
@@ -14,13 +14,9 @@ BuildArch: noarch
Evalidate is simple python module for safe eval()'uating user-supplied (possible malicious) logical expressions in python syntax.
## Purpose
-Originally it's developed for filtering (performing boolean expressions) complex data structures e.g. find employees:
+Originally it's developed for filtering complex data structures e.g.
-```python
-person.age>30 and person.salary>5000 or "Jack" in person.children
-```
-
-Or find cheap smartphones:
+Find cheap smartphones available for sale:
```python
category="smartphones" and price<300 and stock>0
```
@@ -30,6 +26,8 @@ But also, it can be used for other expressions, e.g. arithmetical, like
a+b-100
```
+Evalidate tries to be both secure and fast (when properly used).
+
## Install
```shell
@@ -75,7 +73,7 @@ inherit from base exception class `EvalException`.
## Configure validation
Evalidate is very flexible, depending on parameters, same code can either pass validation or raise exception.
-### safenodes and addnodes
+### Safenodes and addnodes
Evalidate has built-in set of python operations, which are considered 'safe' (from author point of view). Code is considered valid only if all of it's operations are in this list. You can override this list by adding argument `safenodes` like:
```python
result = evalidate.safeeval(src, context, safenodes=['Expression','BinOp','Num','Add'])
@@ -101,7 +99,7 @@ and will raise runtime exception: `ERROR: Runtime error (OverflowError): repeate
### Allowing function calls
Evalidate does not allow any function calls by default:
-```
+```python
>>> from evalidate import safeeval, EvalException
>>> try:
... safeeval('int(1)')
@@ -112,12 +110,12 @@ Operation type Call is not allowed
```
To enable int() function, need to allow 'Call' node and add this function to list of allowed function:
-```
+```python
>>> evalidate.safeeval('int(1)', addnodes=['Call'], funcs=['int'])
1
```
Attempt to call other functions will fail (because it's not in funcs list):
-```
+```python
evalidate.safeeval('1+round(2)', addnodes=['Call'], funcs=['int'])
```
This will throw `ValidationException`.
@@ -126,7 +124,7 @@ Attributes calls (`"aaa".startswith("a")`) could be allowed (with proper `addnod
other indirect function calls (like: `__builtins__['eval']("print(1)")`) are not allowed,
-### accessing attributes (attrs parameter), data as classes
+### Accessing attributes (attrs parameter); data as classes
If data represented as object with attributes (not as dictionary) we have to add 'Attribute' to safe nodes. Increase salary for person for 200, and additionaly 25 for each year (s)he works in company.
@@ -149,15 +147,15 @@ except EvalException as e:
print("ERR:",e)
```
-### calling attributes
+### Calling attributes
This code will not work:
-~~~
+~~~python
safeeval('"abc".startswith("a")')
~~~
Because: `evalidate.ValidationException: Operation type Call is not allowed`
To make it working:
-~~~
+~~~python
print(safeeval('"abc".startswith("a")', addnodes=['Call', 'Attribute'], attrs=['startswith']))
~~~
@@ -222,18 +220,18 @@ evalidate uses [ast.parse()](https://docs.python.org/3/library/ast.html#ast.pars
In my test, works well with 200 nested int(): `int(int(.... int(1)...))` but not with 201. Source code is 1000+ characters. But even if evalidate will get such code, it will just raise `CompilationException`.
-### evalidate.security.test_security
+### evalidate.security.test_security()
Evalidate is very flexible and it's possible to shoot yourself in foot if you will try hard. `test_security()` checks your configuration (addnodes/safenodes, funcs, attrs) against given list of possible attack code or against built-in list of attacks. `test_security()` returns True if everything is OK (all attacks raised ValidationException) or False if something passed.
This code will never print (I hope).
-~~~
+~~~python
from evalidate.security import test_security
test_security() or print("default rules are vulnerable!")
~~~
But this will fail because nodes/funcs leads to successful validation for attack (suppose you do not want anyone to call `int()`)
-~~~
+~~~python
from evalidate.security import test_security
attacks = ['int(1)']
@@ -253,59 +251,49 @@ int(1)
-## Examples
+## Example
### Filtering by user-supplied condition ###
-```python
-from evalidate import safeeval, EvalException
-depot = [
- {
- 'book': 'Sirens of Titan',
- 'price': 12,
- 'stock': 4
- },
- {
- 'book': 'Gone Girl',
- 'price': 9.8,
- 'stock': 0
- },
- {
- 'book': 'Choke',
- 'price': 14,
- 'stock': 2
- },
- {
- 'book': 'Pulp',
- 'price': 7.45,
- 'stock': 4
- }
-]
-
-#src='stock==0' # books out of stock
-src='stock>0 and price>8' # expensive book available for sale
-
-for book in depot:
- try:
- result = safeeval(src,book)
- if result:
- print(book)
- except EvalException as e:
- print("ERR:", e)
-```
+This is code of `examples/products.py`. Expression is validated and compiled once and executed (as byte-code, very fast) many times, so filtering is both fast and secure.
+
+
+~~~python
+#!/usr/bin/env python3
-With first src line ('stock==0') it gives:
+import requests
+from evalidate import evalidate, ValidationException, CompilationException
+import json
+import sys
- {'price': 9.8, 'book': 'Gone Girl', 'stock': 0}
+data = requests.get('https://dummyjson.com/products?limit=100').json()
-With second src line ('stock>0 and price>8') it gives:
+try:
+ src = sys.argv[1]
+except IndexError:
+ src = 'True'
+
+try:
+ node = evalidate(src)
+except (ValidationException, CompilationException) as e:
+ print(e)
+ sys.exit(1)
- {'price': 12, 'book': 'Sirens of Titan', 'stock': 4}
- {'price': 14, 'book': 'Choke', 'stock': 2}
-Note, it uses simple and slow function safeeval(). Each call of safeeval it will parse and validate same source code, and it's not effective. But it's OK if you have small set of elements to check.
+code = compile(node, '<user filter>', 'eval')
-For better example check `examples/products.py` in repo. It uses dataset "products" from https://dummyjson.com/ and it's much more effective on large lists.
+c=0
+for p in data['products']:
+ # print(p)
+ try:
+ r = eval(code, p.copy())
+ if r:
+ print(json.dumps(p, indent=2))
+ c+=1
+ except Exception as e:
+ print("Runtime exception:", e)
+print("# {} products matches".format(c))
+~~~
~~~shell
# print all 100 products
@@ -324,36 +312,6 @@ For better example check `examples/products.py` in repo. It uses dataset "produc
./products.py 'category=="smartphones" and price<300'
~~~
-### Validate, compile and evaluate code
-```python
-import evalidate
-
-def test(src):
- data={'one':1,'two':2}
-
- try:
- node = evalidate.evalidate(src)
- except evalidate.CompilationException:
- print("Bad source code:", repr(src))
- return
- except evalidate.ValidationException:
- print("Dangerous code:", repr(src))
- return
-
- code = compile(node,'<usercode>','eval')
- try:
- result = eval(code,{},data)
- print("result:", result)
- except Exception as e:
- # almost any kind of exception can happen here
- print("Runtime exception:",e)
-
-srclist=['one+two+3', 'one+two+3+os.system("clear")', '', '1/0']
-
-for src in srclist:
- test(src)
-```
-
## Similar projects and benchmark
@@ -363,21 +321,27 @@ While asteval can compute much more complex code (define functions, use python m
- asteval is much slower (evalidate can be used at speed of eval() python bytecode)
- user can provide source code which runs very long time and consumes many resources
+
+[simpleeval](https://github.com/danthedeckie/simpleeval)
+Very similar project, using AST approach too and optimized to re-evaluate pre-parsed expressions. But parsed expressions are stored as more high-level [ast.Expr](https://docs.python.org/3/library/ast.html#ast.Expr) type and this approach is ~10 times slower, while evalidate uses python native `code` type and evaluation itself goes at speed of python eval()
+
+
evalidate is good to run short same code against different data.
## Benchmarking
-We use `evalidate-vs-asteval.py` which is in benchmark/ directory of repository
+We use `evalidate-vs-asteval.py` which is in benchmark/ directory of repository.
+We prepare list of 1 million of products (actually, we take just 100 products sample, but repeat it 10 000 times to get 1 million), and then filter it, finding only specific products on "untrusted user-supplied expression" (`price < 20` in this case)
+
~~~
-$ ./evalidate-vs-asteval.py
-Src: a+b
-Context: {'a': 1, 'b': 2}
-Runs: 100000
-asteval: 3.538s
-asteval (reuse interpreter): 1.232s
-safeeval: 2.384s
-evalidate/compile/eval (reuse compiled code): 0.017s
+Products: 1000000 items
+test_asteval_products(): 25.920s
+test_simpleeval_products(): 1.779s
+test_evalidate_products(): 0.160s
~~~
-0.017s vs 1.232s
+
+As you see, evalidate is almost 10 times faster then simpleeval and both are much faster then asteval.
+
+Maybe my test is not perfectly optimized (I'm not expert with simpleeval/asteval), if you can suggest better filtering sample code (which produces faster result), I will include it. But benchmark code must assume expression as unknown and untrusted.
## Read about eval() risks
@@ -411,13 +375,9 @@ BuildRequires: python3-pip
Evalidate is simple python module for safe eval()'uating user-supplied (possible malicious) logical expressions in python syntax.
## Purpose
-Originally it's developed for filtering (performing boolean expressions) complex data structures e.g. find employees:
-
-```python
-person.age>30 and person.salary>5000 or "Jack" in person.children
-```
+Originally it's developed for filtering complex data structures e.g.
-Or find cheap smartphones:
+Find cheap smartphones available for sale:
```python
category="smartphones" and price<300 and stock>0
```
@@ -427,6 +387,8 @@ But also, it can be used for other expressions, e.g. arithmetical, like
a+b-100
```
+Evalidate tries to be both secure and fast (when properly used).
+
## Install
```shell
@@ -472,7 +434,7 @@ inherit from base exception class `EvalException`.
## Configure validation
Evalidate is very flexible, depending on parameters, same code can either pass validation or raise exception.
-### safenodes and addnodes
+### Safenodes and addnodes
Evalidate has built-in set of python operations, which are considered 'safe' (from author point of view). Code is considered valid only if all of it's operations are in this list. You can override this list by adding argument `safenodes` like:
```python
result = evalidate.safeeval(src, context, safenodes=['Expression','BinOp','Num','Add'])
@@ -498,7 +460,7 @@ and will raise runtime exception: `ERROR: Runtime error (OverflowError): repeate
### Allowing function calls
Evalidate does not allow any function calls by default:
-```
+```python
>>> from evalidate import safeeval, EvalException
>>> try:
... safeeval('int(1)')
@@ -509,12 +471,12 @@ Operation type Call is not allowed
```
To enable int() function, need to allow 'Call' node and add this function to list of allowed function:
-```
+```python
>>> evalidate.safeeval('int(1)', addnodes=['Call'], funcs=['int'])
1
```
Attempt to call other functions will fail (because it's not in funcs list):
-```
+```python
evalidate.safeeval('1+round(2)', addnodes=['Call'], funcs=['int'])
```
This will throw `ValidationException`.
@@ -523,7 +485,7 @@ Attributes calls (`"aaa".startswith("a")`) could be allowed (with proper `addnod
other indirect function calls (like: `__builtins__['eval']("print(1)")`) are not allowed,
-### accessing attributes (attrs parameter), data as classes
+### Accessing attributes (attrs parameter); data as classes
If data represented as object with attributes (not as dictionary) we have to add 'Attribute' to safe nodes. Increase salary for person for 200, and additionaly 25 for each year (s)he works in company.
@@ -546,15 +508,15 @@ except EvalException as e:
print("ERR:",e)
```
-### calling attributes
+### Calling attributes
This code will not work:
-~~~
+~~~python
safeeval('"abc".startswith("a")')
~~~
Because: `evalidate.ValidationException: Operation type Call is not allowed`
To make it working:
-~~~
+~~~python
print(safeeval('"abc".startswith("a")', addnodes=['Call', 'Attribute'], attrs=['startswith']))
~~~
@@ -619,18 +581,18 @@ evalidate uses [ast.parse()](https://docs.python.org/3/library/ast.html#ast.pars
In my test, works well with 200 nested int(): `int(int(.... int(1)...))` but not with 201. Source code is 1000+ characters. But even if evalidate will get such code, it will just raise `CompilationException`.
-### evalidate.security.test_security
+### evalidate.security.test_security()
Evalidate is very flexible and it's possible to shoot yourself in foot if you will try hard. `test_security()` checks your configuration (addnodes/safenodes, funcs, attrs) against given list of possible attack code or against built-in list of attacks. `test_security()` returns True if everything is OK (all attacks raised ValidationException) or False if something passed.
This code will never print (I hope).
-~~~
+~~~python
from evalidate.security import test_security
test_security() or print("default rules are vulnerable!")
~~~
But this will fail because nodes/funcs leads to successful validation for attack (suppose you do not want anyone to call `int()`)
-~~~
+~~~python
from evalidate.security import test_security
attacks = ['int(1)']
@@ -650,59 +612,49 @@ int(1)
-## Examples
+## Example
### Filtering by user-supplied condition ###
-```python
-from evalidate import safeeval, EvalException
-depot = [
- {
- 'book': 'Sirens of Titan',
- 'price': 12,
- 'stock': 4
- },
- {
- 'book': 'Gone Girl',
- 'price': 9.8,
- 'stock': 0
- },
- {
- 'book': 'Choke',
- 'price': 14,
- 'stock': 2
- },
- {
- 'book': 'Pulp',
- 'price': 7.45,
- 'stock': 4
- }
-]
-
-#src='stock==0' # books out of stock
-src='stock>0 and price>8' # expensive book available for sale
-
-for book in depot:
- try:
- result = safeeval(src,book)
- if result:
- print(book)
- except EvalException as e:
- print("ERR:", e)
-```
+This is code of `examples/products.py`. Expression is validated and compiled once and executed (as byte-code, very fast) many times, so filtering is both fast and secure.
+
+
+~~~python
+#!/usr/bin/env python3
+
+import requests
+from evalidate import evalidate, ValidationException, CompilationException
+import json
+import sys
-With first src line ('stock==0') it gives:
+data = requests.get('https://dummyjson.com/products?limit=100').json()
- {'price': 9.8, 'book': 'Gone Girl', 'stock': 0}
+try:
+ src = sys.argv[1]
+except IndexError:
+ src = 'True'
-With second src line ('stock>0 and price>8') it gives:
+try:
+ node = evalidate(src)
+except (ValidationException, CompilationException) as e:
+ print(e)
+ sys.exit(1)
- {'price': 12, 'book': 'Sirens of Titan', 'stock': 4}
- {'price': 14, 'book': 'Choke', 'stock': 2}
-Note, it uses simple and slow function safeeval(). Each call of safeeval it will parse and validate same source code, and it's not effective. But it's OK if you have small set of elements to check.
+code = compile(node, '<user filter>', 'eval')
-For better example check `examples/products.py` in repo. It uses dataset "products" from https://dummyjson.com/ and it's much more effective on large lists.
+c=0
+for p in data['products']:
+ # print(p)
+ try:
+ r = eval(code, p.copy())
+ if r:
+ print(json.dumps(p, indent=2))
+ c+=1
+ except Exception as e:
+ print("Runtime exception:", e)
+print("# {} products matches".format(c))
+~~~
~~~shell
# print all 100 products
@@ -721,36 +673,6 @@ For better example check `examples/products.py` in repo. It uses dataset "produc
./products.py 'category=="smartphones" and price<300'
~~~
-### Validate, compile and evaluate code
-```python
-import evalidate
-
-def test(src):
- data={'one':1,'two':2}
-
- try:
- node = evalidate.evalidate(src)
- except evalidate.CompilationException:
- print("Bad source code:", repr(src))
- return
- except evalidate.ValidationException:
- print("Dangerous code:", repr(src))
- return
-
- code = compile(node,'<usercode>','eval')
- try:
- result = eval(code,{},data)
- print("result:", result)
- except Exception as e:
- # almost any kind of exception can happen here
- print("Runtime exception:",e)
-
-srclist=['one+two+3', 'one+two+3+os.system("clear")', '', '1/0']
-
-for src in srclist:
- test(src)
-```
-
## Similar projects and benchmark
@@ -760,21 +682,27 @@ While asteval can compute much more complex code (define functions, use python m
- asteval is much slower (evalidate can be used at speed of eval() python bytecode)
- user can provide source code which runs very long time and consumes many resources
+
+[simpleeval](https://github.com/danthedeckie/simpleeval)
+Very similar project, using AST approach too and optimized to re-evaluate pre-parsed expressions. But parsed expressions are stored as more high-level [ast.Expr](https://docs.python.org/3/library/ast.html#ast.Expr) type and this approach is ~10 times slower, while evalidate uses python native `code` type and evaluation itself goes at speed of python eval()
+
+
evalidate is good to run short same code against different data.
## Benchmarking
-We use `evalidate-vs-asteval.py` which is in benchmark/ directory of repository
+We use `evalidate-vs-asteval.py` which is in benchmark/ directory of repository.
+We prepare list of 1 million of products (actually, we take just 100 products sample, but repeat it 10 000 times to get 1 million), and then filter it, finding only specific products on "untrusted user-supplied expression" (`price < 20` in this case)
+
~~~
-$ ./evalidate-vs-asteval.py
-Src: a+b
-Context: {'a': 1, 'b': 2}
-Runs: 100000
-asteval: 3.538s
-asteval (reuse interpreter): 1.232s
-safeeval: 2.384s
-evalidate/compile/eval (reuse compiled code): 0.017s
+Products: 1000000 items
+test_asteval_products(): 25.920s
+test_simpleeval_products(): 1.779s
+test_evalidate_products(): 0.160s
~~~
-0.017s vs 1.232s
+
+As you see, evalidate is almost 10 times faster then simpleeval and both are much faster then asteval.
+
+Maybe my test is not perfectly optimized (I'm not expert with simpleeval/asteval), if you can suggest better filtering sample code (which produces faster result), I will include it. But benchmark code must assume expression as unknown and untrusted.
## Read about eval() risks
@@ -805,13 +733,9 @@ Provides: python3-evalidate-doc
Evalidate is simple python module for safe eval()'uating user-supplied (possible malicious) logical expressions in python syntax.
## Purpose
-Originally it's developed for filtering (performing boolean expressions) complex data structures e.g. find employees:
-
-```python
-person.age>30 and person.salary>5000 or "Jack" in person.children
-```
+Originally it's developed for filtering complex data structures e.g.
-Or find cheap smartphones:
+Find cheap smartphones available for sale:
```python
category="smartphones" and price<300 and stock>0
```
@@ -821,6 +745,8 @@ But also, it can be used for other expressions, e.g. arithmetical, like
a+b-100
```
+Evalidate tries to be both secure and fast (when properly used).
+
## Install
```shell
@@ -866,7 +792,7 @@ inherit from base exception class `EvalException`.
## Configure validation
Evalidate is very flexible, depending on parameters, same code can either pass validation or raise exception.
-### safenodes and addnodes
+### Safenodes and addnodes
Evalidate has built-in set of python operations, which are considered 'safe' (from author point of view). Code is considered valid only if all of it's operations are in this list. You can override this list by adding argument `safenodes` like:
```python
result = evalidate.safeeval(src, context, safenodes=['Expression','BinOp','Num','Add'])
@@ -892,7 +818,7 @@ and will raise runtime exception: `ERROR: Runtime error (OverflowError): repeate
### Allowing function calls
Evalidate does not allow any function calls by default:
-```
+```python
>>> from evalidate import safeeval, EvalException
>>> try:
... safeeval('int(1)')
@@ -903,12 +829,12 @@ Operation type Call is not allowed
```
To enable int() function, need to allow 'Call' node and add this function to list of allowed function:
-```
+```python
>>> evalidate.safeeval('int(1)', addnodes=['Call'], funcs=['int'])
1
```
Attempt to call other functions will fail (because it's not in funcs list):
-```
+```python
evalidate.safeeval('1+round(2)', addnodes=['Call'], funcs=['int'])
```
This will throw `ValidationException`.
@@ -917,7 +843,7 @@ Attributes calls (`"aaa".startswith("a")`) could be allowed (with proper `addnod
other indirect function calls (like: `__builtins__['eval']("print(1)")`) are not allowed,
-### accessing attributes (attrs parameter), data as classes
+### Accessing attributes (attrs parameter); data as classes
If data represented as object with attributes (not as dictionary) we have to add 'Attribute' to safe nodes. Increase salary for person for 200, and additionaly 25 for each year (s)he works in company.
@@ -940,15 +866,15 @@ except EvalException as e:
print("ERR:",e)
```
-### calling attributes
+### Calling attributes
This code will not work:
-~~~
+~~~python
safeeval('"abc".startswith("a")')
~~~
Because: `evalidate.ValidationException: Operation type Call is not allowed`
To make it working:
-~~~
+~~~python
print(safeeval('"abc".startswith("a")', addnodes=['Call', 'Attribute'], attrs=['startswith']))
~~~
@@ -1013,18 +939,18 @@ evalidate uses [ast.parse()](https://docs.python.org/3/library/ast.html#ast.pars
In my test, works well with 200 nested int(): `int(int(.... int(1)...))` but not with 201. Source code is 1000+ characters. But even if evalidate will get such code, it will just raise `CompilationException`.
-### evalidate.security.test_security
+### evalidate.security.test_security()
Evalidate is very flexible and it's possible to shoot yourself in foot if you will try hard. `test_security()` checks your configuration (addnodes/safenodes, funcs, attrs) against given list of possible attack code or against built-in list of attacks. `test_security()` returns True if everything is OK (all attacks raised ValidationException) or False if something passed.
This code will never print (I hope).
-~~~
+~~~python
from evalidate.security import test_security
test_security() or print("default rules are vulnerable!")
~~~
But this will fail because nodes/funcs leads to successful validation for attack (suppose you do not want anyone to call `int()`)
-~~~
+~~~python
from evalidate.security import test_security
attacks = ['int(1)']
@@ -1044,59 +970,49 @@ int(1)
-## Examples
+## Example
### Filtering by user-supplied condition ###
-```python
-from evalidate import safeeval, EvalException
-depot = [
- {
- 'book': 'Sirens of Titan',
- 'price': 12,
- 'stock': 4
- },
- {
- 'book': 'Gone Girl',
- 'price': 9.8,
- 'stock': 0
- },
- {
- 'book': 'Choke',
- 'price': 14,
- 'stock': 2
- },
- {
- 'book': 'Pulp',
- 'price': 7.45,
- 'stock': 4
- }
-]
-
-#src='stock==0' # books out of stock
-src='stock>0 and price>8' # expensive book available for sale
-
-for book in depot:
- try:
- result = safeeval(src,book)
- if result:
- print(book)
- except EvalException as e:
- print("ERR:", e)
-```
+This is code of `examples/products.py`. Expression is validated and compiled once and executed (as byte-code, very fast) many times, so filtering is both fast and secure.
+
-With first src line ('stock==0') it gives:
+~~~python
+#!/usr/bin/env python3
- {'price': 9.8, 'book': 'Gone Girl', 'stock': 0}
+import requests
+from evalidate import evalidate, ValidationException, CompilationException
+import json
+import sys
-With second src line ('stock>0 and price>8') it gives:
+data = requests.get('https://dummyjson.com/products?limit=100').json()
+
+try:
+ src = sys.argv[1]
+except IndexError:
+ src = 'True'
+
+try:
+ node = evalidate(src)
+except (ValidationException, CompilationException) as e:
+ print(e)
+ sys.exit(1)
- {'price': 12, 'book': 'Sirens of Titan', 'stock': 4}
- {'price': 14, 'book': 'Choke', 'stock': 2}
-Note, it uses simple and slow function safeeval(). Each call of safeeval it will parse and validate same source code, and it's not effective. But it's OK if you have small set of elements to check.
+code = compile(node, '<user filter>', 'eval')
-For better example check `examples/products.py` in repo. It uses dataset "products" from https://dummyjson.com/ and it's much more effective on large lists.
+c=0
+for p in data['products']:
+ # print(p)
+ try:
+ r = eval(code, p.copy())
+ if r:
+ print(json.dumps(p, indent=2))
+ c+=1
+ except Exception as e:
+ print("Runtime exception:", e)
+print("# {} products matches".format(c))
+~~~
~~~shell
# print all 100 products
@@ -1115,36 +1031,6 @@ For better example check `examples/products.py` in repo. It uses dataset "produc
./products.py 'category=="smartphones" and price<300'
~~~
-### Validate, compile and evaluate code
-```python
-import evalidate
-
-def test(src):
- data={'one':1,'two':2}
-
- try:
- node = evalidate.evalidate(src)
- except evalidate.CompilationException:
- print("Bad source code:", repr(src))
- return
- except evalidate.ValidationException:
- print("Dangerous code:", repr(src))
- return
-
- code = compile(node,'<usercode>','eval')
- try:
- result = eval(code,{},data)
- print("result:", result)
- except Exception as e:
- # almost any kind of exception can happen here
- print("Runtime exception:",e)
-
-srclist=['one+two+3', 'one+two+3+os.system("clear")', '', '1/0']
-
-for src in srclist:
- test(src)
-```
-
## Similar projects and benchmark
@@ -1154,21 +1040,27 @@ While asteval can compute much more complex code (define functions, use python m
- asteval is much slower (evalidate can be used at speed of eval() python bytecode)
- user can provide source code which runs very long time and consumes many resources
+
+[simpleeval](https://github.com/danthedeckie/simpleeval)
+Very similar project, using AST approach too and optimized to re-evaluate pre-parsed expressions. But parsed expressions are stored as more high-level [ast.Expr](https://docs.python.org/3/library/ast.html#ast.Expr) type and this approach is ~10 times slower, while evalidate uses python native `code` type and evaluation itself goes at speed of python eval()
+
+
evalidate is good to run short same code against different data.
## Benchmarking
-We use `evalidate-vs-asteval.py` which is in benchmark/ directory of repository
+We use `evalidate-vs-asteval.py` which is in benchmark/ directory of repository.
+We prepare list of 1 million of products (actually, we take just 100 products sample, but repeat it 10 000 times to get 1 million), and then filter it, finding only specific products on "untrusted user-supplied expression" (`price < 20` in this case)
+
~~~
-$ ./evalidate-vs-asteval.py
-Src: a+b
-Context: {'a': 1, 'b': 2}
-Runs: 100000
-asteval: 3.538s
-asteval (reuse interpreter): 1.232s
-safeeval: 2.384s
-evalidate/compile/eval (reuse compiled code): 0.017s
+Products: 1000000 items
+test_asteval_products(): 25.920s
+test_simpleeval_products(): 1.779s
+test_evalidate_products(): 0.160s
~~~
-0.017s vs 1.232s
+
+As you see, evalidate is almost 10 times faster then simpleeval and both are much faster then asteval.
+
+Maybe my test is not perfectly optimized (I'm not expert with simpleeval/asteval), if you can suggest better filtering sample code (which produces faster result), I will include it. But benchmark code must assume expression as unknown and untrusted.
## Read about eval() risks
@@ -1192,7 +1084,7 @@ Write me: yaroslaff at gmail.com
%prep
-%autosetup -n evalidate-1.0.2
+%autosetup -n evalidate-1.0.3
%build
%py3_build
@@ -1206,20 +1098,20 @@ if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi
if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi
pushd %{buildroot}
if [ -d usr/lib ]; then
- find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst
+ find usr/lib -type f -printf "\"/%h/%f\"\n" >> filelist.lst
fi
if [ -d usr/lib64 ]; then
- find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst
+ find usr/lib64 -type f -printf "\"/%h/%f\"\n" >> filelist.lst
fi
if [ -d usr/bin ]; then
- find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst
+ find usr/bin -type f -printf "\"/%h/%f\"\n" >> filelist.lst
fi
if [ -d usr/sbin ]; then
- find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst
+ find usr/sbin -type f -printf "\"/%h/%f\"\n" >> filelist.lst
fi
touch doclist.lst
if [ -d usr/share/man ]; then
- find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst
+ find usr/share/man -type f -printf "\"/%h/%f.gz\"\n" >> doclist.lst
fi
popd
mv %{buildroot}/filelist.lst .
@@ -1232,5 +1124,5 @@ mv %{buildroot}/doclist.lst .
%{_docdir}/*
%changelog
-* Tue May 30 2023 Python_Bot <Python_Bot@openeuler.org> - 1.0.2-1
+* Thu Jun 08 2023 Python_Bot <Python_Bot@openeuler.org> - 1.0.3-1
- Package Spec generated
diff --git a/sources b/sources
index 538b8f3..fe3a1ba 100644
--- a/sources
+++ b/sources
@@ -1 +1 @@
-697df2eaff0afedae1b3d7972abf38b5 evalidate-1.0.2.tar.gz
+e815215ca9a474f0316811a02cce9c55 evalidate-1.0.3.tar.gz