pax_global_header00006660000000000000000000000064146472024760014526gustar00rootroot0000000000000052 comment=b02b4046e809c04c920fad4ddc8e3f838c59af16 pure_eval-0.2.3/000077500000000000000000000000001464720247600135125ustar00rootroot00000000000000pure_eval-0.2.3/.github/000077500000000000000000000000001464720247600150525ustar00rootroot00000000000000pure_eval-0.2.3/.github/workflows/000077500000000000000000000000001464720247600171075ustar00rootroot00000000000000pure_eval-0.2.3/.github/workflows/pytest.yml000066400000000000000000000022771464720247600211720ustar00rootroot00000000000000name: Tests on: push: branches: - master pull_request: workflow_dispatch: jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: [3.7, 3.8, 3.9, '3.10', 3.11, 3.12, 3.13-dev] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | pip install --upgrade pip pip install --upgrade coveralls .[tests] - name: Test env: PURE_EVAL_SLOW_TESTS: 1 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: "test-${{ matrix.python-version }}-${{ matrix.os }}" COVERALLS_PARALLEL: true run: | coverage run --source pure_eval -m pytest coverage report -m coveralls --service=github coveralls: name: Coveralls Finished needs: test runs-on: ubuntu-latest container: python:3-slim steps: - name: Finished run: | pip3 install --upgrade coveralls coveralls --service=github --finish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} pure_eval-0.2.3/.gitignore000066400000000000000000000034461464720247600155110ustar00rootroot00000000000000pure_eval/version.py # Created by .ignore support plugin (hsz.mobi) ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # vscode settings .vscode/ pure_eval-0.2.3/LICENSE.txt000066400000000000000000000020521464720247600153340ustar00rootroot00000000000000MIT License Copyright (c) 2019 Alex Hall Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pure_eval-0.2.3/MANIFEST.in000066400000000000000000000001011464720247600152400ustar00rootroot00000000000000include LICENSE.txt include pure_eval/py.typed include README.md pure_eval-0.2.3/README.md000066400000000000000000000124621464720247600147760ustar00rootroot00000000000000# `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 ``` pure_eval-0.2.3/make_release.sh000077500000000000000000000011121464720247600164610ustar00rootroot00000000000000#!/usr/bin/env bash set -eux -o pipefail if [ ! -z "$(git status --porcelain)" ]; then set +x echo You have uncommitted changes which would mess up the git tag exit 1 fi if [ -z "${1+x}" ]; then set +x echo Provide a version argument echo "${0} .." exit 1 fi if [[ ! ${1} =~ ^([0-9]+)(\.[0-9]+)?(\.[0-9]+)?$ ]]; then echo "Not a valid release tag." exit 1 fi export TAG="v${1}" git tag -f "${TAG}" git push origin HEAD "${TAG}" rm -rf ./build ./dist python -m build --sdist --wheel . twine upload ./dist/*.whl dist/*.tar.gz pure_eval-0.2.3/pure_eval/000077500000000000000000000000001464720247600154745ustar00rootroot00000000000000pure_eval-0.2.3/pure_eval/__init__.py000066400000000000000000000006621464720247600176110ustar00rootroot00000000000000from .core import Evaluator, CannotEval, group_expressions, is_expression_interesting from .my_getattr_static import getattr_static try: from .version import __version__ except ImportError: # version.py is auto-generated with the git tag when building __version__ = "???" __all__ = [ "Evaluator", "CannotEval", "group_expressions", "is_expression_interesting", "getattr_static", "__version__", ] pure_eval-0.2.3/pure_eval/core.py000066400000000000000000000357151464720247600170110ustar00rootroot00000000000000import ast import builtins import operator from collections import ChainMap, OrderedDict, deque from contextlib import suppress from types import FrameType from typing import Any, Tuple, Iterable, List, Mapping, Dict, Union, Set from pure_eval.my_getattr_static import getattr_static from pure_eval.utils import ( CannotEval, has_ast_name, copy_ast_without_context, is_standard_types, of_standard_types, is_any, of_type, ensure_dict, ) class Evaluator: def __init__(self, names: Mapping[str, Any]): """ Construct a new evaluator with the given variable names. This is a low level API, typically you will use `Evaluator.from_frame(frame)`. :param names: a mapping from variable names to their values. """ self.names = names self._cache = {} # type: Dict[ast.expr, Any] @classmethod def from_frame(cls, frame: FrameType) -> 'Evaluator': """ Construct an Evaluator that can look up variables from the given frame. :param frame: a frame object, e.g. from a traceback or `inspect.currentframe().f_back`. """ return cls(ChainMap( ensure_dict(frame.f_locals), ensure_dict(frame.f_globals), ensure_dict(frame.f_builtins), )) def __getitem__(self, node: ast.expr) -> Any: """ Find the value of the given node. If it cannot be evaluated safely, this raises `CannotEval`. The result is cached either way. :param node: an AST expression to evaluate :return: the value of the node """ if not isinstance(node, ast.expr): raise TypeError("node should be an ast.expr, not {!r}".format(type(node).__name__)) with suppress(KeyError): result = self._cache[node] if result is CannotEval: raise CannotEval else: return result try: self._cache[node] = result = self._handle(node) return result except CannotEval: self._cache[node] = CannotEval raise def _handle(self, node: ast.expr) -> Any: """ This is where the evaluation happens. Users should use `__getitem__`, i.e. `evaluator[node]`, as it provides caching. :param node: an AST expression to evaluate :return: the value of the node """ with suppress(Exception): return ast.literal_eval(node) if isinstance(node, ast.Name): try: return self.names[node.id] except KeyError: raise CannotEval elif isinstance(node, ast.Attribute): value = self[node.value] attr = node.attr return getattr_static(value, attr) elif isinstance(node, ast.Subscript): return self._handle_subscript(node) elif isinstance(node, (ast.List, ast.Tuple, ast.Set, ast.Dict)): return self._handle_container(node) elif isinstance(node, ast.UnaryOp): return self._handle_unary(node) elif isinstance(node, ast.BinOp): return self._handle_binop(node) elif isinstance(node, ast.BoolOp): return self._handle_boolop(node) elif isinstance(node, ast.Compare): return self._handle_compare(node) elif isinstance(node, ast.Call): return self._handle_call(node) raise CannotEval def _handle_call(self, node): if node.keywords: raise CannotEval func = self[node.func] args = [self[arg] for arg in node.args] if ( is_any( func, slice, int, range, round, complex, list, tuple, abs, hex, bin, oct, bool, ord, float, len, chr, ) or len(args) == 0 and is_any(func, set, dict, str, frozenset, bytes, bytearray, object) or len(args) >= 2 and is_any(func, str, divmod, bytes, bytearray, pow) ): args = [ of_standard_types(arg, check_dict_values=False, deep=False) for arg in args ] try: return func(*args) except Exception as e: raise CannotEval from e if len(args) == 1: arg = args[0] if is_any(func, id, type): try: return func(arg) except Exception as e: raise CannotEval from e if is_any(func, all, any, sum): of_type(arg, tuple, frozenset, list, set, dict, OrderedDict, deque) for x in arg: of_standard_types(x, check_dict_values=False, deep=False) try: return func(arg) except Exception as e: raise CannotEval from e if is_any( func, sorted, min, max, hash, set, dict, ascii, str, repr, frozenset ): of_standard_types(arg, check_dict_values=True, deep=True) try: return func(arg) except Exception as e: raise CannotEval from e raise CannotEval def _handle_compare(self, node): left = self[node.left] result = True for op, right in zip(node.ops, node.comparators): right = self[right] op_type = type(op) op_func = { ast.Eq: operator.eq, ast.NotEq: operator.ne, ast.Lt: operator.lt, ast.LtE: operator.le, ast.Gt: operator.gt, ast.GtE: operator.ge, ast.Is: operator.is_, ast.IsNot: operator.is_not, ast.In: (lambda a, b: a in b), ast.NotIn: (lambda a, b: a not in b), }[op_type] if op_type not in (ast.Is, ast.IsNot): of_standard_types(left, check_dict_values=False, deep=True) of_standard_types(right, check_dict_values=False, deep=True) try: result = op_func(left, right) except Exception as e: raise CannotEval from e if not result: return result left = right return result def _handle_boolop(self, node): left = of_standard_types( self[node.values[0]], check_dict_values=False, deep=False ) for right in node.values[1:]: # We need short circuiting so that the whole operation can be evaluated # even if the right operand can't if isinstance(node.op, ast.Or): left = left or of_standard_types( self[right], check_dict_values=False, deep=False ) else: assert isinstance(node.op, ast.And) left = left and of_standard_types( self[right], check_dict_values=False, deep=False ) return left def _handle_binop(self, node): op_type = type(node.op) op = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv, ast.Mod: operator.mod, ast.Pow: operator.pow, ast.LShift: operator.lshift, ast.RShift: operator.rshift, ast.BitOr: operator.or_, ast.BitXor: operator.xor, ast.BitAnd: operator.and_, }.get(op_type) if not op: raise CannotEval left = self[node.left] hash_type = is_any(type(left), set, frozenset, dict, OrderedDict) left = of_standard_types(left, check_dict_values=False, deep=hash_type) formatting = type(left) in (str, bytes) and op_type == ast.Mod right = of_standard_types( self[node.right], check_dict_values=formatting, deep=formatting or hash_type, ) try: return op(left, right) except Exception as e: raise CannotEval from e def _handle_unary(self, node: ast.UnaryOp): value = of_standard_types( self[node.operand], check_dict_values=False, deep=False ) op_type = type(node.op) op = { ast.USub: operator.neg, ast.UAdd: operator.pos, ast.Not: operator.not_, ast.Invert: operator.invert, }[op_type] try: return op(value) except Exception as e: raise CannotEval from e def _handle_subscript(self, node): value = self[node.value] of_standard_types( value, check_dict_values=False, deep=is_any(type(value), dict, OrderedDict) ) index = node.slice if isinstance(index, ast.Slice): index = slice( *[ None if p is None else self[p] for p in [index.lower, index.upper, index.step] ] ) elif isinstance(index, ast.ExtSlice): raise CannotEval else: if isinstance(index, ast.Index): index = index.value index = self[index] of_standard_types(index, check_dict_values=False, deep=True) try: return value[index] except Exception: raise CannotEval def _handle_container( self, node: Union[ast.List, ast.Tuple, ast.Set, ast.Dict] ) -> Union[List, Tuple, Set, Dict]: """Handle container nodes, including List, Set, Tuple and Dict""" if isinstance(node, ast.Dict): elts = node.keys if None in elts: # ** unpacking inside {}, not yet supported raise CannotEval else: elts = node.elts elts = [self[elt] for elt in elts] if isinstance(node, ast.List): return elts if isinstance(node, ast.Tuple): return tuple(elts) # Set and Dict if not all( is_standard_types(elt, check_dict_values=False, deep=True) for elt in elts ): raise CannotEval if isinstance(node, ast.Set): try: return set(elts) except TypeError: raise CannotEval assert isinstance(node, ast.Dict) pairs = [(elt, self[val]) for elt, val in zip(elts, node.values)] try: return dict(pairs) except TypeError: raise CannotEval def find_expressions(self, root: ast.AST) -> Iterable[Tuple[ast.expr, Any]]: """ Find all expressions in the given tree that can be safely evaluated. This is a low level API, typically you will use `interesting_expressions_grouped`. :param root: any AST node :return: generator of pairs (tuples) of expression nodes and their corresponding values. """ for node in ast.walk(root): if not isinstance(node, ast.expr): continue try: value = self[node] except CannotEval: continue yield node, value def interesting_expressions_grouped(self, root: ast.AST) -> List[Tuple[List[ast.expr], Any]]: """ Find all interesting expressions in the given tree that can be safely evaluated, grouping equivalent nodes together. For more control and details, see: - Evaluator.find_expressions - is_expression_interesting - group_expressions :param root: any AST node :return: A list of pairs (tuples) containing: - A list of equivalent AST expressions - The value of the first expression node (which should be the same for all nodes, unless threads are involved) """ return group_expressions( pair for pair in self.find_expressions(root) if is_expression_interesting(*pair) ) def is_expression_interesting(node: ast.expr, value: Any) -> bool: """ Determines if an expression is potentially interesting, at least in my opinion. Returns False for the following expressions whose value is generally obvious: - 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__. For example, a function `def foo(): ...` is not interesting when referred to as `foo` as it usually would, but `bar` can be interesting if `bar is foo`. Similarly the method `self.foo` is not interesting. - Builtins (e.g. `len`) referred to by their usual name. This is a low level API, typically you will use `interesting_expressions_grouped`. :param node: an AST expression :param value: the value of the node :return: a boolean: True if the expression is interesting, False otherwise """ with suppress(ValueError): ast.literal_eval(node) return False # TODO exclude inner modules, e.g. numpy.random.__name__ == 'numpy.random' != 'random' # TODO exclude common module abbreviations, e.g. numpy as np, pandas as pd if has_ast_name(value, node): return False if ( isinstance(node, ast.Name) and getattr(builtins, node.id, object()) is value ): return False return True def group_expressions(expressions: Iterable[Tuple[ast.expr, Any]]) -> List[Tuple[List[ast.expr], Any]]: """ Organise expression nodes and their values such that equivalent nodes are together. Two nodes are considered equivalent if they have the same structure, ignoring context (Load, Store, or Delete) and location (lineno, col_offset). For example, this will group together the same variable name mentioned multiple times in an expression. This will not check the values of the nodes. Equivalent nodes should have the same values, unless threads are involved. This is a low level API, typically you will use `interesting_expressions_grouped`. :param expressions: pairs of AST expressions and their values, as obtained from `Evaluator.find_expressions`, or `(node, evaluator[node])`. :return: A list of pairs (tuples) containing: - A list of equivalent AST expressions - The value of the first expression node (which should be the same for all nodes, unless threads are involved) """ result = {} for node, value in expressions: dump = ast.dump(copy_ast_without_context(node)) result.setdefault(dump, ([], value))[0].append(node) return list(result.values()) pure_eval-0.2.3/pure_eval/my_getattr_static.py000066400000000000000000000101011464720247600215650ustar00rootroot00000000000000import types from pure_eval.utils import of_type, CannotEval _sentinel = object() def _static_getmro(klass): return type.__dict__['__mro__'].__get__(klass) def _check_instance(obj, attr): instance_dict = {} try: instance_dict = object.__getattribute__(obj, "__dict__") except AttributeError: pass return dict.get(instance_dict, attr, _sentinel) def _check_class(klass, attr): for entry in _static_getmro(klass): if _shadowed_dict(type(entry)) is _sentinel: try: return entry.__dict__[attr] except KeyError: pass else: break return _sentinel def _is_type(obj): try: _static_getmro(obj) except TypeError: return False return True def _shadowed_dict(klass): dict_attr = type.__dict__["__dict__"] for entry in _static_getmro(klass): try: class_dict = dict_attr.__get__(entry)["__dict__"] except KeyError: pass else: if not (type(class_dict) is types.GetSetDescriptorType and class_dict.__name__ == "__dict__" and class_dict.__objclass__ is entry): return class_dict return _sentinel def getattr_static(obj, attr): """Retrieve attributes without triggering dynamic lookup via the descriptor protocol, __getattr__ or __getattribute__. Note: this function may not be able to retrieve all attributes that getattr can fetch (like dynamically created attributes) and may find attributes that getattr can't (like descriptors that raise AttributeError). It can also return descriptor objects instead of instance members in some cases. See the documentation for details. """ instance_result = _sentinel if not _is_type(obj): klass = type(obj) dict_attr = _shadowed_dict(klass) if (dict_attr is _sentinel or type(dict_attr) is types.MemberDescriptorType): instance_result = _check_instance(obj, attr) else: raise CannotEval else: klass = obj klass_result = _check_class(klass, attr) if instance_result is not _sentinel and klass_result is not _sentinel: if _check_class(type(klass_result), "__get__") is not _sentinel and ( _check_class(type(klass_result), "__set__") is not _sentinel or _check_class(type(klass_result), "__delete__") is not _sentinel ): return _resolve_descriptor(klass_result, obj, klass) if instance_result is not _sentinel: return instance_result if klass_result is not _sentinel: get = _check_class(type(klass_result), '__get__') if get is _sentinel: return klass_result else: if obj is klass: instance = None else: instance = obj return _resolve_descriptor(klass_result, instance, klass) if obj is klass: # for types we check the metaclass too for entry in _static_getmro(type(klass)): if _shadowed_dict(type(entry)) is _sentinel: try: result = entry.__dict__[attr] get = _check_class(type(result), '__get__') if get is not _sentinel: raise CannotEval return result except KeyError: pass raise CannotEval class _foo: __slots__ = ['foo'] method = lambda: 0 slot_descriptor = _foo.foo wrapper_descriptor = str.__dict__['__add__'] method_descriptor = str.__dict__['startswith'] user_method_descriptor = _foo.__dict__['method'] safe_descriptors_raw = [ slot_descriptor, wrapper_descriptor, method_descriptor, user_method_descriptor, ] safe_descriptor_types = list(map(type, safe_descriptors_raw)) def _resolve_descriptor(d, instance, owner): try: return type(of_type(d, *safe_descriptor_types)).__get__(d, instance, owner) except AttributeError as e: raise CannotEval from e pure_eval-0.2.3/pure_eval/py.typed000066400000000000000000000001041464720247600171660ustar00rootroot00000000000000# Marker file for PEP 561. The pure_eval package uses inline types. pure_eval-0.2.3/pure_eval/utils.py000066400000000000000000000110041464720247600172020ustar00rootroot00000000000000from collections import OrderedDict, deque from datetime import date, time, datetime from decimal import Decimal from fractions import Fraction import ast import enum import typing class CannotEval(Exception): def __repr__(self): return self.__class__.__name__ __str__ = __repr__ def is_any(x, *args): return any( x is arg for arg in args ) def of_type(x, *types): if is_any(type(x), *types): return x else: raise CannotEval def of_standard_types(x, *, check_dict_values: bool, deep: bool): if is_standard_types(x, check_dict_values=check_dict_values, deep=deep): return x else: raise CannotEval def is_standard_types(x, *, check_dict_values: bool, deep: bool): try: return _is_standard_types_deep(x, check_dict_values, deep)[0] except RecursionError: return False def _is_standard_types_deep(x, check_dict_values: bool, deep: bool): typ = type(x) if is_any( typ, str, int, bool, float, bytes, complex, date, time, datetime, Fraction, Decimal, type(None), object, ): return True, 0 if is_any(typ, tuple, frozenset, list, set, dict, OrderedDict, deque, slice): if typ in [slice]: length = 0 else: length = len(x) assert isinstance(deep, bool) if not deep: return True, length if check_dict_values and typ in (dict, OrderedDict): items = (v for pair in x.items() for v in pair) elif typ is slice: items = [x.start, x.stop, x.step] else: items = x for item in items: if length > 100000: return False, length is_standard, item_length = _is_standard_types_deep( item, check_dict_values, deep ) if not is_standard: return False, length length += item_length return True, length return False, 0 class _E(enum.Enum): pass class _C: def foo(self): pass # pragma: nocover def bar(self): pass # pragma: nocover @classmethod def cm(cls): pass # pragma: nocover @staticmethod def sm(): pass # pragma: nocover safe_name_samples = { "len": len, "append": list.append, "__add__": list.__add__, "insert": [].insert, "__mul__": [].__mul__, "fromkeys": dict.__dict__['fromkeys'], "is_any": is_any, "__repr__": CannotEval.__repr__, "foo": _C().foo, "bar": _C.bar, "cm": _C.cm, "sm": _C.sm, "ast": ast, "CannotEval": CannotEval, "_E": _E, } typing_annotation_samples = { name: getattr(typing, name) for name in "List Dict Tuple Set Callable Mapping".split() } safe_name_types = tuple({ type(f) for f in safe_name_samples.values() }) typing_annotation_types = tuple({ type(f) for f in typing_annotation_samples.values() }) def eq_checking_types(a, b): return type(a) is type(b) and a == b def ast_name(node): if isinstance(node, ast.Name): return node.id elif isinstance(node, ast.Attribute): return node.attr else: return None def safe_name(value): typ = type(value) if is_any(typ, *safe_name_types): return value.__name__ elif value is typing.Optional: return "Optional" elif value is typing.Union: return "Union" elif is_any(typ, *typing_annotation_types): return getattr(value, "__name__", None) or getattr(value, "_name", None) else: return None def has_ast_name(value, node): value_name = safe_name(value) if type(value_name) is not str: return False return eq_checking_types(ast_name(node), value_name) def copy_ast_without_context(x): if isinstance(x, ast.AST): kwargs = { field: copy_ast_without_context(getattr(x, field)) for field in x._fields if field != 'ctx' if hasattr(x, field) } a = type(x)(**kwargs) if hasattr(a, 'ctx'): # Python 3.13.0b2+ defaults to Load when we don't pass ctx # https://github.com/python/cpython/pull/118871 del a.ctx return a elif isinstance(x, list): return list(map(copy_ast_without_context, x)) else: return x def ensure_dict(x): """ Handles invalid non-dict inputs """ try: return dict(x) except Exception: return {} pure_eval-0.2.3/pyproject.toml000066400000000000000000000003461464720247600164310ustar00rootroot00000000000000[build-system] requires = ["setuptools>=41", "wheel", "setuptools_scm[toml]>=3.4.3"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "pure_eval/version.py" write_to_template = "__version__ = '{version}'" pure_eval-0.2.3/setup.cfg000066400000000000000000000017041464720247600153350ustar00rootroot00000000000000[metadata] name = pure_eval url = http://github.com/alexmojaki/pure_eval author = Alex Hall author_email = alex.mojaki@gmail.com license = MIT description = Safely evaluate AST nodes without side effects long_description = file: README.md long_description_content_type = text/markdown classifiers = Intended Audience :: Developers Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 License :: OSI Approved :: MIT License Operating System :: OS Independent [options] packages = pure_eval install_requires = include_package_data = True tests_require = pytest setup_requires = setuptools>=44; setuptools_scm[toml]>=3.4.3 [options.extras_require] tests = pytest [options.package_data] pure_eval = py.typed pure_eval-0.2.3/setup.py000066400000000000000000000001051464720247600152200ustar00rootroot00000000000000from setuptools import setup if __name__ == "__main__": setup() pure_eval-0.2.3/tests/000077500000000000000000000000001464720247600146545ustar00rootroot00000000000000pure_eval-0.2.3/tests/__init__.py000066400000000000000000000000001464720247600167530ustar00rootroot00000000000000pure_eval-0.2.3/tests/test_core.py000066400000000000000000000273541464720247600172300ustar00rootroot00000000000000import ast import inspect import sys import typing import itertools import pytest from pure_eval import Evaluator, CannotEval from pure_eval.core import is_expression_interesting, group_expressions def check_eval(source, *expected_values, total=True): frame = inspect.currentframe().f_back evaluator = Evaluator.from_frame(frame) root = ast.parse(source) values = [] for node, value in evaluator.find_expressions(root): expr = ast.Expression(body=node) ast.copy_location(expr, node) code = compile(expr, "", "eval") expected = eval(code, frame.f_globals, frame.f_locals) assert value == expected values.append(value) if total: assert value in expected_values for expected in expected_values: assert expected in values def test_eval_names(): x = 3 check_eval( "(x, check_eval, len), nonexistent", x, check_eval, len, (x, check_eval, len) ) def test_eval_literals(): check_eval( "(1, 'a', [{}])", (1, 'a', [{}]), 1, 'a', [{}], {}, ) def test_eval_attrs(): class Foo: bar = 9 @property def prop(self): return 0 def method(self): pass foo = Foo() foo.spam = 44 check_eval( "foo.bar + foo.spam + Foo.bar", foo.bar, foo.spam, Foo.bar, foo.bar + foo.spam, foo.bar + foo.spam + Foo.bar, foo, Foo ) check_eval( "Foo.spam + Foo.prop + foo.prop + foo.method() + Foo.method", foo, Foo, Foo.method, foo.method ) check_eval("typing.List", typing, typing.List) def test_eval_dict(): d = {1: 2} # All is well, d[1] is evaluated check_eval( "d[1]", d[1], d, 1 ) class BadHash: def __hash__(self): return 0 d[BadHash()] = 3 # d[1] is not evaluated because d contains a bad key check_eval( "d[1]", d, 1 ) d = {1: 2} b = BadHash() # d[b] is not evaluated because b is a bad key check_eval( "d[b]", d, b ) def make_d(): return {1: 2} str(make_d()) # Cannot eval make_d()[1] because the left part cannot eval check_eval( "make_d()[1]", make_d, 1 ) # Cannot eval d[:1] because slices aren't hashable check_eval( "d[:1]", d, 1 ) d = {(1, 3): 2} b = BadHash() # d[(1, b)] is not evaluated because b is a bad key check_eval( "d[(1, b)], d[(1, 3)]", # (1, b) is a bad key, but it's a valid tuple element d, b, d[(1, 3)], (1, 3), 1, 3, (1, b) ) e = 3 check_eval( "{(1, e): 2}, {(1, b): 1}", # b is a bad key b, 1, (1, e), 2, e, {(1, e): 2}, (1, b) ) check_eval("{{}: {}}", {}) def test_eval_set(): a = 1 b = {2, 3} # unhashable itself check_eval( "{a}, b, {a, b, 4}, {b}", # d is a bad key a, {a}, b, 4 ) def test_eval_sequence_subscript(): lst = [12, 34, 56] i = 1 check_eval( "lst[i] + lst[:i][0] + lst[i:][i] + lst[::2][False]", lst[i], lst[:i][0], lst[i:][i], lst[::2], lst[i] + lst[:i][0], lst[i] + lst[:i][0] + lst[i:][i], lst[i] + lst[:i][0] + lst[i:][i] + lst[::2][False], lst, i, lst[:i], 0, lst[i:], 2, ) check_eval( "('abc', 'def')[1][2]", ('abc', 'def')[1][2], total=False ) check_eval( "[lst][0][2]", lst, [lst], [lst][0], [lst][0][2], 2, 0 ) check_eval( "(lst, )[0][2]", lst, (lst, ), (lst, )[0], (lst, )[0][2], 2, 0 ) def test_eval_unary_op(): a = 123 check_eval( "a, -a, +a, ~a", a, -a, +a, ~a, (a, -a, +a, ~a), ) check_eval( "not a", a, not a, ) b = "" check_eval( "not b, -b", b, not b, ) def test_eval_binary_op(): a = 123 b = 456 check_eval( "a + b - a * b - (a ** b) // (b % a)", a + b - a * b - (a ** b) // (b % a), a + b, a * b, (a ** b), (b % a), a + b - a * b, (a ** b) // (b % a), a, b, ) check_eval( "a / b", a / b, a, b, ) check_eval( "a & b", a & b, a, b, ) check_eval( "a | b", a | b, a, b, ) check_eval( "a ^ b", a ^ b, a, b, ) check_eval( "a << 2", a << 2, a, 2 ) check_eval( "a >> 2", a >> 2, a, 2 ) check_eval( "'a %s c' % b", 'a %s c' % b, 'a %s c', b, ) check_eval( "'a %s c' % check_eval, a @ b, a + []", 'a %s c', check_eval, a, b, [], ) def check_interesting(source): frame = inspect.currentframe().f_back evaluator = Evaluator.from_frame(frame) root = ast.parse(source) node = root.body[0].value cannot = value = None try: value = evaluator[node] except CannotEval as e: cannot = e expr = ast.Expression(body=node) ast.copy_location(expr, node) code = compile(expr, "", "eval") try: expected = eval(code, frame.f_globals, frame.f_locals) except Exception: if cannot: return None else: raise else: if cannot: raise cannot else: assert value == expected return is_expression_interesting(node, value) def test_is_expression_interesting(): class Foo: def method(self): pass alias = method foo = Foo() x = [check_interesting] foo.x = x assert check_interesting('x') assert not check_interesting('help') assert not check_interesting('check_interesting') assert not check_interesting('[1]') assert check_interesting('[1, 2][0]') assert check_interesting('foo') assert not check_interesting('Foo') assert check_interesting('foo.x') assert not check_interesting('foo.method') assert check_interesting('foo.alias') assert not check_interesting('Foo.method') assert check_interesting('Foo.alias') assert check_interesting('x[0]') assert not check_interesting('typing.List') assert check_interesting('[typing.List][0]') def test_boolop(): for a, b, c in [ [0, 123, 456], [0, [0], [[0]]], [set(), {1}, {1, (1,)}], ]: str((a, b, c)) for length in [2, 3, 4]: for vals in itertools.product(["1/0", "a", "b", "c"], repeat=length): for op in [ "not in", "is not", *"+ - / // * & ^ % @ | >> or and < <= > >= == != in is".split(), ]: op = " %s " % op source = op.join(vals) check_interesting(source) def test_is(): for a, b, c in [ [check_interesting, CannotEval(), CannotEval], ]: str((a, b, c)) for length in [2, 3, 4]: for vals in itertools.product(["1/0", "a", "b", "c"], repeat=length): for op in ["is", "is not"]: op = " %s " % op source = op.join(vals) check_interesting(source) def test_calls(): # No keywords allowed with pytest.raises(CannotEval): check_interesting("str(b'', encoding='utf8')") # This function not allowed with pytest.raises(CannotEval): check_interesting("print(3)") assert check_interesting("slice(3)") assert check_interesting("slice(3, 5)") assert check_interesting("slice(3, 5, 1)") assert check_interesting("int()") assert check_interesting("int('5')") assert check_interesting("int('55', 12)") assert check_interesting("range(3)") assert check_interesting("range(3, 5)") assert check_interesting("range(3, 5, 1)") assert check_interesting("round(3.14159)") assert check_interesting("round(3.14159, 2)") assert check_interesting("complex()") assert check_interesting("complex(5, 2)") assert check_interesting("list()") assert check_interesting("tuple()") assert check_interesting("dict()") assert check_interesting("bytes()") assert check_interesting("frozenset()") assert check_interesting("bytearray()") assert check_interesting("abs(3)") assert check_interesting("hex(3)") assert check_interesting("bin(3)") assert check_interesting("oct(3)") assert check_interesting("bool(3)") assert check_interesting("chr(3)") assert check_interesting("ord('3')") assert check_interesting("len([CannotEval, len])") assert check_interesting("list([CannotEval, len])") assert check_interesting("tuple([CannotEval, len])") assert check_interesting("str(b'123', 'utf8')") assert check_interesting("bytes('123', 'utf8')") assert check_interesting("bytearray('123', 'utf8')") assert check_interesting("divmod(123, 4)") assert check_interesting("pow(123, 4)") assert check_interesting("id(id)") assert check_interesting("type(id)") assert check_interesting("all([1, 2])") assert check_interesting("any([1, 2])") assert check_interesting("sum([1, 2])") assert check_interesting("sum([len])") is None assert check_interesting("sorted([[1, 2], [3, 4]])") assert check_interesting("min([[1, 2], [3, 4]])") assert check_interesting("max([[1, 2], [3, 4]])") assert check_interesting("hash(((1, 2), (3, 4)))") assert check_interesting("set(((1, 2), (3, 4)))") assert check_interesting("dict(((1, 2), (3, 4)))") assert check_interesting("frozenset(((1, 2), (3, 4)))") assert check_interesting("ascii(((1, 2), (3, 4)))") assert check_interesting("str(((1, 2), (3, 4)))") assert check_interesting("repr(((1, 2), (3, 4)))") def test_unsupported(): with pytest.raises(CannotEval): check_interesting("[x for x in []]") with pytest.raises(CannotEval): check_interesting("{**{}}") with pytest.raises(CannotEval): check_interesting("[*[]]") with pytest.raises(CannotEval): check_interesting("int(*[1])") def test_group_expressions(): x = (1, 2) evaluator = Evaluator({'x': x}) tree = ast.parse('x[0] + x[x[0]]').body[0].value expressions = evaluator.find_expressions(tree) grouped = set( (frozenset(nodes), value) for nodes, value in group_expressions(expressions) ) expected = { (frozenset([tree.left, subscript_item(tree.right)]), x[0]), (frozenset([tree.left.value, subscript_item(tree.right).value, tree.right.value]), x), (frozenset([subscript_item(tree.left), subscript_item(subscript_item(tree.right))]), 0), (frozenset([tree.right]), x[x[0]]), (frozenset([tree]), x[0] + x[x[0]]), } assert grouped == expected grouped = set( (frozenset(nodes), value) for nodes, value in evaluator.interesting_expressions_grouped(tree) ) expected = set( (nodes, value) for nodes, value in expected if value != 0 ) assert grouped == expected def subscript_item(node): if sys.version_info < (3, 9): return node.slice.value else: return node.slice def test_evaluator_wrong_getitem(): evaluator = Evaluator({}) with pytest.raises(TypeError, match="node should be an ast.expr, not 'str'"): # noinspection PyTypeChecker str(evaluator["foo"]) @pytest.mark.parametrize("expr", ["lst[:,:]", "lst[9]"]) def test_cannot_subscript(expr): with pytest.raises(Exception): eval(expr) evaluator = Evaluator({'lst': [1]}) tree = ast.parse(expr) node = tree.body[0].value assert isinstance(node, ast.Subscript) with pytest.raises(CannotEval): str(evaluator[node]) pure_eval-0.2.3/tests/test_getattr_static.py000066400000000000000000000172631464720247600213170ustar00rootroot00000000000000import sys import unittest import types import pytest from pure_eval import CannotEval from pure_eval.my_getattr_static import getattr_static, safe_descriptors_raw class TestGetattrStatic(unittest.TestCase): def assert_getattr(self, thing, attr): self.assertEqual( getattr_static(thing, attr), getattr(thing, attr), ) def assert_cannot_getattr(self, thing, attr): with self.assertRaises(CannotEval): getattr_static(thing, attr) def test_basic(self): class Thing(object): x = object() thing = Thing() self.assert_getattr(thing, 'x') self.assert_cannot_getattr(thing, 'y') def test_inherited(self): class Thing(object): x = object() class OtherThing(Thing): pass something = OtherThing() self.assert_getattr(something, 'x') def test_instance_attr(self): class Thing(object): x = 2 def __init__(self, x): self.x = x thing = Thing(3) self.assert_getattr(thing, 'x') self.assert_getattr(Thing, 'x') del thing.x self.assert_getattr(thing, 'x') def test_property(self): class Thing(object): @property def x(self): raise AttributeError("I'm pretending not to exist") thing = Thing() self.assert_cannot_getattr(thing, 'x') # TODO this should be doable as Thing.x is the property object # It would require checking that type(klass_result) is property and then just returning that self.assert_cannot_getattr(Thing, 'x') def test_descriptor_raises_AttributeError(self): class descriptor(object): def __get__(*_): raise AttributeError("I'm pretending not to exist") desc = descriptor() class Thing(object): x = desc thing = Thing() self.assert_cannot_getattr(thing, 'x') self.assert_cannot_getattr(Thing, 'x') def test_classAttribute(self): class Thing(object): x = object() self.assert_getattr(Thing, 'x') def test_classVirtualAttribute(self): class Thing(object): @types.DynamicClassAttribute def x(self): return self._x _x = object() self.assert_cannot_getattr(Thing(), 'x') self.assert_cannot_getattr(Thing, 'x') def test_inherited_classattribute(self): class Thing(object): x = object() class OtherThing(Thing): pass self.assert_getattr(OtherThing, 'x') def test_slots(self): class Thing(object): y = 'bar' __slots__ = ['x'] def __init__(self): self.x = 'foo' thing = Thing() self.assert_getattr(thing, 'x') self.assert_getattr(Thing, 'x') self.assert_getattr(thing, 'y') self.assert_getattr(Thing, 'y') del thing.x self.assert_cannot_getattr(thing, 'x') def test_metaclass(self): class meta(type): attr = 'foo' class Thing(object, metaclass=meta): pass self.assert_getattr(Thing, 'attr') class SubThing(Thing): pass self.assert_getattr(SubThing, 'attr') class sub(meta): pass class OtherThing(object, metaclass=sub): x = 3 self.assert_getattr(OtherThing, 'attr') class OtherOtherThing(OtherThing): pass self.assert_getattr(OtherOtherThing, 'x') self.assert_getattr(OtherOtherThing, 'attr') def test_no_dict_no_slots(self): self.assert_cannot_getattr(1, 'foo') self.assert_getattr('foo', 'lower') def test_no_dict_no_slots_instance_member(self): # returns descriptor with open(__file__) as handle: self.assert_cannot_getattr(handle, 'name') def test_inherited_slots(self): class Thing(object): __slots__ = ['x'] def __init__(self): self.x = 'foo' class OtherThing(Thing): pass self.assert_getattr(OtherThing(), 'x') def test_descriptor(self): class descriptor(object): def __get__(self, instance, owner): return 3 class Foo(object): d = descriptor() foo = Foo() # for a non data descriptor we return the instance attribute foo.__dict__['d'] = 1 self.assert_getattr(foo, 'd') # if the descriptor is a data-descriptor it would be invoked so we can't get it descriptor.__set__ = lambda s, i, v: None self.assert_cannot_getattr(foo, 'd') del descriptor.__set__ descriptor.__delete__ = lambda s, i, o: None self.assert_cannot_getattr(foo, 'd') def test_metaclass_with_descriptor(self): class descriptor(object): def __get__(self, instance, owner): return 3 class meta(type): d = descriptor() class Thing(object, metaclass=meta): pass self.assert_cannot_getattr(Thing, 'd') def test_class_as_property(self): class Base(object): foo = 3 class Something(Base): @property def __class__(self): return 1 / 0 instance = Something() self.assert_getattr(instance, 'foo') self.assert_getattr(Something, 'foo') def test_mro_as_property(self): class Meta(type): @property def __mro__(self): return 1 / 0 class Base(object): foo = 3 class Something(Base, metaclass=Meta): pass self.assert_getattr(Something(), 'foo') self.assert_getattr(Something, 'foo') def test_dict_as_property(self): class Foo(dict): a = 3 @property def __dict__(self): return 1 / 0 foo = Foo() foo.a = 4 self.assert_cannot_getattr(foo, 'a') self.assert_getattr(Foo, 'a') def test_custom_object_dict(self): class Custom(dict): def get(self, key, default=None): return 1 / 0 __getitem__ = get class Foo(object): a = 3 foo = Foo() foo.__dict__ = Custom() foo.x = 5 self.assert_getattr(foo, 'a') self.assert_getattr(foo, 'x') def test_metaclass_dict_as_property(self): class Meta(type): @property def __dict__(self): return 1 / 0 class Thing(metaclass=Meta): bar = 4 def __init__(self): self.spam = 42 instance = Thing() self.assert_getattr(instance, "spam") # TODO this fails with CannotEval, it doesn't like the __dict__ property, # but it seems that shouldn't actually matter because it's not called # self.assert_getattr(Thing, "bar") def test_module(self): self.assert_getattr(sys, "version") def test_metaclass_with_metaclass_with_dict_as_property(self): class MetaMeta(type): @property def __dict__(self): self.executed = True return dict(spam=42) class Meta(type, metaclass=MetaMeta): executed = False class Thing(metaclass=Meta): pass self.assert_cannot_getattr(Thing, "spam") self.assertFalse(Thing.executed) def test_safe_descriptors_immutable(): for d in safe_descriptors_raw: with pytest.raises((TypeError, AttributeError)): type(d).__get__ = None pure_eval-0.2.3/tests/test_utils.py000066400000000000000000000106551464720247600174340ustar00rootroot00000000000000import ast import inspect import io import os import re import sys import typing from itertools import islice import pytest from pure_eval import CannotEval from pure_eval.utils import ( copy_ast_without_context, safe_name_types, safe_name_samples, safe_name, typing_annotation_samples, is_standard_types, ensure_dict, ) def sys_modules_sources(): for module in sys.modules.values(): try: filename = inspect.getsourcefile(module) except TypeError: continue if not filename: continue filename = os.path.abspath(filename) try: with io.open(filename) as f: source = f.read() except OSError: continue tree = ast.parse(source) yield filename, source, tree def test_sys_modules(): modules = sys_modules_sources() if not os.environ.get('PURE_EVAL_SLOW_TESTS'): modules = islice(modules, 0, 3) for filename, source, tree in modules: print(filename) if not filename.endswith("ast.py"): check_copy_ast_without_context(tree) def check_copy_ast_without_context(tree): tree2 = copy_ast_without_context(tree) dump1 = ast.dump(tree) dump2 = ast.dump(tree2) normalised_dump1 = re.sub( # Two possible matches: # - first one like ", ctx=…" where ", " should be removed # - second one like "(ctx=…" where "(" should be kept ( r"(" r", ctx=(Load|Store|Del)\(\)" r"|" r"(?<=\()ctx=(Load|Store|Del)\(\)" r")" ), "", dump1 ) assert normalised_dump1 == dump2 def test_repr_cannot_eval(): assert repr(CannotEval()) == "CannotEval" def test_safe_name_types(): for f in safe_name_types: with pytest.raises(TypeError): f.__name__ = lambda: 0 def test_safe_name_samples(): for name, f in {**safe_name_samples, **typing_annotation_samples}.items(): assert name == safe_name(f) def test_safe_name_direct(): assert safe_name(list) == "list" assert safe_name(typing.List) == "List" assert safe_name(typing.Union) == "Union" assert safe_name(typing.Optional) == "Optional" assert safe_name(3) is None def test_is_standard_types(): assert is_standard_types(0, check_dict_values=True, deep=True) assert is_standard_types("0", check_dict_values=True, deep=True) assert is_standard_types([0], check_dict_values=True, deep=True) assert is_standard_types({0}, check_dict_values=True, deep=True) assert is_standard_types({0: "0"}, check_dict_values=True, deep=True) assert not is_standard_types(is_standard_types, check_dict_values=True, deep=True) assert not is_standard_types([is_standard_types], check_dict_values=True, deep=True) assert is_standard_types([is_standard_types], check_dict_values=True, deep=False) assert is_standard_types({is_standard_types}, check_dict_values=True, deep=False) assert is_standard_types( {is_standard_types: is_standard_types}, check_dict_values=True, deep=False ) assert not is_standard_types( {is_standard_types: is_standard_types}, check_dict_values=True, deep=True ) assert not is_standard_types( {0: is_standard_types}, check_dict_values=True, deep=True ) assert is_standard_types({0: is_standard_types}, check_dict_values=False, deep=True) assert is_standard_types([[[[[[[{(0,)}]]]]]]], deep=True, check_dict_values=True) assert not is_standard_types( [[[[[[[{(is_standard_types,)}]]]]]]], deep=True, check_dict_values=True ) lst = [] lst.append(lst) assert is_standard_types(lst, deep=False, check_dict_values=True) assert not is_standard_types(lst, deep=True, check_dict_values=True) lst = [0] * 1000000 assert is_standard_types(lst, deep=False, check_dict_values=True) assert is_standard_types(lst[0], deep=True, check_dict_values=True) assert not is_standard_types(lst, deep=True, check_dict_values=True) lst = [[0] * 1000] * 1000 assert is_standard_types(lst, deep=False, check_dict_values=True) assert is_standard_types(lst[0], deep=True, check_dict_values=True) assert not is_standard_types(lst, deep=True, check_dict_values=True) def test_ensure_dict(): assert ensure_dict({}) == {} assert ensure_dict([]) == {} assert ensure_dict('foo') == {} assert ensure_dict({'a': 1}) == {'a': 1} pure_eval-0.2.3/tox.ini000066400000000000000000000001741464720247600150270ustar00rootroot00000000000000[tox] envlist = py{37,38,39,310,311,312,313} [testenv] commands = pytest extras = tests passenv = PURE_EVAL_SLOW_TESTS