# `pure_eval`
[![Build Status](https://travis-ci.org/alexmojaki/pure_eval.svg?branch=master)](https://travis-ci.org/alexmojaki/pure_eval) [![Coverage Status](https://coveralls.io/repos/github/alexmojaki/pure_eval/badge.svg?branch=master)](https://coveralls.io/github/alexmojaki/pure_eval?branch=master) [![Supports Python versions 3.7+](https://img.shields.io/pypi/pyversions/pure_eval.svg)](https://pypi.python.org/pypi/pure_eval)
This is a Python package that lets you safely evaluate certain AST nodes without triggering arbitrary code that may have unwanted side effects.
It can be installed from PyPI:
pip install pure_eval
To demonstrate usage, suppose we have an object defined as follows:
```python
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
print("Calculating area...")
return self.width * self.height
rect = Rectangle(3, 5)
```
Given the `rect` object, we want to evaluate whatever expressions we can in this source code:
```python
source = "(rect.width, rect.height, rect.area)"
```
This library works with the AST, so let's parse the source code and peek inside:
```python
import ast
tree = ast.parse(source)
the_tuple = tree.body[0].value
for node in the_tuple.elts:
print(ast.dump(node))
```
Output:
```python
Attribute(value=Name(id='rect', ctx=Load()), attr='width', ctx=Load())
Attribute(value=Name(id='rect', ctx=Load()), attr='height', ctx=Load())
Attribute(value=Name(id='rect', ctx=Load()), attr='area', ctx=Load())
```
Now to actually use the library. First construct an Evaluator:
```python
from pure_eval import Evaluator
evaluator = Evaluator({"rect": rect})
```
The argument to `Evaluator` should be a mapping from variable names to their values. Or if you have access to the stack frame where `rect` is defined, you can instead use:
```python
evaluator = Evaluator.from_frame(frame)
```
Now to evaluate some nodes, using `evaluator[node]`:
```python
print("rect.width:", evaluator[the_tuple.elts[0]])
print("rect:", evaluator[the_tuple.elts[0].value])
```
Output:
```
rect.width: 3
rect: <__main__.Rectangle object at 0x105b0dd30>
```
OK, but you could have done the same thing with `eval`. The useful part is that it will refuse to evaluate the property `rect.area` because that would trigger unknown code. If we try, it'll raise a `CannotEval` exception.
```python
from pure_eval import CannotEval
try:
print("rect.area:", evaluator[the_tuple.elts[2]]) # fails
except CannotEval as e:
print(e) # prints CannotEval
```
To find all the expressions that can be evaluated in a tree:
```python
for node, value in evaluator.find_expressions(tree):
print(ast.dump(node), value)
```
Output:
```python
Attribute(value=Name(id='rect', ctx=Load()), attr='width', ctx=Load()) 3
Attribute(value=Name(id='rect', ctx=Load()), attr='height', ctx=Load()) 5
Name(id='rect', ctx=Load()) <__main__.Rectangle object at 0x105568d30>
Name(id='rect', ctx=Load()) <__main__.Rectangle object at 0x105568d30>
Name(id='rect', ctx=Load()) <__main__.Rectangle object at 0x105568d30>
```
Note that this includes `rect` three times, once for each appearance in the source code. Since all these nodes are equivalent, we can group them together:
```python
from pure_eval import group_expressions
for nodes, values in group_expressions(evaluator.find_expressions(tree)):
print(len(nodes), "nodes with value:", values)
```
Output:
```
1 nodes with value: 3
1 nodes with value: 5
3 nodes with value: <__main__.Rectangle object at 0x10d374d30>
```
If we want to list all the expressions in a tree, we may want to filter out certain expressions whose values are obvious. For example, suppose we have a function `foo`:
```python
def foo():
pass
```
If we refer to `foo` by its name as usual, then that's not interesting:
```python
from pure_eval import is_expression_interesting
node = ast.parse('foo').body[0].value
print(ast.dump(node))
print(is_expression_interesting(node, foo))
```
Output:
```python
Name(id='foo', ctx=Load())
False
```
But if we refer to it by a different name, then it's interesting:
```python
node = ast.parse('bar').body[0].value
print(ast.dump(node))
print(is_expression_interesting(node, foo))
```
Output:
```python
Name(id='bar', ctx=Load())
True
```
In general `is_expression_interesting` returns False for the following values:
- Literals (e.g. `123`, `'abc'`, `[1, 2, 3]`, `{'a': (), 'b': ([1, 2], [3])}`)
- Variables or attributes whose name is equal to the value's `__name__`, such as `foo` above or `self.foo` if it was a method.
- Builtins (e.g. `len`) referred to by their usual name.
To make things easier, you can combine finding expressions, grouping them, and filtering out the obvious ones with:
```python
evaluator.interesting_expressions_grouped(root)
```
To get the source code of an AST node, I recommend [asttokens](https://github.com/gristlabs/asttokens).
Here's a complete example that brings it all together:
```python
from asttokens import ASTTokens
from pure_eval import Evaluator
source = """
x = 1
d = {x: 2}
y = d[x]
"""
names = {}
exec(source, names)
atok = ASTTokens(source, parse=True)
for nodes, value in Evaluator(names).interesting_expressions_grouped(atok.tree):
print(atok.get_text(nodes[0]), "=", value)
```
Output:
```python
x = 1
d = {1: 2}
y = 2
d[x] = 2
```
Raw data
{
"_id": null,
"home_page": "http://github.com/alexmojaki/pure_eval",
"name": "pure-eval",
"maintainer": null,
"docs_url": null,
"requires_python": null,
"maintainer_email": null,
"keywords": null,
"author": "Alex Hall",
"author_email": "alex.mojaki@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz",
"platform": null,
"description": "# `pure_eval`\n\n[![Build Status](https://travis-ci.org/alexmojaki/pure_eval.svg?branch=master)](https://travis-ci.org/alexmojaki/pure_eval) [![Coverage Status](https://coveralls.io/repos/github/alexmojaki/pure_eval/badge.svg?branch=master)](https://coveralls.io/github/alexmojaki/pure_eval?branch=master) [![Supports Python versions 3.7+](https://img.shields.io/pypi/pyversions/pure_eval.svg)](https://pypi.python.org/pypi/pure_eval)\n\nThis is a Python package that lets you safely evaluate certain AST nodes without triggering arbitrary code that may have unwanted side effects.\n\nIt can be installed from PyPI:\n\n pip install pure_eval\n\nTo demonstrate usage, suppose we have an object defined as follows:\n\n```python\nclass Rectangle:\n def __init__(self, width, height):\n self.width = width\n self.height = height\n\n @property\n def area(self):\n print(\"Calculating area...\")\n return self.width * self.height\n\n\nrect = Rectangle(3, 5)\n```\n\nGiven the `rect` object, we want to evaluate whatever expressions we can in this source code:\n\n```python\nsource = \"(rect.width, rect.height, rect.area)\"\n```\n\nThis library works with the AST, so let's parse the source code and peek inside:\n\n```python\nimport ast\n\ntree = ast.parse(source)\nthe_tuple = tree.body[0].value\nfor node in the_tuple.elts:\n print(ast.dump(node))\n```\n\nOutput:\n\n```python\nAttribute(value=Name(id='rect', ctx=Load()), attr='width', ctx=Load())\nAttribute(value=Name(id='rect', ctx=Load()), attr='height', ctx=Load())\nAttribute(value=Name(id='rect', ctx=Load()), attr='area', ctx=Load())\n```\n\nNow to actually use the library. First construct an Evaluator:\n\n```python\nfrom pure_eval import Evaluator\n\nevaluator = Evaluator({\"rect\": rect})\n```\n\nThe argument to `Evaluator` should be a mapping from variable names to their values. Or if you have access to the stack frame where `rect` is defined, you can instead use:\n\n```python\nevaluator = Evaluator.from_frame(frame)\n```\n\nNow to evaluate some nodes, using `evaluator[node]`:\n\n```python\nprint(\"rect.width:\", evaluator[the_tuple.elts[0]])\nprint(\"rect:\", evaluator[the_tuple.elts[0].value])\n```\n\nOutput:\n\n```\nrect.width: 3\nrect: <__main__.Rectangle object at 0x105b0dd30>\n```\n\nOK, but you could have done the same thing with `eval`. The useful part is that it will refuse to evaluate the property `rect.area` because that would trigger unknown code. If we try, it'll raise a `CannotEval` exception.\n\n```python\nfrom pure_eval import CannotEval\n\ntry:\n print(\"rect.area:\", evaluator[the_tuple.elts[2]]) # fails\nexcept CannotEval as e:\n print(e) # prints CannotEval\n```\n\nTo find all the expressions that can be evaluated in a tree:\n\n```python\nfor node, value in evaluator.find_expressions(tree):\n print(ast.dump(node), value)\n```\n\nOutput:\n\n```python\nAttribute(value=Name(id='rect', ctx=Load()), attr='width', ctx=Load()) 3\nAttribute(value=Name(id='rect', ctx=Load()), attr='height', ctx=Load()) 5\nName(id='rect', ctx=Load()) <__main__.Rectangle object at 0x105568d30>\nName(id='rect', ctx=Load()) <__main__.Rectangle object at 0x105568d30>\nName(id='rect', ctx=Load()) <__main__.Rectangle object at 0x105568d30>\n```\n\nNote that this includes `rect` three times, once for each appearance in the source code. Since all these nodes are equivalent, we can group them together:\n\n```python\nfrom pure_eval import group_expressions\n\nfor nodes, values in group_expressions(evaluator.find_expressions(tree)):\n print(len(nodes), \"nodes with value:\", values)\n```\n\nOutput:\n\n```\n1 nodes with value: 3\n1 nodes with value: 5\n3 nodes with value: <__main__.Rectangle object at 0x10d374d30>\n```\n\nIf we want to list all the expressions in a tree, we may want to filter out certain expressions whose values are obvious. For example, suppose we have a function `foo`:\n\n```python\ndef foo():\n pass\n```\n\nIf we refer to `foo` by its name as usual, then that's not interesting:\n\n```python\nfrom pure_eval import is_expression_interesting\n\nnode = ast.parse('foo').body[0].value\nprint(ast.dump(node))\nprint(is_expression_interesting(node, foo))\n```\n\nOutput:\n\n```python\nName(id='foo', ctx=Load())\nFalse\n```\n\nBut if we refer to it by a different name, then it's interesting:\n\n```python\nnode = ast.parse('bar').body[0].value\nprint(ast.dump(node))\nprint(is_expression_interesting(node, foo))\n```\n\nOutput:\n\n```python\nName(id='bar', ctx=Load())\nTrue\n```\n\nIn general `is_expression_interesting` returns False for the following values:\n- Literals (e.g. `123`, `'abc'`, `[1, 2, 3]`, `{'a': (), 'b': ([1, 2], [3])}`)\n- Variables or attributes whose name is equal to the value's `__name__`, such as `foo` above or `self.foo` if it was a method.\n- Builtins (e.g. `len`) referred to by their usual name.\n\nTo make things easier, you can combine finding expressions, grouping them, and filtering out the obvious ones with:\n\n```python\nevaluator.interesting_expressions_grouped(root)\n```\n\nTo get the source code of an AST node, I recommend [asttokens](https://github.com/gristlabs/asttokens).\n\nHere's a complete example that brings it all together:\n\n```python\nfrom asttokens import ASTTokens\nfrom pure_eval import Evaluator\n\nsource = \"\"\"\nx = 1\nd = {x: 2}\ny = d[x]\n\"\"\"\n\nnames = {}\nexec(source, names)\natok = ASTTokens(source, parse=True)\nfor nodes, value in Evaluator(names).interesting_expressions_grouped(atok.tree):\n print(atok.get_text(nodes[0]), \"=\", value)\n```\n\nOutput:\n\n```python\nx = 1\nd = {1: 2}\ny = 2\nd[x] = 2\n```\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Safely evaluate AST nodes without side effects",
"version": "0.2.3",
"project_urls": {
"Homepage": "http://github.com/alexmojaki/pure_eval"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "8e37efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f",
"md5": "133fc4b94daeec26570d6297817d0b94",
"sha256": "1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"
},
"downloads": -1,
"filename": "pure_eval-0.2.3-py3-none-any.whl",
"has_sig": false,
"md5_digest": "133fc4b94daeec26570d6297817d0b94",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 11842,
"upload_time": "2024-07-21T12:58:20",
"upload_time_iso_8601": "2024-07-21T12:58:20.040816Z",
"url": "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "cd050a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b",
"md5": "d545186f2c899d9dd273c03d71b7ffb7",
"sha256": "5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"
},
"downloads": -1,
"filename": "pure_eval-0.2.3.tar.gz",
"has_sig": false,
"md5_digest": "d545186f2c899d9dd273c03d71b7ffb7",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 19752,
"upload_time": "2024-07-21T12:58:21",
"upload_time_iso_8601": "2024-07-21T12:58:21.801290Z",
"url": "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-07-21 12:58:21",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "alexmojaki",
"github_project": "pure_eval",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"tox": true,
"lcname": "pure-eval"
}