Skip to content

Commit

Permalink
Merge pull request #120 from dflook/fix-namedexpr-scope
Browse files Browse the repository at this point in the history
Fix NamedExpr scope
  • Loading branch information
dflook authored Nov 12, 2024
2 parents 2714b91 + 99616a9 commit bfe655b
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 3 deletions.
1 change: 0 additions & 1 deletion .github/workflows/xtest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ jobs:
with:
image: danielflook/python-minifier-build:${{ matrix.python }}-2024-09-15
run: |
exit 0
if [[ "${{ matrix.python }}" == "python3.4" ]]; then
(cd /usr/lib64/python3.4/test && python3.4 make_ssl_certs.py)
Expand Down
24 changes: 23 additions & 1 deletion src/python_minifier/rename/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,27 @@ def add_parent_to_comprehension(node, namespace):

add_parent(generator.target, namespace=node)
add_parent(generator.iter, namespace=iter_namespace)
iter_namespace = node

for if_ in generator.ifs:
add_parent(if_, namespace=node)

iter_namespace = node

def namedexpr_namespace(node):
"""
Get the namespace for a NamedExpr target
"""

if not isinstance(node, (ast.ListComp, ast.DictComp, ast.SetComp, ast.GeneratorExp)):
return node

return namedexpr_namespace(node.namespace)

def add_parent_to_namedexpr(node):
assert isinstance(node, ast.NamedExpr)

add_parent(node.target, namespace=namedexpr_namespace(node.namespace))
add_parent(node.value, namespace=node.namespace)

