METADATA 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. Metadata-Version: 2.1
  2. Name: pure-eval
  3. Version: 0.2.2
  4. Summary: Safely evaluate AST nodes without side effects
  5. Home-page: http://github.com/alexmojaki/pure_eval
  6. Author: Alex Hall
  7. Author-email: alex.mojaki@gmail.com
  8. License: MIT
  9. Platform: UNKNOWN
  10. Classifier: Intended Audience :: Developers
  11. Classifier: Programming Language :: Python :: 3.5
  12. Classifier: Programming Language :: Python :: 3.6
  13. Classifier: Programming Language :: Python :: 3.7
  14. Classifier: Programming Language :: Python :: 3.8
  15. Classifier: Programming Language :: Python :: 3.9
  16. Classifier: Programming Language :: Python :: 3.10
  17. Classifier: License :: OSI Approved :: MIT License
  18. Classifier: Operating System :: OS Independent
  19. Description-Content-Type: text/markdown
  20. License-File: LICENSE.txt
  21. Provides-Extra: tests
  22. Requires-Dist: pytest ; extra == 'tests'
  23. # `pure_eval`
  24. [![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.5+](https://img.shields.io/pypi/pyversions/pure_eval.svg)](https://pypi.python.org/pypi/pure_eval)
  25. This is a Python package that lets you safely evaluate certain AST nodes without triggering arbitrary code that may have unwanted side effects.
  26. It can be installed from PyPI:
  27. pip install pure_eval
  28. To demonstrate usage, suppose we have an object defined as follows:
  29. ```python
  30. class Rectangle:
  31. def __init__(self, width, height):
  32. self.width = width
  33. self.height = height
  34. @property
  35. def area(self):
  36. print("Calculating area...")
  37. return self.width * self.height
  38. rect = Rectangle(3, 5)
  39. ```
  40. Given the `rect` object, we want to evaluate whatever expressions we can in this source code:
  41. ```python
  42. source = "(rect.width, rect.height, rect.area)"
  43. ```
  44. This library works with the AST, so let's parse the source code and peek inside:
  45. ```python
  46. import ast
  47. tree = ast.parse(source)
  48. the_tuple = tree.body[0].value
  49. for node in the_tuple.elts:
  50. print(ast.dump(node))
  51. ```
  52. Output:
  53. ```python
  54. Attribute(value=Name(id='rect', ctx=Load()), attr='width', ctx=Load())
  55. Attribute(value=Name(id='rect', ctx=Load()), attr='height', ctx=Load())
  56. Attribute(value=Name(id='rect', ctx=Load()), attr='area', ctx=Load())
  57. ```
  58. Now to actually use the library. First construct an Evaluator:
  59. ```python
  60. from pure_eval import Evaluator
  61. evaluator = Evaluator({"rect": rect})
  62. ```
  63. 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:
  64. ```python
  65. evaluator = Evaluator.from_frame(frame)
  66. ```
  67. Now to evaluate some nodes, using `evaluator[node]`:
  68. ```python
  69. print("rect.width:", evaluator[the_tuple.elts[0]])
  70. print("rect:", evaluator[the_tuple.elts[0].value])
  71. ```
  72. Output:
  73. ```
  74. rect.width: 3
  75. rect: <__main__.Rectangle object at 0x105b0dd30>
  76. ```
  77. 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.
  78. ```python
  79. from pure_eval import CannotEval
  80. try:
  81. print("rect.area:", evaluator[the_tuple.elts[2]]) # fails
  82. except CannotEval as e:
  83. print(e) # prints CannotEval
  84. ```
  85. To find all the expressions that can be evaluated in a tree:
  86. ```python
  87. for node, value in evaluator.find_expressions(tree):
  88. print(ast.dump(node), value)
  89. ```
  90. Output:
  91. ```python
  92. Attribute(value=Name(id='rect', ctx=Load()), attr='width', ctx=Load()) 3
  93. Attribute(value=Name(id='rect', ctx=Load()), attr='height', ctx=Load()) 5
  94. Name(id='rect', ctx=Load()) <__main__.Rectangle object at 0x105568d30>
  95. Name(id='rect', ctx=Load()) <__main__.Rectangle object at 0x105568d30>
  96. Name(id='rect', ctx=Load()) <__main__.Rectangle object at 0x105568d30>
  97. ```
  98. 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:
  99. ```python
  100. from pure_eval import group_expressions
  101. for nodes, values in group_expressions(evaluator.find_expressions(tree)):
  102. print(len(nodes), "nodes with value:", values)
  103. ```
  104. Output:
  105. ```
  106. 1 nodes with value: 3
  107. 1 nodes with value: 5
  108. 3 nodes with value: <__main__.Rectangle object at 0x10d374d30>
  109. ```
  110. 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`:
  111. ```python
  112. def foo():
  113. pass
  114. ```
  115. If we refer to `foo` by its name as usual, then that's not interesting:
  116. ```python
  117. from pure_eval import is_expression_interesting
  118. node = ast.parse('foo').body[0].value
  119. print(ast.dump(node))
  120. print(is_expression_interesting(node, foo))
  121. ```
  122. Output:
  123. ```python
  124. Name(id='foo', ctx=Load())
  125. False
  126. ```
  127. But if we refer to it by a different name, then it's interesting:
  128. ```python
  129. node = ast.parse('bar').body[0].value
  130. print(ast.dump(node))
  131. print(is_expression_interesting(node, foo))
  132. ```
  133. Output:
  134. ```python
  135. Name(id='bar', ctx=Load())
  136. True
  137. ```
  138. In general `is_expression_interesting` returns False for the following values:
  139. - Literals (e.g. `123`, `'abc'`, `[1, 2, 3]`, `{'a': (), 'b': ([1, 2], [3])}`)
  140. - Variables or attributes whose name is equal to the value's `__name__`, such as `foo` above or `self.foo` if it was a method.
  141. - Builtins (e.g. `len`) referred to by their usual name.
  142. To make things easier, you can combine finding expressions, grouping them, and filtering out the obvious ones with:
  143. ```python
  144. evaluator.interesting_expressions_grouped(root)
  145. ```
  146. To get the source code of an AST node, I recommend [asttokens](https://github.com/gristlabs/asttokens).
  147. Here's a complete example that brings it all together:
  148. ```python
  149. from asttokens import ASTTokens
  150. from pure_eval import Evaluator
  151. source = """
  152. x = 1
  153. d = {x: 2}
  154. y = d[x]
  155. """
  156. names = {}
  157. exec(source, names)
  158. atok = ASTTokens(source, parse=True)
  159. for nodes, value in Evaluator(names).interesting_expressions_grouped(atok.tree):
  160. print(atok.get_text(nodes[0]), "=", value)
  161. ```
  162. Output:
  163. ```python
  164. x = 1
  165. d = {1: 2}
  166. y = 2
  167. d[x] = 2
  168. ```