Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix NamedExpr scope #120

Merged
merged 5 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)