def add_parent(node, namespace=None):
"""
Expand Down Expand Up @@ -161,6 +178,11 @@ def add_parent(node, namespace=None):
elif isinstance(node.ctx, ast.Store) and isinstance(get_parent(node), ast.AugAssign):
namespace.nonlocal_names.add(node.id)

if isinstance(node, ast.NamedExpr):
# NamedExpr is 'special'
add_parent_to_namedexpr(node)
return

for child in ast.iter_child_nodes(node):
add_parent(child, namespace=namespace)

Expand Down
2 changes: 1 addition & 1 deletion test/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def namespace_name(node):
for name in sorted(namespace.nonlocal_names):
s += indent + ' - nonlocal ' + name + '\n'

for binding in sorted(namespace.bindings, key=lambda b: b.name):
for binding in sorted(namespace.bindings, key=lambda b: b.name or str(b.value)):
s += indent + ' - ' + repr(binding) + '\n'

for child in iter_child_namespaces(namespace):
Expand Down
259 changes: 259 additions & 0 deletions test/test_bind_names_namedexpr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import sys

import pytest

from helpers import assert_namespace_tree

def test_namedexpr_in_module():
if sys.version_info < (3, 8):
pytest.skip('Test is for >= python3.8 only')

source = '''
(a := 1)
'''

expected_namespaces = '''
+ Module
- NameBinding(name='a', allow_rename=True) <references=1>
'''

assert_namespace_tree(source, expected_namespaces)

def test_namedexpr_in_function():
if sys.version_info < (3, 8):
pytest.skip('Test is for >= python3.8 only')

source = '''
def test():
(a := 1)
lambda x: (x := 1)
'''

expected_namespaces = '''
+ Module
- NameBinding(name='test', allow_rename=True) <references=1>
+ Function test
- NameBinding(name='a', allow_rename=True) <references=1>
+ Lambda
- NameBinding(name='x', allow_rename=False) <references=2>
'''

assert_namespace_tree(source, expected_namespaces)

def test_namedexpr_in_listcomp_if_nonlocal():
if sys.version_info < (3, 8):
pytest.skip('Test is for >= python3.8 only')

source = '''
def f(arg, /):
nonlocal x
print([x for y in range(10) if (x := y // 2) & 1])
print(arg, arg)
'''

expected_namespaces = '''
+ Module
- NameBinding(name='f', allow_rename=True) <references=1>
- BuiltinBinding(name='print', allow_rename=True) <references=2>
- BuiltinBinding(name='range', allow_rename=True) <references=1>
- NameBinding(name='x', allow_rename=False) <references=3>
+ Function f
- nonlocal x
- NameBinding(name='arg', allow_rename=True) <references=3>
+ ListComp
- NameBinding(name='y', allow_rename=True) <references=2>
'''

assert_namespace_tree(source, expected_namespaces)


def test_namedexpr_in_listcomp_if_global():
if sys.version_info < (3, 8):
pytest.skip('Test is for >= python3.8 only')

source = '''
def f2():
def f(arg, /):
global x
print([x for y in range(10) if (x := y // 2) & 1])
print(arg, arg)
'''

expected_namespaces = '''
+ Module
- NameBinding(name='f2', allow_rename=True) <references=1>
- BuiltinBinding(name='print', allow_rename=True) <references=2>
- BuiltinBinding(name='range', allow_rename=True) <references=1>
- NameBinding(name='x', allow_rename=True) <references=3>
+ Function f2
- NameBinding(name='f', allow_rename=True) <references=1>
+ Function f
- global x
- NameBinding(name='arg', allow_rename=True) <references=3>
+ ListComp
- NameBinding(name='y', allow_rename=True) <references=2>
'''

assert_namespace_tree(source, expected_namespaces)


def test_namedexpr_in_listcomp_if():
if sys.version_info < (3, 8):
pytest.skip('Test is for >= python3.8 only')

source = '''
def f(arg, /):
print([x for y in range(10) if (x := y // 2) & 1])
print(arg, arg)
'''

expected_namespaces = '''
+ Module
- NameBinding(name='f', allow_rename=True) <references=1>
- BuiltinBinding(name='print', allow_rename=True) <references=2>
- BuiltinBinding(name='range', allow_rename=True) <references=1>
+ Function f
- NameBinding(name='arg', allow_rename=True) <references=3>
- NameBinding(name='x', allow_rename=True) <references=2>
+ ListComp
- NameBinding(name='y', allow_rename=True) <references=2>
'''

assert_namespace_tree(source, expected_namespaces)


def test_namedexpr_in_listcomp_body():
if sys.version_info < (3, 8):
pytest.skip('Test is for >= python3.8 only')

source = '''
def f(arg, /):
print([(x := y // 2) for _ in range(x)])
print(arg, arg)
'''

expected_namespaces = '''
+ Module
- NameBinding(name='f', allow_rename=True) <references=1>
- BuiltinBinding(name='print', allow_rename=True) <references=2>
- BuiltinBinding(name='range', allow_rename=True) <references=1>
- NameBinding(name='y', allow_rename=False) <references=1>
+ Function f
- NameBinding(name='arg', allow_rename=True) <references=3>
- NameBinding(name='x', allow_rename=True) <references=2>
+ ListComp
- NameBinding(name='_', allow_rename=True) <references=1>
'''

assert_namespace_tree(source, expected_namespaces)

def test_namedexpr_in_dictcomp_body():
if sys.version_info < (3, 8):
pytest.skip('Test is for >= python3.8 only')

source = '''
{i: (x := i // 2) for i in range(1)}
'''

expected_namespaces = '''
+ Module
- BuiltinBinding(name='range', allow_rename=True) <references=1>
- NameBinding(name='x', allow_rename=True) <references=1>
+ DictComp
- NameBinding(name='i', allow_rename=True) <references=3>
'''

assert_namespace_tree(source, expected_namespaces)


def test_namedexpr_in_dictcomp_if():
if sys.version_info < (3, 8):
pytest.skip('Test is for >= python3.8 only')

source = '''
{x: y for y in range(1) if (x := y // 2)}
'''

expected_namespaces = '''
+ Module
- BuiltinBinding(name='range', allow_rename=True) <references=1>
- NameBinding(name='x', allow_rename=True) <references=2>
+ DictComp
- NameBinding(name='y', allow_rename=True) <references=3>
'''

assert_namespace_tree(source, expected_namespaces)

def test_namedexpr_in_setcomp_body():
if sys.version_info < (3, 8):
pytest.skip('Test is for >= python3.8 only')

source = '''
{(x := y // 2) for y in range(1)}
'''

expected_namespaces = '''
+ Module
- BuiltinBinding(name='range', allow_rename=True) <references=1>
- NameBinding(name='x', allow_rename=True) <references=1>
+ SetComp
- NameBinding(name='y', allow_rename=True) <references=2>
'''

assert_namespace_tree(source, expected_namespaces)


def test_namedexpr_in_setcomp_if():
if sys.version_info < (3, 8):
pytest.skip('Test is for >= python3.8 only')

source = '''
{x for y in range(1) if (x := y // 2)}
'''

expected_namespaces = '''
+ Module
- BuiltinBinding(name='range', allow_rename=True) <references=1>
- NameBinding(name='x', allow_rename=True) <references=2>
+ SetComp
- NameBinding(name='y', allow_rename=True) <references=2>
'''

assert_namespace_tree(source, expected_namespaces)

def test_namedexpr_in_generatorexp_body():
if sys.version_info < (3, 8):
pytest.skip('Test is for >= python3.8 only')

source = '''
((x := y // 2) for y in range(1))
'''

expected_namespaces = '''
+ Module
- BuiltinBinding(name='range', allow_rename=True) <references=1>
- NameBinding(name='x', allow_rename=True) <references=1>
+ GeneratorExp
- NameBinding(name='y', allow_rename=True) <references=2>
'''

assert_namespace_tree(source, expected_namespaces)


def test_namedexpr_in_generatorexp_if():
if sys.version_info < (3, 8):
pytest.skip('Test is for >= python3.8 only')

source = '''
(x for y in range(1) if (x := y // 2))
'''

expected_namespaces = '''
+ Module
- BuiltinBinding(name='range', allow_rename=True) <references=1>
- NameBinding(name='x', allow_rename=True) <references=2>
+ GeneratorExp
- NameBinding(name='y', allow_rename=True) <references=2>
'''

assert_namespace_tree(source, expected_namespaces)

0 comments on commit bfe655b

Please sign in to comment.