From eee14de148cbaf4a408b4a4e250c84008df36523 Mon Sep 17 00:00:00 2001 From: Beomsoo Kim Date: Tue, 12 Nov 2024 21:19:08 +0900 Subject: [PATCH 1/3] Add test_nonlocal.py that mirrors test_global.py --- Lib/test/test_nonlocal.py | 276 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 Lib/test/test_nonlocal.py diff --git a/Lib/test/test_nonlocal.py b/Lib/test/test_nonlocal.py new file mode 100644 index 00000000000000..523456846bf2dc --- /dev/null +++ b/Lib/test/test_nonlocal.py @@ -0,0 +1,276 @@ +"""This module includes tests for syntax errors that occur when a name +declared as `nonlocal` is used in ways that violate the language +specification, such as after assignment, usage, or annotation. The tests +verify that syntax errors are correctly raised for improper `nonlocal` +statements following variable use or assignment within functions. +Additionally, it tests various name-binding scenarios for nonlocal +variables to ensure correct behavior. + +See `test_scope.py` for additional related behavioral tests covering +variable scoping and usage in different contexts. +""" + +import contextlib +from test.support import check_syntax_error +from test.support.warnings_helper import check_warnings +from types import SimpleNamespace +import unittest +import warnings + + +class NonlocalTests(unittest.TestCase): + + def setUp(self): + self.enterContext(check_warnings()) + warnings.filterwarnings("error", module="") + + ###################################################### + ### Syntax error cases as covered in Python/symtable.c + ###################################################### + + def test_name_param(self): + prog_text = """\ +def fn(name_param): + nonlocal name_param +""" + check_syntax_error(self, prog_text, lineno=2, offset=5) + + def test_name_after_assign(self): + prog_text = """\ +def fn(): + name_assign = 1 + nonlocal name_assign +""" + check_syntax_error(self, prog_text, lineno=3, offset=5) + + def test_name_after_use(self): + prog_text = """\ +def fn(): + print(name_use) + nonlocal name_use +""" + check_syntax_error(self, prog_text, lineno=3, offset=5) + + def test_name_annot(self): + prog_text = """\ +def fn(): + name_annot: int + nonlocal name_annot +""" + check_syntax_error(self, prog_text, lineno=3, offset=5) + + ############################################################### + ### Tests for nonlocal variables across all name binding cases, + ### as described in executionmodel.rst + ############################################################### + + def test_assignment_statement(self): + name_assignment_statement = None + value = object() + + def inner(): + nonlocal name_assignment_statement + name_assignment_statement = value + + inner() + self.assertIs(name_assignment_statement, value) + + def test_unpacking_assignment(self): + name_unpacking_assignment = None + value = object() + + def inner(): + nonlocal name_unpacking_assignment + _, name_unpacking_assignment = [None, value] + + inner() + self.assertIs(name_unpacking_assignment, value) + + def test_assignment_expression(self): + name_assignment_expression = None + value = object() + + def inner(): + nonlocal name_assignment_expression + if name_assignment_expression := value: + pass + + inner() + self.assertIs(name_assignment_expression, value) + + def test_iteration_variable(self): + name_iteration_variable = None + value = object() + + def inner(): + nonlocal name_iteration_variable + for name_iteration_variable in [value]: + pass + + inner() + self.assertIs(name_iteration_variable, value) + + def test_func_def(self): + name_func_def = None + + def inner(): + nonlocal name_func_def + + def name_func_def(): + pass + + inner() + self.assertIs(name_func_def, name_func_def) + + def test_class_def(self): + name_class_def = None + + def inner(): + nonlocal name_class_def + + class name_class_def: + pass + + inner() + self.assertIs(name_class_def, name_class_def) + + def test_type_alias(self): + name_type_alias = None + + def inner(): + nonlocal name_type_alias + type name_type_alias = tuple[int, int] + + inner() + self.assertIs(name_type_alias, name_type_alias) + + def test_caught_exception(self): + name_caught_exc = None + + def inner(): + nonlocal self, inner, name_caught_exc + idx = inner.__code__.co_freevars.index("name_caught_exc") + try: + 1 / 0 + except ZeroDivisionError as name_caught_exc: + # `name_caught_exc` is cleared automatically after the except block + self.assertIs(inner.__closure__[idx].cell_contents, name_caught_exc) + + inner() + + def test_caught_exception_group(self): + name_caught_exc_group = None + + def inner(): + nonlocal self, inner, name_caught_exc_group + idx = inner.__code__.co_freevars.index("name_caught_exc_group") + + try: + try: + 1 / 0 + except ZeroDivisionError as exc: + raise ExceptionGroup("eg", [exc]) + except* ZeroDivisionError as name_caught_exc_group: + # `name_caught_exc_group` is cleared automatically after the except block + self.assertIs( + inner.__closure__[idx].cell_contents, name_caught_exc_group + ) + + inner() + + def test_enter_result(self): + name_enter_result = None + value = object() + + def inner(): + nonlocal name_enter_result + with contextlib.nullcontext(value) as name_enter_result: + pass + + inner() + self.assertIs(name_enter_result, value) + + def test_import_result(self): + name_import_result = None + value = contextlib + + def inner(): + nonlocal name_import_result + import contextlib as name_import_result + + inner() + self.assertIs(name_import_result, value) + + def test_match(self): + name_match = None + value = object() + + def inner(): + nonlocal name_match + match value: + case name_match: + pass + + inner() + self.assertIs(name_match, value) + + def test_match_as(self): + name_match_as = None + value = object() + + def inner(): + nonlocal name_match_as + match value: + case _ as name_match_as: + pass + + inner() + self.assertIs(name_match_as, value) + + def test_match_seq(self): + name_match_seq = None + value = object() + + def inner(): + nonlocal name_match_seq + match (None, value): + case (_, name_match_seq): + pass + + inner() + self.assertIs(name_match_seq, value) + + def test_match_map(self): + name_match_map = None + value = object() + + def inner(): + nonlocal name_match_map + match {"key": value}: + case {"key": name_match_map}: + pass + + inner() + self.assertIs(name_match_map, value) + + def test_match_attr(self): + name_match_attr = None + value = object() + + def inner(): + nonlocal name_match_attr + match SimpleNamespace(key=value): + case SimpleNamespace(key=name_match_attr): + pass + + inner() + self.assertIs(name_match_attr, value) + + +def setUpModule(): + unittest.enterModuleContext(warnings.catch_warnings()) + warnings.filterwarnings("error", module="") + + +if __name__ == "__main__": + unittest.main() From 9d328538d53d94b26fac20540131e97a0fdc055f Mon Sep 17 00:00:00 2001 From: Beomsoo Kim Date: Tue, 12 Nov 2024 21:34:22 +0900 Subject: [PATCH 2/3] Add a Documentation NEWS entry --- .../2024-11-12-21-31-39.gh-issue-58749.2fpEYM.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Documentation/2024-11-12-21-31-39.gh-issue-58749.2fpEYM.rst diff --git a/Misc/NEWS.d/next/Documentation/2024-11-12-21-31-39.gh-issue-58749.2fpEYM.rst b/Misc/NEWS.d/next/Documentation/2024-11-12-21-31-39.gh-issue-58749.2fpEYM.rst new file mode 100644 index 00000000000000..36af48dd762ed0 --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2024-11-12-21-31-39.gh-issue-58749.2fpEYM.rst @@ -0,0 +1,5 @@ +Corrected the language specification to match CPython's behavior: names in a +`global` statement are only restricted from being used or assigned prior to +their declaration in scope. Additional limitations previously suggested by +the spec do not apply in CPython. +Change by Beomsoo Kim From 404bb9ca90d3c872a81f5cadbafda3320a659fcb Mon Sep 17 00:00:00 2001 From: Beomsoo Kim Date: Tue, 12 Nov 2024 21:42:21 +0900 Subject: [PATCH 3/3] Add a Tests NEWS entry --- .../next/Tests/2024-11-12-21-41-57.gh-issue-58749.LuzORa.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Tests/2024-11-12-21-41-57.gh-issue-58749.LuzORa.rst diff --git a/Misc/NEWS.d/next/Tests/2024-11-12-21-41-57.gh-issue-58749.LuzORa.rst b/Misc/NEWS.d/next/Tests/2024-11-12-21-41-57.gh-issue-58749.LuzORa.rst new file mode 100644 index 00000000000000..3feef9fe6d096e --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2024-11-12-21-41-57.gh-issue-58749.LuzORa.rst @@ -0,0 +1,5 @@ +Added tests to verify that `global` and `nonlocal` statement names are only +restricted from use or assignment before their declaration, aligning with +the updated language specification and CPython's behavior. The tests also +ensure that `global` and `nonlocal` statements exhibit correct behavior. +Change by Beomsoo Kim.