├── .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 |
--------------------------------------------------------------------------------