├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENCE ├── MANIFEST.in ├── Makefile ├── README.rst ├── ast_tools.py ├── file.txt ├── logo.jpeg ├── setup.cfg ├── setup.py ├── sneklang.py └── test_snek.py /.coveragerc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readevalprint/sneklang/97a89f2699a1e69769ea1ed07af2d65bf97a4ce9/.coveragerc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *,cover 2 | *.swp 3 | *.pyc 4 | build 5 | dist 6 | MANIFEST 7 | .idea 8 | .tox 9 | 10 | testfile.txt 11 | 12 | 13 | # Created by https://www.gitignore.io/api/python,vim,pytest 14 | # Edit at https://www.gitignore.io/?templates=python,vim,pytest 15 | 16 | #!! ERROR: pytest is undefined. Use list command to see defined gitignore types !!# 17 | 18 | ### Python ### 19 | # Byte-compiled / optimized / DLL files 20 | __pycache__/ 21 | *.py[cod] 22 | *$py.class 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | build/ 30 | develop-eggs/ 31 | dist/ 32 | downloads/ 33 | eggs/ 34 | .eggs/ 35 | lib/ 36 | lib64/ 37 | parts/ 38 | sdist/ 39 | var/ 40 | wheels/ 41 | pip-wheel-metadata/ 42 | share/python-wheels/ 43 | *.egg-info/ 44 | .installed.cfg 45 | *.egg 46 | MANIFEST 47 | 48 | # PyInstaller 49 | # Usually these files are written by a python script from a template 50 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 51 | *.manifest 52 | *.spec 53 | 54 | # Installer logs 55 | pip-log.txt 56 | pip-delete-this-directory.txt 57 | 58 | # Unit test / coverage reports 59 | htmlcov/ 60 | .tox/ 61 | .nox/ 62 | .coverage 63 | .coverage.* 64 | .cache 65 | nosetests.xml 66 | coverage.xml 67 | *.cover 68 | .hypothesis/ 69 | .pytest_cache/ 70 | 71 | # Translations 72 | *.mo 73 | *.pot 74 | 75 | # Django stuff: 76 | *.log 77 | local_settings.py 78 | db.sqlite3 79 | 80 | # Flask stuff: 81 | instance/ 82 | .webassets-cache 83 | 84 | # Scrapy stuff: 85 | .scrapy 86 | 87 | # Sphinx documentation 88 | docs/_build/ 89 | 90 | # PyBuilder 91 | target/ 92 | 93 | # Jupyter Notebook 94 | .ipynb_checkpoints 95 | 96 | # IPython 97 | profile_default/ 98 | ipython_config.py 99 | 100 | # pyenv 101 | .python-version 102 | 103 | # celery beat schedule file 104 | celerybeat-schedule 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | ### Python Patch ### 137 | .venv/ 138 | 139 | ### Vim ### 140 | # Swap 141 | [._]*.s[a-v][a-z] 142 | [._]*.sw[a-p] 143 | [._]s[a-rt-v][a-z] 144 | [._]ss[a-gi-z] 145 | [._]sw[a-p] 146 | 147 | # Session 148 | Session.vim 149 | 150 | # Temporary 151 | .netrwhist 152 | *~ 153 | # Auto-generated tag files 154 | tags 155 | # Persistent undo 156 | [._]*.un~ 157 | 158 | # End of https://www.gitignore.io/api/python,vim,pytest 159 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | 4 | 5 | matrix: 6 | include: 7 | - python: 3.7 8 | env: TOXENV=clean,py37,report 9 | - python: 3.8 10 | env: TOXENV=clean,py38,report 11 | 12 | branches: 13 | only: 14 | - master 15 | 16 | 17 | install: 18 | - pip install coveralls flake8 pytest-cov tox 19 | - pip install -e . 20 | script: 21 | - make test 22 | after_success: 23 | - coveralls 24 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Sneklang 2 | Copyright (C) 2022 Timothy John Watts 3 | 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include test_snek.py 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | flake8 ./sneklang.py ./test_snek.py 3 | time tox -p all 4 | 5 | autotest: 6 | ls ./sneklang.py ./test_snek.py README.rst | entr make test 7 | 8 | .PHONY: test 9 | 10 | dist/: setup.py sneklang.py README.rst 11 | python setup.py build sdist 12 | twine check dist/* 13 | 14 | pypi: test dist/ 15 | twine check dist/* 16 | twine upload dist/* 17 | 18 | clean: 19 | rm -rf build 20 | rm -rf dist 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | .. image:: logo.jpeg 3 | :target: logo.jpeg 4 | 5 | Sneklang 6 | ======================== 7 | 8 | .. image:: https://travis-ci.org/readevalprint/sneklang.svg?branch=master&4 9 | :target: https://travis-ci.org/readevalprint/sneklang 10 | :alt: Build Status 11 | 12 | .. image:: https://coveralls.io/repos/github/readevalprint/sneklang/badge.svg?branch=master&4 13 | :target: https://coveralls.io/r/readevalprint/sneklang?branch=master 14 | :alt: Coverage Status 15 | 16 | .. image:: https://badge.fury.io/py/sneklang.svg?5 17 | :target: https://badge.fury.io/py/sneklang 18 | :alt: PyPI Version 19 | 20 | Try online 21 | ---------- 22 | 23 | https://sneklang.functup.com 24 | 25 | Supports 26 | -------- 27 | 28 | Python 3.7 or 3.8 29 | 30 | 31 | Basic Usage 32 | ----------- 33 | 34 | ``snek_eval`` returns a list of all the expressions in the provided code. 35 | Generally you care about the last one. 36 | 37 | 38 | To get very simple evaluating: 39 | 40 | .. code-block:: python 41 | 42 | from sneklang import snek_eval 43 | 44 | snek_eval("'Hi!' + ' world!'") 45 | 46 | returns ``[Hi! World!]``. 47 | 48 | Expressions can be as complex and convoluted as you want: 49 | 50 | .. code-block:: python 51 | 52 | snek_eval("21 + 19 / 7 + (8 % 3) ** 9") 53 | 54 | returns ``[535.714285714]``. 55 | 56 | You can add your own functions in as well. 57 | 58 | .. code-block:: python 59 | 60 | snek_eval("square(11)", scope={"square": lambda x: x*x}) 61 | 62 | returns ``[121]``. 63 | 64 | 65 | Try some dictionary or set comprehension. 66 | 67 | .. code-block:: python 68 | 69 | >>> from sneklang import snek_eval 70 | >>> snek_eval("{a:b for a,b in [('a', 1), ('b',2)]}") 71 | [{'a': 1, 'b': 2}] 72 | 73 | >>> snek_eval("{a*a for a in [1,2,3]}") 74 | [{1, 4, 9}] 75 | 76 | 77 | You can even define functions within the sand box at evaluation time. 78 | 79 | .. code-block:: python 80 | 81 | >>> from sneklang import snek_eval 82 | >>> snek_eval(''' 83 | ... def my_function(x): 84 | ... return x + 3 85 | ... 86 | ... my_function(5) 87 | ... 88 | ... ''') 89 | [None, 8] 90 | 91 | 92 | Advanced Usage 93 | -------------- 94 | 95 | 96 | 97 | 98 | Some times you will want to run a dynamically defined sandboxed funtion in your app. 99 | 100 | .. code-block:: python 101 | 102 | >>> user_scope = {} 103 | >>> out = snek_eval(''' 104 | ... def my_function(x=2): 105 | ... return x ** 3 106 | ... ''', scope=user_scope) 107 | >>> user_func = user_scope['my_function'] 108 | >>> user_func() 109 | 8 110 | 111 | 112 | Or maybe create a decorator 113 | 114 | .. code-block:: python 115 | 116 | >>> user_scope = {} 117 | >>> out = snek_eval(''' 118 | ... def foo_decorator(func): 119 | ... def inner(s): 120 | ... return "this is foo", func(s) 121 | ... return inner 122 | ... 123 | ... @foo_decorator 124 | ... def bar(s): 125 | ... return "this is bar", s 126 | ... 127 | ... output = bar("BAZ") 128 | ... ''', scope=user_scope) 129 | >>> user_scope['output'] 130 | ('this is foo', ('this is bar', 'BAZ')) 131 | 132 | 133 | 134 | You can also delete variables and catch exception 135 | 136 | .. code-block:: python 137 | 138 | >>> user_scope = {} 139 | >>> out = snek_eval(''' 140 | ... a = [1, 2, 3, 4, 5, 6, 7] 141 | ... del a[3:5] 142 | ... try: 143 | ... a[10] 144 | ... except Exception as e: 145 | ... b = "We got an error: " + str(e) 146 | ... ''', scope=user_scope) 147 | >>> user_scope['a'] 148 | [1, 2, 3, 6, 7] 149 | >>> user_scope['b'] 150 | "We got an error: IndexError('list index out of range')" 151 | 152 | 153 | 154 | All exceptions will be wrapped in a `SnekRuntimeError` with `__context__` containing the 155 | original exception. 156 | 157 | .. code-block:: python 158 | 159 | >>> user_scope = {} 160 | >>> out = snek_eval(''' 161 | ... try: 162 | ... raise Exception("this is my last resort") 163 | ... except Exception as e: 164 | ... caught_exception = e 165 | ... ''', scope=user_scope) 166 | >>> user_scope['caught_exception'] 167 | SnekRuntimeError("Exception('this is my last resort')") 168 | >>> user_scope['caught_exception'].__context__ 169 | Exception('this is my last resort') 170 | 171 | .. code-block:: python 172 | 173 | >>> user_scope = {} 174 | >>> out = snek_eval(''' 175 | ... try: 176 | ... try: 177 | ... 1/0 178 | ... except Exception as e: 179 | ... raise Exception("Bad math") from e 180 | ... except Exception as e: 181 | ... caught_exception = e 182 | ... ''', scope=user_scope) 183 | >>> user_scope['caught_exception'] 184 | SnekRuntimeError("Exception('Bad math')") 185 | >>> user_scope['caught_exception'].__context__ 186 | Exception('Bad math') 187 | >>> user_scope['caught_exception'].__context__.__context__ 188 | SnekRuntimeError("ZeroDivisionError('division by zero')") 189 | 190 | 191 | And sometimes, users write crappy code... `MAX_CALL_DEPTH` is configurable, of course. 192 | Here you can see some extreamly ineffecient code to multiply a number by 2 193 | 194 | .. code-block:: python 195 | 196 | >>> from sneklang import SnekRuntimeError 197 | >>> user_scope = {} 198 | >>> out = snek_eval(''' 199 | ... def multiply_by_2(x): 200 | ... return (2 + multiply_by_2(x-1)) if x > 0 else 0 201 | ... ''', scope=user_scope) 202 | 203 | >>> multiply_by_2 = user_scope['multiply_by_2'] 204 | >>> multiply_by_2(5) 205 | 10 206 | >>> try: 207 | ... multiply_by_2(50) 208 | ... except SnekRuntimeError as e: 209 | ... print(f'oh no! "{e}" On line:{e.lineno} col:{e.col}') 210 | oh no! "RecursionError('Sorry, stack is to large')" On line:3 col:15 211 | 212 | 213 | 214 | >>> try: 215 | ... snek_eval("int('foo is not a number')") 216 | ... except SnekRuntimeError as e: 217 | ... print('oh no! {}'.format(e)) 218 | oh no! ValueError("invalid literal for int() with base 10: 'foo is not a number'") 219 | 220 | 221 | 222 | Limited Power 223 | ~~~~~~~~~~~~~ 224 | 225 | Also note, the ``**`` operator has been locked down by default to have a 226 | maximum input value of ``4000000``, which makes it somewhat harder to make 227 | expressions which go on for ever. You can change this limit by changing the 228 | ``sneklang.POWER_MAX`` module level value to whatever is an appropriate value 229 | for you (and the hardware that you're running on) or if you want to completely 230 | remove all limitations, you can set the ``s.operators[ast.Pow] = operator.pow`` 231 | or make your own function. 232 | 233 | On my computer, ``9**9**5`` evaluates almost instantly, but ``9**9**6`` takes 234 | over 30 seconds. Since ``9**7`` is ``4782969``, and so over the ``POWER_MAX`` 235 | limit, it throws a ``NumberTooHigh`` exception for you. (Otherwise it would go 236 | on for hours, or until the computer runs out of memory) 237 | 238 | Strings (and other Iterables) Safety 239 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 240 | 241 | There are also limits on string length (100000 characters, 242 | ``MAX_STRING_LENGTH``). This can be changed if you wish. 243 | 244 | Related to this, if you try to create a silly long string/bytes/list, by doing 245 | ``'i want to break free'.split() * 9999999999`` for instance, it will block you. 246 | 247 | If Expressions 248 | -------------- 249 | 250 | You can use python style ``if x then y else z`` type expressions: 251 | 252 | .. code-block:: python 253 | 254 | >>> snek_eval("'equal' if x == y else 'not equal'", scope={"x": 1, "y": 2}) 255 | ['not equal'] 256 | 257 | which, of course, can be nested: 258 | 259 | .. code-block:: python 260 | 261 | >>> snek_eval("'a' if 1 == 2 else 'b' if 2 == 3 else 'c'") 262 | ['c'] 263 | 264 | 265 | Functions 266 | --------- 267 | 268 | You can define functions which you'd like the expresssions to have access to: 269 | 270 | .. code-block:: python 271 | 272 | >>> snek_eval("double(21)", scope={"double": lambda x:x*2}) 273 | [42] 274 | 275 | You can define "real" functions to pass in rather than lambdas, of course too, 276 | and even re-name them so that expressions can be shorter 277 | 278 | .. code-block:: python 279 | 280 | >>> def square(x): 281 | ... return x ** 2 282 | >>> snek_eval("s(10) + square(2)", scope={"s": square, "square":square}) 283 | [104] 284 | 285 | If you don't provide your own ``scope`` dict, then the the following defaults 286 | are provided in the ``DEFAULT_SCOPE`` dict: 287 | 288 | +----------------+--------------------------------------------------+ 289 | | ``int(x)`` | Convert ``x`` to an ``int``. | 290 | +----------------+--------------------------------------------------+ 291 | | ``float(x)`` | Convert ``x`` to a ``float``. | 292 | +----------------+--------------------------------------------------+ 293 | | ``str(x)`` | Convert ``x`` to a ``str`` (``unicode`` in py2) | 294 | +----------------+--------------------------------------------------+ 295 | 296 | .. code-block:: python 297 | 298 | >>> snek_eval("a + b", scope={"a": 11, "b": 100}) 299 | [111] 300 | 301 | >>> snek_eval("a + b", scope={"a": "Hi ", "b": "world!"}) 302 | ['Hi world!'] 303 | 304 | You can also hand the scope of variable enames over to a function, if you prefer: 305 | 306 | .. code-block:: python 307 | 308 | >>> import sneklang 309 | >>> import random 310 | >>> my_scope = {} 311 | >>> my_scope.update( 312 | ... square=(lambda x:x*x), 313 | ... randint=(lambda top: int(random.random() * top)) 314 | ... ) 315 | >>> snek_eval('square(randint(int("1")))', scope=my_scope) 316 | [0] 317 | 318 | 319 | 320 | Other... 321 | -------- 322 | 323 | 324 | Object attributes that start with ``_`` or ``func_`` are disallowed by default. 325 | If you really need that (BE CAREFUL!), then modify the module global 326 | ``sneklang.DISALLOW_PREFIXES``. 327 | 328 | A few builtin functions are listed in ``sneklang.DISALLOW_FUNCTIONS``. ``type``, ``open``, etc. 329 | If you need to give access to this kind of functionality to your expressions, then be very 330 | careful. You'd be better wrapping the functions in your own safe wrappers. 331 | 332 | The initial idea came from J.F. Sebastian on Stack Overflow 333 | ( http://stackoverflow.com/a/9558001/1973500 ) with modifications and many improvements, 334 | see the head of the main file for contributors list. 335 | 336 | Then danthedeckie on Github with simpleeval(https://github.com/danthedeckie/simpleeval) 337 | 338 | I've filled it out a bit more to allow safe funtion definitions, and better scope management. 339 | 340 | Please read the ``test_snek.py`` file for other potential gotchas or 341 | details. I'm very happy to accept pull requests, suggestions, or other issues. 342 | Enjoy! 343 | 344 | Developing 345 | ---------- 346 | 347 | Run tests:: 348 | 349 | $ make test 350 | 351 | Or to set the tests running on every file change: 352 | 353 | $ make autotest 354 | 355 | (requires ``entr``) 356 | 357 | -------------------------------------------------------------------------------- /ast_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | AST Tools - Copyright (c) 2019 Timothy Watts 3 | All rights reserved. 4 | 5 | Collection of useful tools to inspect Pythons AST. 6 | 7 | This file is part of Sneklang and is released under the "GNU Affero General Public License ". 8 | Please see the LICENSE file that should have been included as part of this package. 9 | 10 | """ 11 | 12 | 13 | def parse_ast(node): 14 | # check if this is a node or list 15 | if isinstance(node, list): 16 | result = [] 17 | for child_node in node: # A list of nodes, really 18 | result += [parse_ast(child_node)] 19 | return result 20 | 21 | # A node it seems 22 | if '_ast' == getattr(node, '__module__', False): 23 | result = {} 24 | for k in node.__dict__: 25 | result[k] = parse_ast(getattr(node, k)) 26 | # The original class would be nice if we want to reconstruct the tree 27 | return node.__class__, result 28 | 29 | # Who knows what it is, just return it. 30 | return node 31 | 32 | def deserialize(node): 33 | """ Returns an ast instance from an expanded dict. """ 34 | if isinstance(node, tuple): 35 | klass, kws = node 36 | return klass(**deserialize(kws)) 37 | elif isinstance(node, dict): 38 | d = {} 39 | for k, v in node.items(): 40 | d[k] = deserialize(v) 41 | return d 42 | elif isinstance(node, list): 43 | return [deserialize(n) for n in node] 44 | else: 45 | return node 46 | -------------------------------------------------------------------------------- /file.txt: -------------------------------------------------------------------------------- 1 | 11 2 | -------------------------------------------------------------------------------- /logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readevalprint/sneklang/97a89f2699a1e69769ea1ed07af2d65bf97a4ce9/logo.jpeg -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | [flake8] 3 | max-line-length = 120 4 | ignore=F405,F403,W503 5 | 6 | [isort:settings] 7 | multi_line_output=3 8 | include_trailing_comma=True 9 | force_grid_wrap=0 10 | use_parentheses=True 11 | line_length=88 12 | 13 | [tox:tox] 14 | envlist = clean,py38,py39,py310,report 15 | 16 | [testenv] 17 | changedir = . 18 | deps = 19 | pytest 20 | pytest-cov 21 | {py38,py39,py310}: clean 22 | report: py38,py39,py310 23 | commands = 24 | pytest --cov --cov-append --cov-config=setup.cfg --cov-report=term-missing --doctest-modules --doctest-glob="README.rst" --no-cov-on-fail 25 | 26 | 27 | 28 | [testenv:py{38,39,310}] 29 | depends = 30 | clean 31 | 32 | [testenv:report] 33 | deps = coverage 34 | skip_install = true 35 | depends = 36 | py38 37 | py39 38 | py310 39 | commands = 40 | coverage report --rcfile=setup.cfg 41 | coverage html --rcfile=setup.cfg 42 | 43 | [testenv:clean] 44 | deps = coverage 45 | skip_install = true 46 | commands = 47 | coverage erase --rcfile=setup.cfg 48 | 49 | 50 | [coverage:run] 51 | branch = true 52 | include = 53 | sneklang.py 54 | 55 | [coverage:report] 56 | show_missing = true 57 | fail_under = 90 58 | include = 59 | sneklang.py 60 | 61 | 62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | __version__ = "0.5.2" 4 | 5 | setup( 6 | name="sneklang", 7 | py_modules=["sneklang"], 8 | version=__version__, 9 | description="Experimental minimal subset of Python for safe evaluation", 10 | long_description=open("README.rst", "r").read(), 11 | long_description_content_type="text/x-rst", 12 | author="Timoth Watts", 13 | author_email="tim@readevalprint.com", 14 | url="https://github.com/readevalprint/sneklang", 15 | keywords=["sandbox", "parse", "ast"], 16 | test_suite="test_snek", 17 | install_requires=["pytest", "ConfigArgParse", "python-forge"], 18 | classifiers=[ 19 | "Development Status :: 4 - Beta", 20 | "Intended Audience :: Developers", 21 | "License :: OSI Approved :: MIT License", 22 | "Topic :: Software Development :: Libraries :: Python Modules", 23 | "Programming Language :: Python", 24 | ], 25 | ) 26 | -------------------------------------------------------------------------------- /sneklang.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sneklang - (C) 2020 Timothy Watts 3 | ------------------------------------- 4 | 5 | Minimal subset of Python for safe evaluation 6 | 7 | ------------------------------------- 8 | 9 | Initial idea copied from J.F. Sebastian on Stack Overflow 10 | ( http://stackoverflow.com/a/9558001/1973500 ) with 11 | modifications and many improvements. 12 | 13 | Since then it has been adapted from simpleeval by danthedeckie. 14 | 15 | ------------------------------------- 16 | Basic Usage: 17 | 18 | >>> from sneklang import snek_eval 19 | >>> snek_eval("20 + 30\\n4+4") 20 | [50, 8] 21 | 22 | You can add your own functions easily too: 23 | 24 | if file.txt contents is "11" 25 | 26 | >>> def get_file(): 27 | ... with open("file.txt", 'r') as f: 28 | ... return f.read() 29 | 30 | >>> snek_eval('int(get_file()) + 31', scope = {"get_file": get_file}) 31 | [42] 32 | 33 | For more information, see the full package documentation on pypi, or the github 34 | repo. 35 | 36 | ----------- 37 | 38 | 39 | >>> snek_eval("40 + two", scope={"two": 2}) 40 | [42] 41 | 42 | 43 | 44 | 45 | >>> my_func = snek_eval(''' 46 | ... a = 1 47 | ... def foo(b): 48 | ... c = 3 49 | ... return a, b, c 50 | ... foo 51 | ... ''')[-1] 52 | >>> my_func(2) 53 | (1, 2, 3) 54 | 55 | 56 | 57 | >>> try: 58 | ... snek_eval(''' 59 | ... def fib(n): 60 | ... return ((fib(n-1) + fib(n-2)) if n > 1 else n) 61 | ... fib(7) 62 | ... ''') 63 | ... except RecursionError as e: 64 | ... print("Oh no! The current recursion limit is too low for this: %s" % format(sys.getrecursionlimit())) 65 | [None, 13] 66 | 67 | >>> try: 68 | ... snek_eval(''' 69 | ... def bar(): 70 | ... bar() 71 | ... bar() 72 | ... ''') 73 | ... except SnekRuntimeError as e: # Snek error 74 | ... print(e) 75 | ... except RecursionError as e: # uncaught, this would hit a python error. 76 | ... print("Oh no! The current recursion limit is too low for ths function: %s" % format(sys.getrecursionlimit())) 77 | RecursionError('Sorry, stack is to large') 78 | 79 | """ 80 | 81 | from collections import namedtuple as nt 82 | import re 83 | import ast 84 | import operator as op 85 | import builtins 86 | import sys 87 | import types 88 | import itertools 89 | from collections import Counter, defaultdict 90 | from functools import partial 91 | import inspect 92 | from copy import copy 93 | import forge 94 | 95 | ######################################## 96 | # Module wide 'globals' 97 | MAX_STRING_LENGTH = 100000 98 | MAX_POWER = 100 * 100 # highest exponent 99 | MAX_SCOPE_SIZE = MAX_STRING_LENGTH * 2 100 | MAX_NODE_CALLS = 10000 101 | MAX_CALL_DEPTH = 32 102 | DISALLOW_PREFIXES = ["_"] 103 | DISALLOW_METHODS = [(str, "format"), (type, "mro"), (str, "format_map")] 104 | 105 | 106 | # Disallow functions: 107 | # This, strictly speaking, is not necessary. These /should/ never be accessable anyway, 108 | # if DISALLOW_PREFIXES and DISALLOW_METHODS are all right. This is here to try and help 109 | # people not be stupid. Allowing these functions opens up all sorts of holes - if any of 110 | # their functionality is required, then please wrap them up in a safe container. And think 111 | # very hard about it first. And don't say I didn't warn you. 112 | 113 | DISALLOW_FUNCTIONS = { 114 | type, 115 | eval, 116 | getattr, 117 | setattr, 118 | help, 119 | repr, 120 | compile, 121 | open, 122 | exec, 123 | format, 124 | vars, 125 | zip, 126 | } 127 | 128 | 129 | _whitlist_functions_dict = { 130 | "str": ["join"], 131 | "builtins": [ 132 | "Exception", 133 | "int", 134 | "isinstance", 135 | "issubclass", 136 | "iter", 137 | "len", 138 | "list", 139 | "print", 140 | "str", 141 | "sum", 142 | ], 143 | "dict": ["get", "items", "keys", "values"], 144 | "list": ["sort", "append", "pop", "count", "index", "reverse"], 145 | "script": ["*"], 146 | "__main__": ["*"], 147 | "sneklang": ["*"], 148 | } 149 | 150 | 151 | WHITLIST_FUNCTIONS = [] 152 | 153 | for k, v in _whitlist_functions_dict.items(): 154 | for kk in v: 155 | WHITLIST_FUNCTIONS.append(k + "." + kk) 156 | 157 | 158 | ######################################## 159 | # Exceptions: 160 | 161 | 162 | class Return(Exception): 163 | """Not actually an exception, just a way to break out of the function""" 164 | 165 | def __init__(self, value): 166 | self.value = value 167 | 168 | 169 | class Break(Exception): 170 | """Not actually an exception, just a way to break out of the loop""" 171 | 172 | 173 | class Continue(Exception): 174 | """Not actually an exception, just a way to continue the loop""" 175 | 176 | 177 | class InvalidExpression(Exception): 178 | """Generic Exception""" 179 | 180 | pass 181 | 182 | 183 | class DangerousValue(Exception): 184 | """When you try to pass in something dangerous to snek, it won't catch everything though""" 185 | 186 | def __init__(self, *args): 187 | super().__init__(*args) 188 | 189 | 190 | class SnekRuntimeError(Exception): 191 | """Something caused the Snek code to crash""" 192 | 193 | col = None 194 | lineno = None 195 | 196 | def __init__(self, msg, node=None): 197 | self.__node = node 198 | if node: # pragma: no branch 199 | self.col = getattr(self.__node, "col_offset", None) 200 | self.lineno = getattr(self.__node, "lineno", None) 201 | super().__init__(msg) 202 | 203 | 204 | ######################################## 205 | # Default simple functions to include: 206 | 207 | 208 | def safe_mod(a, b): 209 | """only allow modulo on numbers, not string formating""" 210 | if isinstance(a, str): 211 | raise NotImplementedError("String formating is not supported") 212 | return a % b 213 | 214 | 215 | def safe_power(a, b): # pylint: disable=invalid-name 216 | """a limited exponent/to-the-power-of function, for safety reasons""" 217 | 218 | if abs(a) > MAX_POWER or abs(b) > MAX_POWER: 219 | raise MemoryError("Sorry! I don't want to evaluate {0} ** {1}".format(a, b)) 220 | return a ** b 221 | 222 | 223 | def safe_mult(a, b): # pylint: disable=invalid-name 224 | """limit the number of times an iterable can be repeated...""" 225 | if hasattr(a, "__len__") and b * len(str(a)) >= MAX_SCOPE_SIZE: 226 | raise MemoryError("Sorry, I will not evalute something that long.") 227 | if hasattr(b, "__len__") and a * len(str(b)) >= MAX_SCOPE_SIZE: 228 | raise MemoryError("Sorry, I will not evalute something that long.") 229 | 230 | return a * b 231 | 232 | 233 | def safe_add(a, b): # pylint: disable=invalid-name 234 | """iterable length limit again""" 235 | 236 | if hasattr(a, "__len__") and hasattr(b, "__len__"): 237 | if len(a) + len(b) > MAX_STRING_LENGTH: 238 | raise MemoryError( 239 | "Sorry, adding those two together would make something too long." 240 | ) 241 | return a + b 242 | 243 | 244 | ######################################## 245 | # Defaults for the evaluator: 246 | 247 | BUILTIN_EXCEPTIONS = { 248 | k: v 249 | for k, v in vars(builtins).items() 250 | if issubclass(type(v), type) and issubclass(v, Exception) 251 | } 252 | 253 | DEFAULT_SCOPE = { 254 | "True": True, 255 | "False": False, 256 | "None": None, 257 | "int": int, 258 | "float": float, 259 | "str": str, 260 | "bool": bool, 261 | "list": list, 262 | "tuple": tuple, 263 | "dict": dict, 264 | "set": set, 265 | "len": len, 266 | "min": min, 267 | "max": max, 268 | "any": any, 269 | "all": all, 270 | "round": round, 271 | "sorted": sorted, 272 | "sum": sum, 273 | "isinstance": isinstance, 274 | "enumerate": enumerate, 275 | "isinstance": isinstance, 276 | "issubclass": issubclass, 277 | "iter": iter, 278 | "range": range, 279 | "Exception": Exception, 280 | **BUILTIN_EXCEPTIONS, 281 | } 282 | 283 | 284 | def make_modules(mod_dict): 285 | return { 286 | k: (v.__dict__.update(mod_dict[k]) or v) 287 | for k, v in {k: types.ModuleType(k) for k in mod_dict}.items() 288 | } 289 | 290 | 291 | class Scope(dict): 292 | __slots__ = ("dicts",) 293 | 294 | def __init__(self, mapping=(), **kwargs): 295 | self.dicts = [kwargs] 296 | 297 | def __repr__(self): 298 | return repr(self.dicts[1:]) 299 | 300 | def __copy__(self): 301 | duplicate = copy(super()) 302 | duplicate.dicts = self.dicts[:] 303 | return duplicate 304 | 305 | def push(self, d): 306 | self.dicts.append(d) 307 | 308 | def __iter__(self): 309 | return iter(self.flatten()) 310 | 311 | def __setitem__(self, key, value): 312 | self.dicts[-1][key] = value 313 | 314 | def __getitem__(self, key): 315 | for d in reversed(self.dicts): 316 | if key in d: 317 | return d[key] 318 | raise KeyError(key) 319 | 320 | def __delitem__(self, key): 321 | del self.dicts[-1][key] 322 | 323 | def __contains__(self, key): 324 | return any(key in d for d in self.dicts) 325 | 326 | def flatten(self): 327 | flat = {} 328 | for d in self.dicts: 329 | flat.update(d) 330 | return flat 331 | 332 | def update(self, other_dict): 333 | return self.dicts[-1].update(other_dict) 334 | 335 | def locals(self): 336 | return self.dicts[-1] 337 | 338 | def globals(self): 339 | return self.dicts[1] # layer 0 is builtins 340 | 341 | 342 | class SnekEval(object): 343 | nodes_called = 0 344 | # temp place to track return values 345 | _last_eval_result = None 346 | 347 | def __init__(self, scope=None, modules=None, call_stack=None): 348 | 349 | if call_stack is None: 350 | call_stack = [] 351 | self.call_stack = call_stack 352 | 353 | self.operators = { 354 | ast.Add: safe_add, 355 | ast.Sub: op.sub, 356 | ast.Mult: safe_mult, 357 | ast.Div: op.truediv, 358 | ast.FloorDiv: op.floordiv, 359 | ast.Pow: safe_power, 360 | ast.Mod: safe_mod, 361 | ast.Eq: op.eq, 362 | ast.NotEq: op.ne, 363 | ast.Gt: op.gt, 364 | ast.Lt: op.lt, 365 | ast.GtE: op.ge, 366 | ast.LtE: op.le, 367 | ast.Not: op.not_, 368 | ast.USub: op.neg, 369 | ast.UAdd: op.pos, 370 | ast.In: lambda x, y: op.contains(y, x), 371 | ast.NotIn: lambda x, y: not op.contains(y, x), 372 | ast.Is: op.is_, 373 | ast.IsNot: op.is_not, 374 | ast.BitOr: op.or_, 375 | ast.BitXor: op.xor, 376 | ast.BitAnd: op.and_, 377 | } 378 | 379 | if scope is None: 380 | scope = {} 381 | 382 | self.scope = Scope() 383 | self.scope.update(DEFAULT_SCOPE) 384 | self.scope.push(scope) 385 | 386 | self.modules = {} 387 | if modules is not None: 388 | self.modules = modules 389 | 390 | self.nodes = { 391 | ast.Constant: self._eval_constant, 392 | ast.Num: self._eval_num, 393 | ast.Bytes: self._eval_bytes, 394 | ast.Str: self._eval_str, 395 | ast.Name: self._eval_name, 396 | ast.UnaryOp: self._eval_unaryop, 397 | ast.BinOp: self._eval_binop, 398 | ast.BoolOp: self._eval_boolop, 399 | ast.Compare: self._eval_compare, 400 | ast.IfExp: self._eval_ifexp, 401 | ast.If: self._eval_if, 402 | ast.Try: self._eval_try, 403 | ast.ExceptHandler: self._eval_excepthandler, 404 | ast.Call: self._eval_call, 405 | ast.keyword: self._eval_keyword, 406 | ast.Subscript: self._eval_subscript, 407 | ast.Attribute: self._eval_attribute, 408 | ast.Index: self._eval_index, 409 | ast.Slice: self._eval_slice, 410 | ast.Module: self._eval_module, 411 | ast.Expr: self._eval_expr, 412 | ast.AugAssign: self._eval_augassign, 413 | ast.Assign: self._eval_assign, 414 | ast.Lambda: self._eval_lambda, 415 | ast.FunctionDef: self._eval_functiondef, 416 | ast.arguments: self._eval_arguments, 417 | ast.Return: self._eval_return, 418 | ast.JoinedStr: self._eval_joinedstr, # f-string 419 | ast.NameConstant: self._eval_nameconstant, 420 | ast.FormattedValue: self._eval_formattedvalue, 421 | ast.Dict: self._eval_dict, 422 | ast.Tuple: self._eval_tuple, 423 | ast.List: self._eval_list, 424 | ast.Set: self._eval_set, 425 | ast.ListComp: self._eval_comprehension, 426 | ast.SetComp: self._eval_comprehension, 427 | ast.DictComp: self._eval_comprehension, 428 | # ast.GeneratorExp: self._eval_comprehension, 429 | ast.ImportFrom: self._eval_importfrom, 430 | ast.Import: self._eval_import, 431 | ast.For: self._eval_for, 432 | ast.While: self._eval_while, 433 | ast.Break: self._eval_break, 434 | ast.Continue: self._eval_continue, 435 | ast.Pass: self._eval_pass, 436 | ast.Assert: self._eval_assert, 437 | ast.Delete: self._eval_delete, 438 | ast.Raise: self._eval_raise, 439 | # not really none, these are handled differently 440 | ast.And: None, 441 | ast.Or: None, 442 | ast.Store: None, # Not used, needed for validation 443 | } 444 | 445 | self.assignments = { 446 | ast.Name: self._assign_name, 447 | ast.Tuple: self._assign_tuple_or_list, 448 | ast.Subscript: self._assign_subscript, 449 | ast.List: self._assign_tuple_or_list, 450 | ast.Starred: self._assign_starred, 451 | } 452 | 453 | self.deletions = { 454 | ast.Name: self._delete_name, 455 | ast.Subscript: self._delete_subscript, 456 | } 457 | 458 | # Check for forbidden functions: 459 | for name, func in self.scope.flatten().items(): 460 | if callable(func): 461 | try: 462 | hash(func) 463 | except TypeError: 464 | raise DangerousValue( 465 | "This function '{}' in scope might be a bad idea.".format(name) 466 | ) 467 | if func in DISALLOW_FUNCTIONS: 468 | raise DangerousValue( 469 | "This function '{}' in scope is {} and is in DISALLOW_FUNCTIONS".format( 470 | name, func 471 | ) 472 | ) 473 | 474 | def validate(self, expr): 475 | """Validate that all ast.Nodes are supported by this sandbox""" 476 | tree = ast.parse(expr) 477 | ignored_nodes = set( 478 | [ 479 | ast.Load, 480 | ast.Del, 481 | ast.Starred, 482 | ast.arg, 483 | ast.comprehension, 484 | ast.alias, 485 | ast.GeneratorExp, 486 | ast.ListComp, 487 | ast.DictComp, 488 | ast.SetComp, 489 | ] 490 | ) 491 | valid_nodes = ( 492 | ignored_nodes 493 | | set(self.nodes) 494 | | set(self.operators) 495 | | set(self.deletions) 496 | | set(self.assignments) 497 | ) 498 | for node in ast.walk(tree): 499 | if node.__class__ not in valid_nodes: 500 | exc = NotImplementedError( 501 | f"Sorry, {node.__class__.__name__} is not available in this evaluator" 502 | ) 503 | exc._snek_node = node 504 | raise exc 505 | compile(expr, "", "exec", dont_inherit=True) 506 | 507 | def eval(self, expr): 508 | """evaluate an expresssion, using the operators, functions and 509 | scope previously set up.""" 510 | 511 | # set a copy of the expression aside, so we can give nice errors... 512 | self.expr = expr 513 | try: 514 | self.validate(expr) 515 | except (Exception,) as e: 516 | exc = e 517 | node = None 518 | if hasattr(exc, "_snek_node"): # pragma: no branch 519 | node = exc._snek_node 520 | raise SnekRuntimeError(repr(exc), node=node) from exc 521 | 522 | # and evaluate: 523 | return self._eval(ast.parse(expr)) 524 | 525 | def _eval(self, node): 526 | """The internal evaluator used on each node in the parsed tree.""" 527 | 528 | try: 529 | try: 530 | lineno = getattr(node, "lineno", None) # noqa: F841 531 | col = getattr(node, "col", None) # noqa: F841 532 | 533 | try: 534 | handler = self.nodes[type(node)] 535 | except KeyError: 536 | raise NotImplementedError( 537 | "Sorry, {0} is not available in this " 538 | "evaluator".format(type(node).__name__) 539 | ) 540 | node.call_stack = self.call_stack 541 | self._last_eval_result = handler(node) 542 | return self._last_eval_result 543 | finally: 544 | self.track(node) 545 | except (Return, Break, Continue, SnekRuntimeError): 546 | raise 547 | except Exception as e: 548 | exc = e 549 | if not hasattr(exc, "_snek_node"): # pragma: no branch 550 | exc._snek_node = node 551 | raise SnekRuntimeError(repr(exc), node=node) from exc 552 | 553 | def _eval_assert(self, node): 554 | if not self._eval(node.test): 555 | if node.msg: 556 | raise AssertionError(self._eval(node.msg)) 557 | raise AssertionError() 558 | 559 | def _eval_while(self, node): 560 | while self._eval(node.test): 561 | try: 562 | for b in node.body: 563 | self._eval(b) 564 | except Break: 565 | break 566 | except Continue: 567 | continue 568 | else: 569 | for b in node.orelse: 570 | self._eval(b) 571 | 572 | def _eval_for(self, node): 573 | def recurse_targets(target, value): 574 | """ 575 | Recursively (enter, (into, (nested, name), unpacking)) = \ 576 | and, (assign, (values, to), each 577 | """ 578 | self.track(target) 579 | if isinstance(target, ast.Name): 580 | self.scope[target.id] = value 581 | else: 582 | for t, v in zip(target.elts, value): 583 | recurse_targets(t, v) 584 | 585 | for v in self._eval(node.iter): 586 | recurse_targets(node.target, v) 587 | try: 588 | for b in node.body: 589 | self._eval(b) 590 | except Break: 591 | break 592 | except Continue: 593 | continue 594 | else: 595 | for b in node.orelse: 596 | self._eval(b) 597 | 598 | def _eval_import(self, node): 599 | for alias in node.names: 600 | asname = alias.asname or alias.name 601 | try: 602 | self.scope[asname] = self.modules[alias.name] 603 | except KeyError: 604 | raise ModuleNotFoundError(alias.name) 605 | 606 | def _eval_importfrom(self, node): 607 | for alias in node.names: 608 | asname = alias.asname or alias.name 609 | try: 610 | module = self.modules[node.module] 611 | except KeyError: 612 | raise ModuleNotFoundError(node.module) 613 | if alias.name == "*": 614 | self.scope.update(module.__dict__) 615 | else: 616 | try: 617 | submodule = module.__dict__[alias.name] 618 | self.scope[asname] = submodule 619 | except KeyError: 620 | raise ImportError(alias.name) 621 | 622 | def _eval_expr(self, node): 623 | return self._eval(node.value) 624 | 625 | def _eval_module(self, node): 626 | return [self._eval(b) for b in node.body] 627 | 628 | def _eval_arguments(self, node): 629 | NONEXISTANT_DEFAULT = object() # a unique object to contrast with None 630 | posonlyargs_and_defaults = [] 631 | num_args = len(node.args) 632 | if hasattr(node, "posonlyargs"): 633 | for (arg, default) in itertools.zip_longest( 634 | node.posonlyargs[::-1], 635 | node.defaults[::-1][num_args:], 636 | fillvalue=NONEXISTANT_DEFAULT, 637 | ): 638 | if default is NONEXISTANT_DEFAULT: 639 | posonlyargs_and_defaults.append(forge.pos(arg.arg)) 640 | else: 641 | posonlyargs_and_defaults.append( 642 | forge.pos(arg.arg, default=self._eval(default)) 643 | ) 644 | posonlyargs_and_defaults.reverse() 645 | 646 | args_and_defaults = [] 647 | for (arg, default) in itertools.zip_longest( 648 | node.args[::-1], 649 | node.defaults[::-1][:num_args], 650 | fillvalue=NONEXISTANT_DEFAULT, 651 | ): 652 | if default is NONEXISTANT_DEFAULT: 653 | args_and_defaults.append(forge.arg(arg.arg)) 654 | else: 655 | args_and_defaults.append( 656 | forge.arg(arg.arg, default=self._eval(default)) 657 | ) 658 | args_and_defaults.reverse() 659 | vpo = (node.vararg and forge.args(node.vararg.arg)) or [] 660 | 661 | kwonlyargs_and_defaults = [] 662 | # kwonlyargs is 1:1 to kw_defaults, no need to jump through hoops 663 | for (arg, default) in zip(node.kwonlyargs, node.kw_defaults): 664 | if not default: 665 | kwonlyargs_and_defaults.append(forge.kwo(arg.arg)) 666 | else: 667 | kwonlyargs_and_defaults.append( 668 | forge.kwo(arg.arg, default=self._eval(default)) 669 | ) 670 | vkw = (node.kwarg and forge.kwargs(node.kwarg.arg)) or {} 671 | 672 | return ( 673 | [ 674 | *posonlyargs_and_defaults, 675 | *args_and_defaults, 676 | *vpo, 677 | *kwonlyargs_and_defaults, 678 | ], 679 | vkw, 680 | ) 681 | 682 | def _eval_break(self, node): 683 | raise Break() 684 | 685 | def _eval_continue(self, node): 686 | raise Continue() 687 | 688 | def _eval_pass(self, node): 689 | pass 690 | 691 | def _eval_return(self, node): 692 | ret = None 693 | if node.value is not None: 694 | ret = self._eval(node.value) 695 | raise Return(ret) 696 | 697 | def _eval_lambda(self, node): 698 | 699 | sig_list, sig_dict = self._eval(node.args) 700 | _class = self.__class__ 701 | 702 | def _func(*args, **kwargs): 703 | local_scope = { 704 | inspect.getfullargspec(_func).varargs: args, 705 | **{ 706 | kwo: kwargs.pop(kwo) 707 | for kwo in inspect.getfullargspec(_func).kwonlyargs 708 | + inspect.getfullargspec(_func).args 709 | }, 710 | inspect.getfullargspec(_func).varkw: kwargs, 711 | } 712 | s = _class( 713 | modules=self.modules, scope=copy(self.scope), call_stack=self.call_stack 714 | ) 715 | s.scope.push(local_scope) 716 | s.expr = self.expr 717 | s.track = self.track 718 | return s._eval(node.body) 719 | 720 | _func = forge.sign(*sig_list, **sig_dict)(_func) 721 | del _func.__wrapped__ 722 | _func.__name__ = "" 723 | _func.__qualname__ = "" 724 | _func.__module__ = "script" 725 | 726 | return _func 727 | 728 | def _eval_functiondef(self, node): 729 | 730 | sig_list, sig_dict = self._eval(node.args) 731 | _annotations = { 732 | a.arg: self._eval(a.annotation) 733 | for a in node.args.args 734 | + getattr(node.args, "posonlyargs", [None]) # for backwards compat 735 | + getattr(node.args, "kwonlyargs", [None]) # for backwards compat 736 | + [node.args.kwarg] # is a single element 737 | if a and a.annotation 738 | } 739 | _class = self.__class__ 740 | 741 | def _func(*args, **kwargs): 742 | # reconostruct what the orignial function arguments would have been 743 | local_scope = { 744 | inspect.getfullargspec(_func).varargs: args, 745 | **{ 746 | kwo: kwargs.pop(kwo) 747 | for kwo in inspect.getfullargspec(_func).kwonlyargs 748 | + inspect.getfullargspec(_func).args 749 | }, 750 | inspect.getfullargspec(_func).varkw: kwargs, 751 | } 752 | s = _class( 753 | modules=self.modules, scope=copy(self.scope), call_stack=self.call_stack 754 | ) 755 | s.scope.push(local_scope) 756 | s.expr = self.expr 757 | s.track = self.track 758 | for b in node.body: 759 | try: 760 | s._eval(b) 761 | except Return as r: 762 | return r.value 763 | 764 | _func.__name__ = node.name 765 | _func.__module__ = "script" 766 | _func.__annotations__ = _annotations 767 | _func.__qualname__ = node.name 768 | _func = forge.sign(*sig_list, **sig_dict)(_func) 769 | 770 | # prevent unwrap from detecting this nested function 771 | del _func.__wrapped__ 772 | _func.__doc__ = ast.get_docstring(node) 773 | 774 | decorated_func = _func 775 | decorators = [self._eval(d) for d in node.decorator_list] 776 | for decorator in decorators[::-1]: 777 | decorated_func = decorator(decorated_func) 778 | 779 | self.scope[node.name] = decorated_func 780 | 781 | def _assign_tuple_or_list(self, node, values): 782 | try: 783 | iter(values) 784 | except TypeError: 785 | raise TypeError( 786 | f"cannot unpack non-iterable { type(values).__name__ } object" 787 | ) 788 | len_elts = len(node.elts) 789 | len_values = len(values) 790 | starred_indexes = [ 791 | i for i, n in enumerate(node.elts) if isinstance(n, ast.Starred) 792 | ] 793 | if len(starred_indexes) == 1: 794 | if len_elts - 1 > len_values: 795 | raise ValueError( 796 | f"not enough values to unpack (expected at least { len_elts - 1 }, got { len_values })" 797 | ) 798 | starred_index = starred_indexes[0] 799 | before_slice = slice( 800 | starred_index, len_values - (len_elts - starred_index - 1) 801 | ) 802 | after_slice = slice(len_values - (len_elts - starred_index - 1), len_values) 803 | starred_values = ( 804 | *values[:starred_index], 805 | list(values[before_slice]), 806 | *values[after_slice], 807 | ) 808 | for target, value in zip(node.elts, starred_values): 809 | handler = self.assignments[type(target)] 810 | handler(target, value) 811 | 812 | else: 813 | if len_elts > len_values: 814 | raise ValueError( 815 | f"not enough values to unpack (expected { len_elts }, got { len_values })" 816 | ) 817 | elif len_elts < len_values: 818 | raise ValueError(f"too many values to unpack (expected { len_elts })") 819 | 820 | for target, value in zip(node.elts, values): 821 | handler = self.assignments[type(target)] 822 | handler(target, value) 823 | 824 | def _assign_name(self, node, value): 825 | self.scope[node.id] = value 826 | return value 827 | 828 | def _assign_subscript(self, node, value): 829 | _slice = self._eval(node.slice) 830 | self._eval(node.value)[_slice] = value 831 | return value 832 | 833 | def _assign_starred(self, node, value): 834 | return self._assign([node.value], value) 835 | 836 | def _delete(self, targets): 837 | if len(targets) > 1: 838 | raise NotImplementedError( 839 | "Sorry, cannot delete {} targets.".format(len(targets)) 840 | ) 841 | target = targets[0] 842 | try: 843 | handler = self.deletions[type(target)] 844 | handler(target) 845 | except KeyError: 846 | raise NotImplementedError( 847 | "Sorry, cannot delete {}".format(type(target).__name__) 848 | ) 849 | 850 | def _delete_name(self, node): 851 | del self.scope[node.id] 852 | 853 | def _delete_subscript(self, node): 854 | _slice = self._eval(node.slice) 855 | del self._eval(node.value)[_slice] 856 | 857 | def _eval_delete(self, node): 858 | return self._delete(node.targets) 859 | 860 | def _eval_raise(self, node): 861 | exc = self._eval(node.exc) 862 | exc.node = node 863 | if node.cause is not None: 864 | cause = self._eval(node.cause) 865 | raise exc from cause 866 | raise exc 867 | 868 | def _assign(self, targets, value): 869 | for target in targets: 870 | try: 871 | handler = self.assignments[type(target)] 872 | self.track(target) 873 | except KeyError: # pragma: no cover 874 | # This is caught in validate() 875 | raise NotImplementedError( 876 | "Sorry, cannot assign to {0}".format(type(target).__name__) 877 | ) 878 | handler(target, value) 879 | 880 | def _eval_augassign(self, node): 881 | if ( 882 | len(self.scope.dicts) > 2 # 0 is builtins, 1 globals, then local scope 883 | and hasattr(node.target, "id") 884 | and node.target.id not in self.scope.dicts[-1] 885 | ): 886 | raise UnboundLocalError( 887 | f"local variable '{node.target.id}' referenced before assignment" 888 | ) 889 | try: 890 | value = self.operators[type(node.op)]( 891 | self._eval(node.target), self._eval(node.value) 892 | ) 893 | except KeyError: # pragma: no cover 894 | # This is caught in validate() 895 | raise NotImplementedError( 896 | "Sorry, {0} is not available in this " 897 | "evaluator".format(type(node.op).__name__) 898 | ) 899 | return self._assign([node.target], value) 900 | 901 | def _eval_assign(self, node): 902 | value = self._eval(node.value) 903 | return self._assign(node.targets, value) 904 | 905 | @staticmethod 906 | def _eval_constant(node): 907 | if len(repr(node.value)) > MAX_STRING_LENGTH: 908 | raise MemoryError( 909 | "Value is too large ({0} > {1} )".format( 910 | len(repr(node.value)), MAX_STRING_LENGTH 911 | ) 912 | ) 913 | return node.value 914 | 915 | @staticmethod 916 | def _eval_num(node): 917 | if len(repr(node.n)) > MAX_STRING_LENGTH: 918 | raise MemoryError( 919 | "Value is too large ({0} > {1} )".format( 920 | len(repr(node.n)), MAX_STRING_LENGTH 921 | ) 922 | ) 923 | return node.n 924 | 925 | @staticmethod 926 | def _eval_bytes(node): 927 | if len(node.s) > MAX_STRING_LENGTH: 928 | raise MemoryError( 929 | "Byte Literal in statement is too long!" 930 | " ({0}, when {1} is max)".format(len(node.s), MAX_STRING_LENGTH) 931 | ) 932 | return node.s 933 | 934 | @staticmethod 935 | def _eval_str(node): 936 | if len(node.s) > MAX_STRING_LENGTH: 937 | raise MemoryError( 938 | "String Literal in statement is too long!" 939 | " ({0}, when {1} is max)".format(len(node.s), MAX_STRING_LENGTH) 940 | ) 941 | return node.s 942 | 943 | @staticmethod 944 | def _eval_nameconstant(node): 945 | return node.value 946 | 947 | def _eval_unaryop(self, node): 948 | return self.operators[type(node.op)](self._eval(node.operand)) 949 | 950 | def _eval_binop(self, node): 951 | try: 952 | return self.operators[type(node.op)]( 953 | self._eval(node.left), self._eval(node.right) 954 | ) 955 | except KeyError: # pragma: no cover 956 | # This is caught in validate() 957 | raise NotImplementedError( 958 | "Sorry, {0} is not available in this " 959 | "evaluator".format(type(node.op).__name__) 960 | ) 961 | 962 | def _eval_boolop(self, node): 963 | if isinstance(node.op, ast.And): 964 | vout = False 965 | for value in node.values: 966 | vout = self._eval(value) 967 | if not vout: 968 | return vout 969 | return vout 970 | elif isinstance(node.op, ast.Or): 971 | for value in node.values: 972 | vout = self._eval(value) 973 | if vout: 974 | return vout 975 | return vout 976 | else: # pragma: no cover 977 | # This should never happen as there are only two bool operators And and Or 978 | raise NotImplementedError( 979 | "Sorry, {0} is not available in this " 980 | "evaluator".format(type(node).__name__) 981 | ) 982 | 983 | def _eval_compare(self, node): 984 | right = self._eval(node.left) 985 | to_return = True 986 | for operation, comp in zip(node.ops, node.comparators): 987 | if not to_return: 988 | break 989 | left = right 990 | right = self._eval(comp) 991 | to_return = self.operators[type(operation)](left, right) 992 | return to_return 993 | 994 | def _eval_ifexp(self, node): 995 | return ( 996 | self._eval(node.body) if self._eval(node.test) else self._eval(node.orelse) 997 | ) 998 | 999 | def _eval_if(self, node): 1000 | if self._eval(node.test): 1001 | [self._eval(b) for b in node.body] 1002 | else: 1003 | [self._eval(b) for b in node.orelse] 1004 | 1005 | def _eval_try(self, node): 1006 | try: 1007 | for b in node.body: 1008 | self._eval(b) 1009 | except: # noqa: E722 1010 | caught = False 1011 | for h in node.handlers: 1012 | if self._eval(h): 1013 | caught = True 1014 | break 1015 | if not caught: 1016 | raise 1017 | else: 1018 | [self._eval(oe) for oe in node.orelse] 1019 | finally: 1020 | [self._eval(f) for f in node.finalbody] 1021 | 1022 | def _eval_excepthandler(self, node): 1023 | _type, exc, traceback = sys.exc_info() 1024 | if isinstance(exc, Return): 1025 | return False 1026 | if ( 1027 | (node.type is None) 1028 | or isinstance(exc, self._eval(node.type)) 1029 | or ( 1030 | isinstance(exc, SnekRuntimeError) 1031 | and isinstance(exc.__context__, self._eval(node.type)) 1032 | ) 1033 | ): 1034 | # Surprisingly this is how python does it 1035 | # See: https://docs.python.org/3/reference/compound_stmts.html#the-try-statement 1036 | if node.name: 1037 | self.scope[node.name] = exc 1038 | try: 1039 | [self._eval(b) for b in node.body] 1040 | finally: 1041 | if node.name in self.scope: 1042 | del self.scope[node.name] 1043 | return True 1044 | return False 1045 | 1046 | def _eval_call(self, node): 1047 | if len(self.call_stack) >= MAX_CALL_DEPTH: 1048 | raise RecursionError("Sorry, stack is to large") 1049 | func = self._eval(node.func) 1050 | if not callable(func): 1051 | raise TypeError( 1052 | "Sorry, {} type is not callable".format(type(func).__name__) 1053 | ) 1054 | 1055 | modname = getattr(func, "__module__", None) 1056 | qualname = getattr(func, "__qualname__", None) 1057 | 1058 | if modname: 1059 | fullname = modname + "." + qualname 1060 | wildcard = modname + ".*" 1061 | else: 1062 | fullname = qualname 1063 | wildcard = ".".join(fullname.split(".")[:-1] + ["*"]) 1064 | 1065 | if func in DISALLOW_FUNCTIONS: 1066 | raise DangerousValue(f"This function is forbidden: {fullname}") 1067 | 1068 | if fullname not in WHITLIST_FUNCTIONS and wildcard not in WHITLIST_FUNCTIONS: 1069 | raise NotImplementedError( 1070 | "This function is not allowed: {}".format(fullname) 1071 | ) 1072 | kwarg_kwargs = [self._eval(k) for k in node.keywords] 1073 | 1074 | f = func 1075 | for a in node.args: 1076 | if a.__class__ == ast.Starred: 1077 | args = self._eval(a.value) 1078 | else: 1079 | args = [self._eval(a)] 1080 | f = partial(f, *args) 1081 | for kwargs in kwarg_kwargs: 1082 | f = partial(f, **kwargs) 1083 | 1084 | self.call_stack.append([node, self.expr]) 1085 | ret = f() 1086 | self.call_stack.pop() 1087 | return ret 1088 | 1089 | def _eval_keyword(self, node): 1090 | if node.arg is not None: 1091 | return {node.arg: self._eval(node.value)} 1092 | # Not possible until kwargs are enabled 1093 | return self._eval(node.value) 1094 | 1095 | def _eval_name(self, node): 1096 | try: 1097 | return self.scope[node.id] 1098 | except KeyError: 1099 | msg = "'{0}' is not defined".format(node.id) 1100 | raise NameError(msg) 1101 | # raise NameNotDefined(node) 1102 | 1103 | def _eval_subscript(self, node): 1104 | container = self._eval(node.value) 1105 | key = self._eval(node.slice) 1106 | return container[key] 1107 | 1108 | def _eval_attribute(self, node): 1109 | for prefix in DISALLOW_PREFIXES: 1110 | if node.attr.startswith(prefix): 1111 | raise NotImplementedError( 1112 | "Sorry, access to this attribute " 1113 | "is not available. " 1114 | "({0})".format(node.attr) 1115 | ) 1116 | # eval node 1117 | node_evaluated = self._eval(node.value) 1118 | if (type(node_evaluated), node.attr) in DISALLOW_METHODS: 1119 | raise DangerousValue( 1120 | "Sorry, this method is not available. " 1121 | "({0}.{1})".format(node_evaluated.__class__.__name__, node.attr) 1122 | ) 1123 | return getattr(node_evaluated, node.attr) 1124 | 1125 | def _eval_index(self, node): 1126 | return self._eval(node.value) 1127 | 1128 | def _eval_slice(self, node): 1129 | lower = upper = step = None 1130 | if node.lower is not None: 1131 | lower = self._eval(node.lower) 1132 | if node.upper is not None: 1133 | upper = self._eval(node.upper) 1134 | if node.step is not None: 1135 | step = self._eval(node.step) 1136 | return slice(lower, upper, step) 1137 | 1138 | def _eval_joinedstr(self, node): 1139 | length = 0 1140 | evaluated_values = [] 1141 | for n in node.values: 1142 | val = str(self._eval(n)) 1143 | if len(val) + length > MAX_STRING_LENGTH: 1144 | raise MemoryError("Sorry, I will not evaluate something this long.") 1145 | length += len(val) 1146 | evaluated_values.append(val) 1147 | return "".join(evaluated_values) 1148 | 1149 | def _eval_formattedvalue(self, node): 1150 | if node.format_spec: 1151 | # from https://stackoverflow.com/a/44553570/260366 1152 | 1153 | format_spec = self._eval(node.format_spec) 1154 | r = r"(([\s\S])?([<>=\^]))?([\+\- ])?([#])?([0])?(\d*)([,])?((\.)(\d*))?([sbcdoxXneEfFgGn%])?" 1155 | FormatSpec = nt( 1156 | "FormatSpec", 1157 | "fill align sign alt zero_padding width comma decimal precision type", 1158 | ) 1159 | match = re.fullmatch(r, format_spec) 1160 | 1161 | if match: 1162 | parsed_spec = FormatSpec( 1163 | *match.group(2, 3, 4, 5, 6, 7, 8, 10, 11, 12) 1164 | ) # skip groups not interested in 1165 | if int(parsed_spec.width or 0) > 100: 1166 | raise MemoryError("Sorry, this format width is too long.") 1167 | 1168 | if int(parsed_spec.precision or 0) > 100: 1169 | raise MemoryError("Sorry, this format precision is too long.") 1170 | 1171 | conversion_dict = {-1: "", 115: "!s", 114: "!r", 97: "!a"} 1172 | 1173 | fmt = "{" + conversion_dict[node.conversion] + ":" + format_spec + "}" 1174 | return fmt.format(self._eval(node.value)) 1175 | return self._eval(node.value) 1176 | 1177 | def _eval_dict(self, node): 1178 | if len(node.keys) > MAX_STRING_LENGTH: 1179 | raise MemoryError("Dict in statement is too long!") 1180 | res = {} 1181 | for (k, v) in zip(node.keys, node.values): 1182 | if k is None: 1183 | res.update(self._eval(v)) 1184 | else: 1185 | res[self._eval(k)] = self._eval(v) 1186 | 1187 | return res 1188 | 1189 | def _eval_tuple(self, node): 1190 | if len(node.elts) > MAX_STRING_LENGTH: 1191 | raise MemoryError("Tuple in statement is too long!") 1192 | return tuple(self._eval(x) for x in node.elts) 1193 | 1194 | def _eval_list(self, node): 1195 | if len(node.elts) > MAX_STRING_LENGTH: 1196 | raise MemoryError("List in statement is too long!") 1197 | return list(self._eval(x) for x in node.elts) 1198 | 1199 | def _eval_set(self, node): 1200 | if len(node.elts) > MAX_STRING_LENGTH: 1201 | raise MemoryError("Set in statement is too long!") 1202 | return set(self._eval(x) for x in node.elts) 1203 | 1204 | def track(self, node): 1205 | if hasattr(node, "nodes_called"): 1206 | return 1207 | 1208 | self.nodes_called += 1 1209 | if self.nodes_called > MAX_NODE_CALLS: 1210 | raise TimeoutError("This program has too many evaluations") 1211 | size = len(repr(self.scope)) + len(repr(self._last_eval_result)) 1212 | if size > MAX_SCOPE_SIZE: 1213 | raise MemoryError("Scope has used too much memory") 1214 | 1215 | def _eval_comprehension(self, node): 1216 | 1217 | if isinstance(node, ast.ListComp): 1218 | to_return = list() 1219 | elif isinstance(node, ast.DictComp): 1220 | to_return = dict() 1221 | elif isinstance(node, ast.SetComp): 1222 | to_return = set() 1223 | else: # pragma: no cover 1224 | raise Exception("should never happen") 1225 | 1226 | self.scope.push({}) 1227 | 1228 | def recurse_targets(target, value): 1229 | """ 1230 | Recursively (enter, (into, (nested, name), unpacking)) = \ 1231 | and, (assign, (values, to), each 1232 | """ 1233 | self.track(target) 1234 | if isinstance(target, ast.Name): 1235 | self.scope[target.id] = value 1236 | else: 1237 | for t, v in zip(target.elts, value): 1238 | recurse_targets(t, v) 1239 | 1240 | def do_generator(gi=0): 1241 | g = node.generators[gi] 1242 | 1243 | for i in self._eval(g.iter): 1244 | recurse_targets(g.target, i) 1245 | if all(self._eval(iff) for iff in g.ifs): 1246 | if len(node.generators) > gi + 1: 1247 | do_generator(gi + 1) 1248 | else: 1249 | if isinstance(node, ast.ListComp): 1250 | to_return.append(self._eval(node.elt)) 1251 | elif isinstance(node, ast.DictComp): 1252 | to_return[self._eval(node.key)] = self._eval(node.value) 1253 | elif isinstance(node, ast.SetComp): 1254 | to_return.add(self._eval(node.elt)) 1255 | else: # pragma: no cover 1256 | raise Exception("should never happen") 1257 | 1258 | do_generator() 1259 | 1260 | self.scope.dicts.pop() 1261 | return to_return 1262 | 1263 | 1264 | def snek_eval(expr, scope=None, call_stack=None, module_dict=None): 1265 | """Simply evaluate an expresssion""" 1266 | 1267 | modules = None 1268 | if module_dict: 1269 | modules = make_modules(module_dict) 1270 | 1271 | s = SnekEval(scope=scope, modules=modules, call_stack=call_stack) 1272 | return s.eval(expr) 1273 | 1274 | 1275 | class SnekCoverage(SnekEval): 1276 | 1277 | seen_nodes = defaultdict(int) 1278 | 1279 | def __init__(self, *args, **kwargs): 1280 | return super(SnekCoverage, self).__init__(*args, **kwargs) 1281 | 1282 | def eval(self, expr): 1283 | self.seen_nodes = { 1284 | (n.lineno, n.col_offset, n.__class__.__name__): 0 1285 | for n in ast.walk(ast.parse(expr)) 1286 | if hasattr(n, "col_offset") 1287 | } 1288 | return super(SnekCoverage, self).eval(expr) 1289 | 1290 | def _assign(self, targets, value): 1291 | ret = super(SnekCoverage, self)._assign(targets, value) 1292 | # currently only one target is allowed, but still. 1293 | for node in targets: 1294 | self.track(node) 1295 | return ret 1296 | 1297 | def _eval(self, node): 1298 | ret = super(SnekCoverage, self)._eval(node) 1299 | self.track(node) 1300 | return ret 1301 | 1302 | def _eval_arguments(self, node): 1303 | ret = super(SnekCoverage, self)._eval_arguments(node) 1304 | for node_arg in node.args: 1305 | self.track(node_arg) 1306 | return ret 1307 | 1308 | def track(self, node): 1309 | if hasattr(node, "seen_nodes"): 1310 | xx = Counter(node.seen_nodes) 1311 | yy = Counter(self.seen_nodes) 1312 | xx.update(yy) 1313 | self.seen_nodes = dict(xx) 1314 | if hasattr(node, "col_offset"): 1315 | self.seen_nodes[ 1316 | (node.lineno, node.col_offset, node.__class__.__name__) 1317 | ] += 1 1318 | 1319 | 1320 | def snek_test_coverage(expr, scope=None, call_stack=None, module_dict=None): 1321 | """Run all test_* function in this expression""" 1322 | 1323 | modules = make_modules(module_dict or {}) 1324 | 1325 | s = SnekCoverage(scope=scope, modules=modules, call_stack=call_stack) 1326 | s.eval(expr) 1327 | test_names = [n for n in s.scope if n.startswith("test_") and callable(s.scope[n])] 1328 | for name in test_names: 1329 | s.scope[name]() 1330 | return sorted(s.seen_nodes.items()) 1331 | 1332 | 1333 | def ascii_format_coverage(coverage, source): 1334 | pct = sum(v > 0 for k, v in coverage) / len(coverage) 1335 | # total = sum(v for k, v in coverage) 1336 | out = "" 1337 | for (r, c, name), v in coverage: 1338 | if v: 1339 | continue 1340 | out += f"Missing {name} on line: {r} col: {c}\n" 1341 | out += (source.splitlines()[r - 1]) + "\n" 1342 | out += (c * "-") + "^\n" 1343 | out += f"{ int(pct * 100) }% coverage\n" 1344 | return out 1345 | -------------------------------------------------------------------------------- /test_snek.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | 4 | import sneklang 5 | 6 | sneklang.WHITLIST_FUNCTIONS += ["test_snek.*"] 7 | 8 | from sneklang import * 9 | 10 | 11 | def snek_is_still_python(code, snek_scope=None): 12 | """ 13 | Here we run the code in python exec, then in snek_eval, then compare the `scope['result']` 14 | """ 15 | print("===code") 16 | print(code.strip()) 17 | print("===code") 18 | py_scope = {} 19 | snek_scope = snek_scope or {} 20 | exec(code, py_scope) 21 | 22 | print("python result", py_scope.get("result")) 23 | print("snek_eval output", snek_eval(code, scope=snek_scope)) 24 | print("snek result", snek_scope.get("result")) 25 | assert ( 26 | py_scope["result"] == snek_scope["result"] 27 | ), f'{code}\n{py_scope["result"]} != {snek_scope["result"]}' 28 | 29 | 30 | def test_snek_comprehension_python(): 31 | CASES = [ 32 | """ 33 | non_flat= [ [1,2,3], [4,5,6], [7,8] ] 34 | result = [y for x in non_flat for y in x] 35 | """, 36 | """ 37 | a = 'foo' 38 | l = [a for a in [1,2,3]] 39 | result = a, l 40 | """, 41 | """ 42 | a = {'b':['c', 'd', 'e']} 43 | a['b'][1] = 'D' 44 | result = a 45 | """, 46 | """ 47 | a = [1,2,3,4,5,6] 48 | a[2:5] = ['a', 'b'] 49 | result = a 50 | """, 51 | """ 52 | a = [1,2,3,4,5,6,7,8] 53 | a[::2] = [0,0,0,0] 54 | result = a 55 | """, 56 | """ 57 | a = [1,2,3,4,5,6,7,8] 58 | del a[3:6:-1] 59 | result = a 60 | """, 61 | """ 62 | a = [1] 63 | a = [a, a, a, a] 64 | result = a[2]""", 65 | """ 66 | a = [(1,1), (2,2), (3,3)] 67 | result = [(x,y) for x,y in a] 68 | """, 69 | """ 70 | a = [1,2,3,4,5,6,7,8,9,10] 71 | result = [x for x in a if x % 2] 72 | """, 73 | """ 74 | d = {**{"a": 1}, **{"b":2}, "z": 3} 75 | result = list(d.items()) 76 | result.sort() 77 | """, 78 | """ 79 | a = {"a":1} 80 | result = {None: 1, **a, **{"a":2, **{"b":2}}, **{"b":3}} 81 | """, 82 | ] 83 | for code in CASES: 84 | snek_is_still_python(code) 85 | 86 | 87 | def test_snek_assertions(): 88 | code = """ 89 | assert True, "no" 90 | result = 1""" 91 | snek_is_still_python(code) 92 | 93 | code = """ 94 | result = 1 95 | try: 96 | assert False, "no" 97 | except: 98 | pass 99 | result = 2""" 100 | snek_is_still_python(code) 101 | 102 | code = """ 103 | result = 1 104 | try: 105 | assert False 106 | except: 107 | pass 108 | result = 2""" 109 | snek_is_still_python(code) 110 | 111 | 112 | def test_assignment(): 113 | code = """ 114 | a = b,c = 1,2 115 | result = a,b,c """ 116 | snek_is_still_python(code) 117 | 118 | 119 | def test_starred(): 120 | code = """ 121 | a, *b, c,(*d,e) = (1,2,3,4,5,6,7,8,(9,10,)) 122 | result = a, b, c, d, e""" 123 | snek_is_still_python(code) 124 | 125 | 126 | def test_augassign(): 127 | for operator in ["+=", "-=", "/=", "//=", "%=", "!=", "*=", "^="]: 128 | code = f""" 129 | result = 110 130 | result {operator} 21 131 | """ 132 | snek_is_still_python(code) 133 | 134 | 135 | def test_snek_delete(): 136 | code = """ 137 | a = 1 138 | b = [a, a] 139 | del a 140 | result = [b, b]""" 141 | snek_is_still_python(code) 142 | 143 | 144 | def test_snek_kwargs(): 145 | code = """ 146 | def foo(a,b,c,d,e,f,g): 147 | return (a,b,c,d,e,f,g) 148 | 149 | result = foo(1, *[2,3], *[4], e=5, **{'f':6}, **{'g':7} ) 150 | """ 151 | snek_is_still_python(code) 152 | 153 | 154 | def test_args(): 155 | code = """ 156 | def standard_arg(arg): 157 | return arg 158 | result = standard_arg("a") + standard_arg(arg="b")""" 159 | snek_is_still_python(code) 160 | 161 | code = """ 162 | def standard_arg(arg=1): 163 | return arg 164 | result = standard_arg("a") + standard_arg(arg="b")""" 165 | snek_is_still_python(code) 166 | 167 | 168 | @pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8 or higher") 169 | def test_pos_args(): 170 | code = """ 171 | def pos_only_arg(arg, /): 172 | return arg 173 | 174 | result = pos_only_arg("a")""" 175 | snek_is_still_python(code) 176 | 177 | code = """ 178 | def pos_only_arg(arg=1, /): 179 | return arg 180 | 181 | result = pos_only_arg("a")""" 182 | snek_is_still_python(code) 183 | 184 | 185 | def test_kwd_only_arg(): 186 | code = """ 187 | def kwd_only_arg(*, arg): 188 | return arg 189 | 190 | result = kwd_only_arg(arg=42)""" 191 | snek_is_still_python(code) 192 | 193 | code = """ 194 | def kwd_only_arg(*, arg="foo"): 195 | return arg 196 | 197 | result = kwd_only_arg(arg=42)""" 198 | snek_is_still_python(code) 199 | 200 | 201 | @pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8 or higher") 202 | def test_func_combined(): 203 | code = """ 204 | def combined_example(pos_only, /, standard, *, kwd_only, **kwgs): 205 | return (pos_only, standard, kwd_only, kwgs) 206 | result = combined_example(1, 2, kwd_only=3, foo=[1,2,3])""" 207 | snek_is_still_python(code) 208 | 209 | 210 | def test_snek_lambda(): 211 | code = """ 212 | foo = lambda a,b,c,d,e,f,g: (a,b,c,d,e,f,g) 213 | result = foo(1, *[2,3], *[4], e=5, **{'f':6}, **{'g':7} ) 214 | """ 215 | snek_is_still_python(code) 216 | 217 | 218 | def test_fstring(): 219 | code = """ 220 | result = f'{"😇"!a:^40}' 221 | """ 222 | snek_is_still_python(code) 223 | 224 | 225 | def test_snek_is_python_closure(): 226 | code = """ 227 | a = 0 228 | def nest(b): 229 | a = 4 230 | def _inner(c): 231 | a = 5 232 | def __inner(d): 233 | def ___inner(a): 234 | # shadow a 235 | return a,b,c,d 236 | return ___inner 237 | return __inner 238 | return _inner 239 | result = nest(1)(2)(3)(4) 240 | result 241 | """ 242 | snek_is_still_python(code) 243 | 244 | 245 | def test_snek_is_python_ifs(): 246 | snek_is_still_python( 247 | """ 248 | s = 'the answer is ' 249 | def foo(i): 250 | if i < 0: 251 | res = s + 'too low' 252 | elif i > 0: 253 | res = s + 'too high' 254 | else: 255 | res = s + 'just right' 256 | return res 257 | 258 | result = [foo(i) for i in [-1,0,1]] 259 | """ 260 | ) 261 | 262 | 263 | def test_snek_is_python_while(): 264 | snek_is_still_python( 265 | """ 266 | result = [] 267 | n = 10 268 | while n: 269 | n = n -1 270 | result = result + [n] 271 | """ 272 | ) 273 | snek_is_still_python( 274 | """ 275 | result = [] 276 | n = 0 277 | while n: 278 | n = n -1 279 | result = result + [n] 280 | else: 281 | result = 'nonono' 282 | """ 283 | ) 284 | snek_is_still_python( 285 | """ 286 | result = [] 287 | n = 0 288 | while n < 10: 289 | n = n + 1 290 | result = result + [n] 291 | if n > 5: 292 | break 293 | """ 294 | ) 295 | 296 | snek_is_still_python( 297 | """ 298 | result = [] 299 | n = 0 300 | while n < 10: 301 | n = n + 1 302 | result = result + [n] 303 | if n == 5: 304 | continue 305 | """ 306 | ) 307 | 308 | 309 | def test_snek_is_python_for(): 310 | snek_is_still_python( 311 | """ 312 | def evens(s): 313 | out = '' 314 | try: 315 | for i, c in s: 316 | if i % 2 == 0: 317 | out = out + c 318 | else: 319 | out = -1 320 | except: 321 | return False 322 | return out 323 | 324 | result = [evens(s) for s in 325 | [[(0, 'a'), 326 | (1, 'b'), 327 | (2, 'c'), 328 | (3, 'd'), 329 | (4, 'e'), 330 | (5, 'f'), 331 | (6, 'g'), 332 | (7, 'h')],[], 'asdf'] ] 333 | """ 334 | ) 335 | snek_is_still_python( 336 | """ 337 | result = [] 338 | for n in [1,2,3,4,5,6,7,8,10]: 339 | result = result + [n] 340 | if n > 5: 341 | break 342 | """ 343 | ) 344 | 345 | snek_is_still_python( 346 | """ 347 | result = [] 348 | for n in [1,2,3,4,5,6,7,8,10]: 349 | result = result + [n] 350 | if n == 5: 351 | continue 352 | """ 353 | ) 354 | 355 | 356 | def test_decorators(): 357 | snek_is_still_python( 358 | """ 359 | result = [] 360 | a = 12 361 | def my_decorator(log): 362 | a = 13 363 | result.append(f"d1 {log} {a}") 364 | def _wrapper(func): 365 | a = 14 366 | result.append(f"_wrapper {log} {a}") 367 | def _inner(*args, **kwargs): 368 | a = 15 369 | result.append(f"inner {log} {a}") 370 | func(*args, **kwargs) 371 | 372 | a = 16 373 | return _inner 374 | a = 17 375 | return _wrapper 376 | a = 18 377 | 378 | @my_decorator(1) 379 | @my_decorator(2) 380 | def say(word=None): 381 | result.append(word or "hi world!") 382 | say() 383 | say('ola mundo') 384 | """ 385 | ) 386 | 387 | 388 | def test_snek_is_python_try(): 389 | snek_is_still_python( 390 | """ 391 | def foo(a): 392 | res = 'try|' 393 | try: 394 | try: 395 | res = res + str(1/a) + "|" 396 | except ArithmeticError as e: 397 | res= res + 'ArithmeticError|' 398 | except Exception as e2: 399 | return 'oops|' 400 | else: 401 | res = res + 'else|' 402 | finally: 403 | res = res + 'fin|' 404 | return a, res 405 | result = [foo(i) for i in [-1,0,1, 'a']] 406 | """, 407 | snek_scope={"Exception": Exception, "ArithmeticError": ArithmeticError}, 408 | ) 409 | 410 | 411 | EXCEPTION_CASES = [ 412 | ( 413 | "*a, *b = c", 414 | {}, 415 | 'SnekRuntimeError("SyntaxError(', 416 | ), 417 | ( 418 | "*a, b, c = [1]", 419 | {}, 420 | "SnekRuntimeError(\"ValueError('not enough values to unpack (expected at least 2, got 1)')\")", 421 | ), 422 | ("nope", {}, "SnekRuntimeError('NameError(\"\\'nope\\' is not defined\")')"), 423 | ( 424 | "a=1; a.b", 425 | {}, 426 | "SnekRuntimeError('AttributeError(\"\\'int\\' object has no attribute \\'b\\'\")')", 427 | ), 428 | ("1/0", {}, "SnekRuntimeError(\"ZeroDivisionError('division by zero')\")"), 429 | ( 430 | "len(str(10000 ** 10001))", 431 | {}, 432 | "SnekRuntimeError('MemoryError(\"Sorry! I don\\'t want to evaluate 10000 ** 10001\")')", 433 | ), 434 | ( 435 | "'aaaa' * 200000", 436 | {}, 437 | "SnekRuntimeError(\"MemoryError('Sorry, I will not evalute something that long.')\")", 438 | ), 439 | ( 440 | "200000 * 'aaaa'", 441 | {}, 442 | "SnekRuntimeError(\"MemoryError('Sorry, I will not evalute something that long.')\")", 443 | ), 444 | ( 445 | "(10000 * 'world!') + (10000 * 'world!')", 446 | {}, 447 | "SnekRuntimeError(\"MemoryError('Sorry, adding those two together would make something too long.')\")", 448 | ), 449 | ( 450 | "4 @ 3", 451 | {}, 452 | "SnekRuntimeError(\"NotImplementedError('Sorry, MatMult is not available in this evaluator')\")", 453 | ), 454 | ( 455 | "", 456 | {"open": open}, 457 | # dfferences in how python version report the error 458 | "DangerousValue", 459 | ), 460 | ( 461 | "a.clear()", 462 | {"a": []}, 463 | # dfferences in how python version report the error 464 | "NotImplementedError", 465 | ), 466 | ( 467 | "a @= 3", 468 | {}, 469 | "SnekRuntimeError(\"NotImplementedError('Sorry, MatMult is not available in this evaluator')\")", 470 | ), 471 | ("int.mro()", {}, 'SnekRuntimeError("DangerousValue'), 472 | (repr("a" * 100001), {}, 'SnekRuntimeError("MemoryError'), 473 | ( 474 | repr({i: 1 for i in range(1000001)}), 475 | {}, 476 | "SnekRuntimeError(\"MemoryError('Dict in statement is too long!')\")", 477 | ), 478 | ( 479 | repr(tuple(range(1000001))), 480 | {}, 481 | "SnekRuntimeError(\"MemoryError('Tuple in statement is too long!')\")", 482 | ), 483 | ( 484 | repr(set(range(1000001))), 485 | {}, 486 | "SnekRuntimeError(\"MemoryError('Set in statement is too long!')\")", 487 | ), 488 | ("b'" + ("a" * 100001) + "'", {}, 'SnekRuntimeError("MemoryError('), 489 | (("1" + "0" * sneklang.MAX_STRING_LENGTH), {}, 'SnekRuntimeError("MemoryError('), 490 | ( 491 | repr(list("a" * 100001)), 492 | {}, 493 | "SnekRuntimeError(\"MemoryError('List in statement is too long!')\")", 494 | ), 495 | ("1()", {}, "SnekRuntimeError(\"TypeError('Sorry, int type is not callable')\")"), 496 | ( 497 | "forbidden_func()[0]()", 498 | {"forbidden_func": lambda: [type]}, 499 | "SnekRuntimeError(\"DangerousValue('This function is forbidden: builtins.type')\")", 500 | ), 501 | ( 502 | "a()([])", 503 | {"a": lambda: sorted}, 504 | "SnekRuntimeError(\"NotImplementedError('This function is not allowed: builtins.sorted')\")", 505 | ), 506 | ("a[1]", {"a": []}, "SnekRuntimeError(\"IndexError('list index out of range')\")"), 507 | ( 508 | "a.__length__", 509 | {"a": []}, 510 | "SnekRuntimeError(\"NotImplementedError('Sorry, access to this attribute is not available. (__length__)')\")", 511 | ), 512 | ( 513 | "'say{}'.format('hi') ", 514 | {}, 515 | "SnekRuntimeError(\"DangerousValue('Sorry, this method is not available. (str.format)')\")", 516 | ), 517 | ( 518 | "class A: 1", 519 | {}, 520 | "SnekRuntimeError(\"NotImplementedError('Sorry, ClassDef is not available in this evaluator')\")", 521 | ), 522 | ( 523 | "a.b", 524 | {"a": object()}, 525 | "SnekRuntimeError('AttributeError(\"\\'object\\' object has no attribute \\'b\\'\")')", 526 | ), 527 | ( 528 | "'a' + 1", 529 | {}, 530 | "SnekRuntimeError('TypeError(\\'can only concatenate str (not \"int\") to str\\')')", 531 | ), 532 | ( 533 | "import non_existant", 534 | {}, 535 | "SnekRuntimeError(\"ModuleNotFoundError('non_existant')\")", 536 | ), 537 | ( 538 | "from nowhere import non_existant", 539 | {}, 540 | "SnekRuntimeError(\"ModuleNotFoundError('nowhere')\")", 541 | ), 542 | ('assert False, "no"', {}, "SnekRuntimeError(\"AssertionError('no')\")"), 543 | ( 544 | "del a,b,c", 545 | {}, 546 | "SnekRuntimeError(\"NotImplementedError('Sorry, cannot delete 3 targets.')\")", 547 | ), 548 | ( 549 | "del a.c", 550 | {}, 551 | "SnekRuntimeError(\"NotImplementedError('Sorry, cannot delete Attribute')\")", 552 | ), 553 | ( 554 | "[1,2,3][[]]", 555 | {}, 556 | "SnekRuntimeError(\"TypeError('list indices must be integers or slices, not list')\")", 557 | ), 558 | ( 559 | "1<<1", 560 | {}, 561 | "SnekRuntimeError(\"NotImplementedError('Sorry, LShift is not available in this evaluator')\")", 562 | ), 563 | ("assert False", {}, "SnekRuntimeError('AssertionError()')"), 564 | ("assert False, 'oh no'", {}, "SnekRuntimeError(\"AssertionError('oh no')\")"), 565 | ( 566 | "(a for a in a)", 567 | {}, 568 | "SnekRuntimeError(\"NotImplementedError('Sorry, GeneratorExp is not available in this evaluator')\")", 569 | ), 570 | ( 571 | "vars(object)", 572 | {"vars": vars}, 573 | "DangerousValue(\"This function 'vars' in scope is and is in DISALLOW_FUNCTIONS\")", 574 | ), 575 | ( 576 | "a, b = 1", 577 | {}, 578 | "SnekRuntimeError(\"TypeError('cannot unpack non-iterable int object')\")", 579 | ), 580 | ( 581 | "a, b = 1, 2, 3", 582 | {}, 583 | "SnekRuntimeError(\"ValueError('too many values to unpack (expected 2)')\")", 584 | ), 585 | ( 586 | "a, b, c = 1, 2", 587 | {}, 588 | "SnekRuntimeError(\"ValueError('not enough values to unpack (expected 3, got 2)')\")", 589 | ), 590 | ( 591 | "f'{1:<1000}'", 592 | {}, 593 | "SnekRuntimeError(\"MemoryError('Sorry, this format width is too long.')\")", 594 | ), 595 | ( 596 | "f'{1/3:.1000}'", 597 | {}, 598 | "SnekRuntimeError(\"MemoryError('Sorry, this format precision is too long.')\")", 599 | ), 600 | ( 601 | "f'{1:a}'", 602 | {}, 603 | "SnekRuntimeError('ValueError(\"Unknown format code \\'a\\' for object of type \\'int\\'\")')", 604 | ), 605 | ] 606 | 607 | 608 | @pytest.mark.filterwarnings("ignore::SyntaxWarning") 609 | def test_exceptions(): 610 | for i, (code, scope, ex_repr) in enumerate(EXCEPTION_CASES): 611 | try: 612 | out = snek_eval(code, scope=scope) 613 | except Exception as e: 614 | exc = e 615 | assert ex_repr in repr( 616 | exc 617 | ), f"{repr(repr(exc))}\nFailed {code} \nin CASE {i}" 618 | continue 619 | pytest.fail("{}\nneeded to raise: {}\nreturned: {}".format(code, ex_repr, out)) 620 | 621 | 622 | @pytest.mark.skipif(sys.version_info >= (3, 8), reason="Old way of checking functions") 623 | def test_old_dangerous_values(): 624 | with pytest.raises(sneklang.DangerousValue) as excinfo: 625 | snek_eval("a", scope={"a": {}.keys}) 626 | assert ( 627 | repr(excinfo.value) 628 | == "DangerousValue(\"This function 'a' in scope might be a bad idea.\")" 629 | ) 630 | 631 | 632 | def test_smoketests(): 633 | 634 | CASES = [ 635 | ("1 + 1", [2]), 636 | ("1 and []", [[]]), 637 | ("None or []", [[]]), 638 | ("3 ** 3", [27]), 639 | ("len(str(1000 ** 1000))", [3001]), 640 | ("True != False", [True]), 641 | ("None is None", [True]), 642 | ("True is not None", [True]), 643 | ("'a' in 'abc'", [True]), 644 | ("'d' not in 'abc'", [True]), 645 | ("- 1 * 2", [-2]), 646 | ("True or False", [True]), 647 | ("1 > 2 > 3", [False]), 648 | ("'abcd'[1]", ["b"]), 649 | ("'abcd'[1:3]", ["bc"]), 650 | ("'abcd'[:3]", ["abc"]), 651 | ("'abcd'[2:]", ["cd"]), 652 | ("'abcdefgh'[::3]", ["adg"]), 653 | ("('abc' 'xyz')", ["abcxyz"]), 654 | ("(b'abc' b'xyz')", [b"abcxyz"]), 655 | ("f'{1 + 2}'", ["3"]), 656 | ("{'a': 1}['a']", [1]), 657 | (repr([1] * 100), [[1] * 100]), 658 | (repr(set([1, 2, 3, 3, 3])), [set([1, 2, 3])]), 659 | ("[a + 1 for a in [1,2,3]]", [[2, 3, 4]]), 660 | ("[a + 1 for a in [1,2,3]]", [[2, 3, 4]]), 661 | ("{'a': 1}.get('a')", [1]), 662 | ("{'a': 1}.items()", [{"a": 1}.items()]), 663 | ("{'a': 1}.keys()", [{"a": 1}.keys()]), 664 | ("list({'a': 1}.values())", [[1]]), 665 | ("[a for a in [1,2,3,4,5,6,7,8,9,10] if a % 2 if a % 5]", [[1, 3, 7, 9]]), 666 | ] 667 | for code, out in CASES: 668 | assert snek_eval(code) == out, f"{code} should equal {out}" 669 | # Verify evaluates same as python 670 | assert [eval(code)] == out, code 671 | 672 | 673 | def test_call_stack(): 674 | scope = {} 675 | snek_eval("def foo(x): return x, x > 0 and foo(x-1) or 0", scope=scope) 676 | scope["foo"](3) 677 | with pytest.raises(SnekRuntimeError) as excinfo: 678 | scope["foo"](50) 679 | assert ( 680 | repr(excinfo.value) 681 | == "SnekRuntimeError(\"RecursionError('Sorry, stack is to large')\")" 682 | ) 683 | # test fake call stack 684 | snek_eval( 685 | "def foo(x): return foo(x - 1) if x > 0 else 0", 686 | scope=scope, 687 | call_stack=30 * [1], 688 | ) 689 | with pytest.raises(SnekRuntimeError) as excinfo: 690 | scope["foo"](3) 691 | assert ( 692 | repr(excinfo.value) 693 | == "SnekRuntimeError(\"RecursionError('Sorry, stack is to large')\")" 694 | ) 695 | 696 | 697 | def test_settings(): 698 | code = """ 699 | i=0 700 | a=[] 701 | while i < 10: 702 | a=[a, a] 703 | i+=1 704 | """ 705 | orig = sneklang.MAX_NODE_CALLS 706 | with pytest.raises(SnekRuntimeError) as excinfo: 707 | scope = {} 708 | sneklang.MAX_NODE_CALLS = 20 709 | snek_eval(code, scope=scope) 710 | assert ( 711 | repr(excinfo.value) 712 | == "SnekRuntimeError(\"TimeoutError('This program has too many evaluations')\")" 713 | ) 714 | sneklang.MAX_NODE_CALLS = orig 715 | 716 | orig = sneklang.MAX_SCOPE_SIZE 717 | with pytest.raises(SnekRuntimeError) as excinfo: 718 | scope = {} 719 | sneklang.MAX_SCOPE_SIZE = 500 720 | snek_eval(code, scope=scope) 721 | assert ( 722 | repr(excinfo.value) 723 | == "SnekRuntimeError(\"MemoryError('Scope has used too much memory')\")" 724 | ) 725 | sneklang.MAX_SCOPE_SIZE = orig 726 | 727 | 728 | def test_importing(): 729 | assert ( 730 | snek_eval("import a as c; c", module_dict={"a": {"b": "123"}})[ 731 | 1 732 | ].__class__.__name__ 733 | == "module" 734 | ) 735 | assert snek_eval("from a import b as c; c", module_dict={"a": {"b": "123"}}) == [ 736 | None, 737 | "123", 738 | ] 739 | with pytest.raises(SnekRuntimeError) as excinfo: 740 | snek_eval("from a import d", module_dict={"a": {"b": "123"}}) 741 | assert repr(excinfo.value) == "SnekRuntimeError(\"ImportError('d')\")" 742 | 743 | 744 | def test_dissallowed_functions(): 745 | 746 | snek_eval("", scope={"thing": {}}) 747 | with pytest.raises(DangerousValue): 748 | snek_eval("", scope={"open": open}) 749 | 750 | 751 | def test_undefined(): 752 | with pytest.raises(SnekRuntimeError, match="NameError"): 753 | snek_eval("a += 3") 754 | 755 | 756 | def test_undefined_local(): 757 | with pytest.raises(SnekRuntimeError, match="UnboundLocalError"): 758 | snek_eval( 759 | """ 760 | a =1 761 | def foo(): 762 | a += 3 763 | return a 764 | foo() 765 | """ 766 | ) 767 | 768 | 769 | def test_return_nothing(): 770 | assert ( 771 | snek_eval( 772 | """ 773 | def foo(): 774 | return 775 | foo()""" 776 | ) 777 | == [None, None] 778 | ) 779 | 780 | 781 | def test_exception_variable_assignment(): 782 | snek_is_still_python( 783 | """ 784 | e = 1 785 | try: 786 | try: 1/0 787 | except ZeroDivisionError as e: pass 788 | e 789 | except NameError as e2: 790 | result = True 791 | """ 792 | ) 793 | 794 | 795 | def test_return_in_exception(): 796 | assert ( 797 | snek_eval( 798 | """ 799 | def foo(): 800 | try: 801 | return 1 802 | except Exception as e: 803 | return e 804 | foo()""" 805 | ) 806 | == [None, 1] 807 | ) 808 | 809 | 810 | def test_eval_keyword(): 811 | assert ( 812 | snek_eval( 813 | """ 814 | def foo(a,b): 815 | return a,b 816 | foo(1,b=2)""" 817 | ) 818 | == [None, (1, 2)] 819 | ) 820 | 821 | 822 | def test_eval_functiondef_does_nothing(): 823 | assert ( 824 | snek_eval( 825 | """ 826 | def foo(a,b): 827 | pass 828 | foo(1,b=2)""" 829 | ) 830 | == [None, None] 831 | ) 832 | 833 | 834 | def test_eval_joinedstr(): 835 | with pytest.raises(SnekRuntimeError): 836 | sneklang.MAX_SCOPE_SIZE = 10000000 837 | snek_eval( 838 | """ 839 | a='a' * 50000 840 | f"{a} {a}" 841 | """ 842 | ) 843 | sneklang.MAX_SCOPE_SIZE = 100000 844 | 845 | assert ( 846 | snek_eval( 847 | """ 848 | width = 10 849 | precision = 4 850 | value = 12.345 851 | f"result: {value:{width}.{precision}}" # nested fields 852 | """ 853 | )[-1] 854 | == "result: 12.35" 855 | ) 856 | 857 | 858 | def test_coverage(): 859 | src = """ 860 | def bar(): 861 | return untested_variable 862 | 863 | def foo(a): 864 | b = 1 865 | return (b if a else 2) 866 | 867 | def test_foo(): 868 | [foo(i) for i in [False, [], 0]] 869 | 870 | """ 871 | coverage = snek_test_coverage(src) 872 | 873 | assert ( 874 | ascii_format_coverage(coverage, src) 875 | == """Missing Return on line: 3 col: 4 876 | return untested_variable 877 | ----^ 878 | Missing Name on line: 3 col: 11 879 | return untested_variable 880 | -----------^ 881 | Missing Name on line: 7 col: 12 882 | return (b if a else 2) 883 | ------------^ 884 | 87% coverage 885 | """ 886 | ) 887 | --------------------------------------------------------------------------------