From 88af63020fce83417639e730245b38e2e8ec4720 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 22 Apr 2025 14:14:14 -0400 Subject: [PATCH 1/6] [red-knot] Add mdtests for `global` statement --- .../resources/mdtest/scopes/global.md | 200 ++++++++++++++++++ .../resources/mdtest/scopes/nonlocal.md | 11 - 2 files changed, 200 insertions(+), 11 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/scopes/global.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md new file mode 100644 index 0000000000000..59460557c4204 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md @@ -0,0 +1,200 @@ +# `global` references + +## Implicit global in function + +A name reference to a never-defined symbol in a function is implicitly a global lookup. + +```py +x = 1 + +def f(): + reveal_type(x) # revealed: Unknown | Literal[1] +``` + +## Explicit global in function + +```py +x = 1 + +def f(): + global x + reveal_type(x) # revealed: Unknown | Literal[1] +``` + +## Unassignable type in function + +```py +x: int = 1 + +def f(): + y: int = 1 + # error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`" + y = "" + + global x + # @Todo(error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`") + x = "" +``` + +## Nested intervening scope + +TODO this should give the outer module type of `Unknown | Literal[1]`, not `Literal[""]` + +```py +x = 1 + +def outer(): + x = "" + + def inner(): + global x + reveal_type(x) # revealed: Unknown | Literal[""] +``` + +## Narrowing + +An assignment following a `global` statement should narrow the type in the local scope after the +assignment. The revealed type of `Literal[1]` here is in line with [pyright], while [mypy] reports +`builtins.int`. + +```py +x: int | None + +def f(): + global x + x = 1 + reveal_type(x) # revealed: Literal[1] +``` + +This related case is adapted from a `mypy` [test][t] with the comment: + +> This is unsafe, but we don't generate an error, for convenience. Besides, this is probably a very +> rare case. + +```py +g: str | None + +def f(): + global g + g = "x" + def nested() -> str: + return g +``` + +## `nonlocal` and `global` + +A binding cannot be both `nonlocal` and `global`. This should emit a semantic syntax error. CPython +marks the `nonlocal` line, while `mypy`, `pyright`, and `ruff` (`PLE0115`) mark the `global` line. + +```py +x = 1 + +def f(): + x = 1 + def g() -> None: + nonlocal x + global x # TODO error: [invalid-syntax] "name 'x' is nonlocal and global" + x = None +``` + +## Global declaration after `global` statement + +This is also adapted from a `mypy` [test]. + +```py +def f(): + global x + # TODO this should also not be an error + y = x # error: [unresolved-reference] "Name `x` used when not defined" + x = 1 # No error. + +x = 2 +``` + +## Semantic syntax errors + +TODO: these cases are from the `PLE0118` `ruff` tests and should all cause +`load-before-global-declaration` errors. + +```py +x = 1 + +def f(): + print(x) + global x + print(x) + +def f(): + global x + print(x) + global x + print(x) + +def f(): + print(x) + global x, y + print(x) + +def f(): + global x, y + print(x) + global x, y + print(x) + +def f(): + x = 1 + global x + x = 1 + +def f(): + global x + x = 1 + global x + x = 1 + +def f(): + del x + global x, y + del x + +def f(): + global x, y + del x + global x, y + del x + +def f(): + del x + global x + del x + +def f(): + global x + del x + global x + del x + +def f(): + del x + global x, y + del x + +def f(): + global x, y + del x + global x, y + del x + +def f(): + print(f"{x=}") + global x + +# still an error in module scope +x = None +global x +``` + +[mypy]: https://mypy-play.net/?mypy=latest&python=3.12&gist=84f45a50e34d0426db26f5f57449ab98 +[pyright]: https://pyright-play.net/?pythonVersion=3.8&strict=true&code=B4LgBAlgdgLmA%2BYByB7KBTAUJgJugZmPgBQCUImYVYA5gDYoBGAhnWMJdcGALxgCMnKgCd0AN3SsA%2BjACeAB3TFgpKgGIwoia3Q5wAGQgx0w1gG1%2BAXUxA +[t]: https://github.com/python/mypy/blob/master/test-data/unit/check-optional.test#L1134 +[test]: https://github.com/python/mypy/blob/master/test-data/unit/check-possibly-undefined.test#L194 diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md index 7aa16794c3553..862757ce95e0e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md @@ -43,14 +43,3 @@ def f(): def h(): reveal_type(x) # revealed: Unknown | Literal[1] ``` - -## Implicit global in function - -A name reference to a never-defined symbol in a function is implicitly a global lookup. - -```py -x = 1 - -def f(): - reveal_type(x) # revealed: Unknown | Literal[1] -``` From b3802f25f3d09b5bb96811a7dc4e34541c6fda21 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Wed, 23 Apr 2025 10:26:37 -0400 Subject: [PATCH 2/6] Apply suggestions from code review Co-authored-by: Carl Meyer --- .../resources/mdtest/scopes/global.md | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md index 59460557c4204..81d4f9031fc4b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md @@ -32,13 +32,13 @@ def f(): y = "" global x - # @Todo(error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`") + # TODO: error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`" x = "" ``` ## Nested intervening scope -TODO this should give the outer module type of `Unknown | Literal[1]`, not `Literal[""]` +A `global` statement causes lookup to skip any bindings in intervening scopes: ```py x = 1 @@ -48,14 +48,14 @@ def outer(): def inner(): global x + # TODO should be `Unknown | Literal[1]` reveal_type(x) # revealed: Unknown | Literal[""] ``` ## Narrowing An assignment following a `global` statement should narrow the type in the local scope after the -assignment. The revealed type of `Literal[1]` here is in line with [pyright], while [mypy] reports -`builtins.int`. +assignment. ```py x: int | None @@ -66,20 +66,6 @@ def f(): reveal_type(x) # revealed: Literal[1] ``` -This related case is adapted from a `mypy` [test][t] with the comment: - -> This is unsafe, but we don't generate an error, for convenience. Besides, this is probably a very -> rare case. - -```py -g: str | None - -def f(): - global g - g = "x" - def nested() -> str: - return g -``` ## `nonlocal` and `global` @@ -99,8 +85,6 @@ def f(): ## Global declaration after `global` statement -This is also adapted from a `mypy` [test]. - ```py def f(): global x From 90780f9d29a97d5860392e3ddd444e46306150f2 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 23 Apr 2025 10:29:42 -0400 Subject: [PATCH 3/6] run pre-commit --- .../resources/mdtest/scopes/global.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md index 81d4f9031fc4b..4976f14c7cd5f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md @@ -66,7 +66,6 @@ def f(): reveal_type(x) # revealed: Literal[1] ``` - ## `nonlocal` and `global` A binding cannot be both `nonlocal` and `global`. This should emit a semantic syntax error. CPython @@ -177,8 +176,3 @@ def f(): x = None global x ``` - -[mypy]: https://mypy-play.net/?mypy=latest&python=3.12&gist=84f45a50e34d0426db26f5f57449ab98 -[pyright]: https://pyright-play.net/?pythonVersion=3.8&strict=true&code=B4LgBAlgdgLmA%2BYByB7KBTAUJgJugZmPgBQCUImYVYA5gDYoBGAhnWMJdcGALxgCMnKgCd0AN3SsA%2BjACeAB3TFgpKgGIwoia3Q5wAGQgx0w1gG1%2BAXUxA -[t]: https://github.com/python/mypy/blob/master/test-data/unit/check-optional.test#L1134 -[test]: https://github.com/python/mypy/blob/master/test-data/unit/check-possibly-undefined.test#L194 From 5b75658cff8dfb79d6657f8ca8d00b1d54a274cb Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 23 Apr 2025 10:29:49 -0400 Subject: [PATCH 4/6] add annotations to clean up `Unknown` --- .../resources/mdtest/scopes/global.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md index 4976f14c7cd5f..9ef00c74504ec 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md @@ -41,15 +41,15 @@ def f(): A `global` statement causes lookup to skip any bindings in intervening scopes: ```py -x = 1 +x: int = 1 def outer(): - x = "" + x: str = "" def inner(): global x - # TODO should be `Unknown | Literal[1]` - reveal_type(x) # revealed: Unknown | Literal[""] + # TODO: revealed: int + reveal_type(x) # revealed: str ``` ## Narrowing From aa1c095358a5f9921316bfcdffcf46c6236fd81c Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 23 Apr 2025 10:36:38 -0400 Subject: [PATCH 5/6] add error TODOs for semantic syntax error tests --- .../resources/mdtest/scopes/global.md | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md index 9ef00c74504ec..d4187cc055ad0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md @@ -96,83 +96,82 @@ x = 2 ## Semantic syntax errors -TODO: these cases are from the `PLE0118` `ruff` tests and should all cause -`load-before-global-declaration` errors. +Using a name prior to its `global` declaration in the same scope is a syntax error. ```py x = 1 def f(): - print(x) + print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x print(x) def f(): global x - print(x) + print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x print(x) def f(): - print(x) + print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x, y print(x) def f(): global x, y - print(x) + print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x, y print(x) def f(): - x = 1 + x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x x = 1 def f(): global x - x = 1 + x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x x = 1 def f(): - del x + del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x, y del x def f(): global x, y - del x + del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x, y del x def f(): - del x + del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x del x def f(): global x - del x + del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x del x def f(): - del x + del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x, y del x def f(): global x, y - del x + del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x, y del x def f(): - print(f"{x=}") + print(f"{x=}") # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x # still an error in module scope -x = None +x = None # TODO: error: [invalid-syntax] name `x` is used prior to global declaration global x ``` From 8abea12fdfc2ef3a290d16070496a7ed3ea4f196 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 23 Apr 2025 10:39:40 -0400 Subject: [PATCH 6/6] consistently use `TODO:` before `error:` --- .../red_knot_python_semantic/resources/mdtest/scopes/global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md index d4187cc055ad0..ef5c1b6a72a63 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md @@ -78,7 +78,7 @@ def f(): x = 1 def g() -> None: nonlocal x - global x # TODO error: [invalid-syntax] "name 'x' is nonlocal and global" + global x # TODO: error: [invalid-syntax] "name 'x' is nonlocal and global" x = None ```