Skip to content

Commit 722800e

Browse files
d-maurerdataflakeMichael Howitz
authored
Support assignment expressions (i.e. the ":=" operator) (#195)
* Support assignment expressions (i.e. the ":=" operator * make flake8 happy * add tests for `NamedExpr` support * make `isort` happy * check that "private" variables cannot be used as target * use version detection support from `_compat`; get rid of `util` * make `isort` happy * move `test_NamedExpr` out of `src/RestrictedPython/tests` to `tests` Co-authored-by: Jens Vagelpohl <[email protected]> Co-authored-by: Michael Howitz <[email protected]>
1 parent 9d0a217 commit 722800e

File tree

3 files changed

+77
-0
lines changed

3 files changed

+77
-0
lines changed

docs/CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Changes
44
5.1a0 (unreleased)
55
------------------
66

7+
- Add support for (Python 3.8+) assignment expressions (i.e. the ``:=`` operator)
8+
79
- Fix ``compile_restricted_function`` with SyntaxErrors that have no text
810
(`#181 <https://github.com/zopefoundation/RestrictedPython/issues/181>`_)
911

src/RestrictedPython/transformer.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,3 +1473,22 @@ def visit_AsyncFor(self, node):
14731473
def visit_AsyncWith(self, node):
14741474
"""Deny async functionality."""
14751475
self.not_allowed(node)
1476+
1477+
# Assignment expressions (walrus operator ``:=``)
1478+
# New in 3.8
1479+
def visit_NamedExpr(self, node):
1480+
"""Allow assignment expressions under some circumstances."""
1481+
# while the grammar requires ``node.target`` to be a ``Name``
1482+
# the abstract syntax is more permissive and allows an ``expr``.
1483+
# We support only a ``Name``.
1484+
# This is safe as the expression can only add/modify local
1485+
# variables. While this may hide global variables, an
1486+
# (implicitly performed) name check guarantees (as usual)
1487+
# that no essential global variable is hidden.
1488+
node = self.node_contents_visit(node) # this checks ``node.target``
1489+
target = node.target
1490+
if not isinstance(target, ast.Name):
1491+
self.error(
1492+
node,
1493+
"Assignment expressions are only allowed for simple targets")
1494+
return node

tests/test_NamedExpr.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Assignment expression (``NamedExpr``) tests."""
2+
3+
4+
from ast import NodeTransformer
5+
from ast import parse
6+
from RestrictedPython import compile_restricted
7+
from RestrictedPython import safe_globals
8+
from RestrictedPython._compat import IS_PY38_OR_GREATER
9+
from unittest import skipUnless
10+
from unittest import TestCase
11+
12+
13+
@skipUnless(IS_PY38_OR_GREATER, "Feature available for Python 3.8+")
14+
class TestNamedExpr(TestCase):
15+
def test_works(self):
16+
code, gs = compile_str("if x:= x + 1: True\n")
17+
gs["x"] = 0
18+
exec(code, gs)
19+
self.assertEqual(gs["x"], 1)
20+
21+
def test_no_private_target(self):
22+
with self.assertRaises(SyntaxError):
23+
compile_str("if _x_:= 1: True\n")
24+
25+
def test_simple_only(self):
26+
# we test here that only a simple variable is allowed
27+
# as assignemt expression target
28+
# Currently (Python 3.8, 3.9), this is enforced by the
29+
# Python concrete syntax; therefore, some (``ast``) trickery is
30+
# necessary to produce a test for it.
31+
class TransformNamedExprTarget(NodeTransformer):
32+
def visit_NamedExpr(self, node):
33+
# this is brutal but sufficient for the test
34+
node.target = None
35+
return node
36+
37+
mod = parse("if x:= x + 1: True\n")
38+
mod = TransformNamedExprTarget().visit(mod)
39+
with self.assertRaisesRegex(
40+
SyntaxError,
41+
"Assignment expressions are only allowed for simple target"):
42+
code, gs = compile_str(mod)
43+
44+
45+
def compile_str(s, name="<unknown>"):
46+
"""code and globals for *s*.
47+
48+
*s* must be acceptable for ``compile_restricted`` (this is (especially) the
49+
case for an ``str`` or ``ast.Module``).
50+
51+
*name* is a ``str`` used in error messages.
52+
"""
53+
code = compile_restricted(s, name, 'exec')
54+
gs = safe_globals.copy()
55+
gs["__debug__"] = True # assert active
56+
return code, gs

0 commit comments

Comments
 (0)