diff --git a/conformance/results/mypy/annotations_forward_refs.toml b/conformance/results/mypy/annotations_forward_refs.toml index 15211054e..17c700e57 100644 --- a/conformance/results/mypy/annotations_forward_refs.toml +++ b/conformance/results/mypy/annotations_forward_refs.toml @@ -2,7 +2,7 @@ conformant = "Partial" notes = """ Does not report error for a forward reference that is not enclosed in quotes. Does not report error for use of quoted type with `|` operator (runtime error). -Incorrectly generates error for quoted type defined in class scope. +Resolves forward references in type annotations at the point of definition instead of end-of-scope """ output = """ annotations_forward_refs.py:41: error: Invalid type comment or annotation [valid-type] @@ -22,17 +22,16 @@ annotations_forward_refs.py:53: error: Invalid type comment or annotation [vali annotations_forward_refs.py:54: error: Invalid type comment or annotation [valid-type] annotations_forward_refs.py:55: error: Module "types" is not valid as a type [valid-type] annotations_forward_refs.py:55: note: Perhaps you meant to use a protocol matching the module structure? -annotations_forward_refs.py:80: error: Name "ClassF" is not defined; did you mean "ClassA", "ClassB", or "ClassC"? [name-defined] -annotations_forward_refs.py:87: error: Function "annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] -annotations_forward_refs.py:87: note: Perhaps you need "Callable[...]" or a callback protocol? -annotations_forward_refs.py:89: error: Function "annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] -annotations_forward_refs.py:89: note: Perhaps you need "Callable[...]" or a callback protocol? -annotations_forward_refs.py:96: error: Expression is of type int?, not "int" [assert-type] +annotations_forward_refs.py:79: error: Name "ClassF" is not defined; did you mean "ClassA", "ClassB", or "ClassC"? [name-defined] +annotations_forward_refs.py:88: error: Function "annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] +annotations_forward_refs.py:88: note: Perhaps you need "Callable[...]" or a callback protocol? +annotations_forward_refs.py:90: error: Function "annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] +annotations_forward_refs.py:90: note: Perhaps you need "Callable[...]" or a callback protocol? """ conformance_automated = "Fail" errors_diff = """ Line 24: Expected 1 errors Line 25: Expected 1 errors -Line 87: Unexpected errors ['annotations_forward_refs.py:87: error: Function "annotations_forward_refs.ClassD.int" is not valid as a type [valid-type]'] -Line 96: Unexpected errors ['annotations_forward_refs.py:96: error: Expression is of type int?, not "int" [assert-type]'] +Line 81: Expected 1 errors +Line 83: Expected 1 errors """ diff --git a/conformance/results/pycroscope/annotations_forward_refs.toml b/conformance/results/pycroscope/annotations_forward_refs.toml index 63fbfc009..3be8a3b07 100644 --- a/conformance/results/pycroscope/annotations_forward_refs.toml +++ b/conformance/results/pycroscope/annotations_forward_refs.toml @@ -1,19 +1,20 @@ conformant = "Partial" notes = """ Fails to reject `"x" | int` annotations that fail at runtime. +Resolves forward references in type annotations at the point of definition instead of end-of-scope Rejects some valid quoted annotations. """ conformance_automated = "Fail" errors_diff = """ Line 24: Expected 1 errors Line 25: Expected 1 errors +Line 81: Expected 1 errors +Line 83: Expected 1 errors Line 14: Unexpected errors ['./annotations_forward_refs.py:14:8: Undefined name: ClassA [undefined_name]', './annotations_forward_refs.py:14:22: Undefined name: ClassA [undefined_name]', './annotations_forward_refs.py:14:42: Undefined name: ClassA [undefined_name]', './annotations_forward_refs.py:14:62: Undefined name: ClassA [undefined_name]'] Line 16: Unexpected errors ['./annotations_forward_refs.py:16:16: Any[error] is not equivalent to ./annotations_forward_refs.py.ClassA'] Line 17: Unexpected errors ['./annotations_forward_refs.py:17:16: list[Any[error]] is not equivalent to list[./annotations_forward_refs.py.ClassA]'] Line 18: Unexpected errors ['./annotations_forward_refs.py:18:16: list[Any[error]] is not equivalent to list[./annotations_forward_refs.py.ClassA]'] Line 19: Unexpected errors ['./annotations_forward_refs.py:19:16: list[int | Any[error]] is not equivalent to list[./annotations_forward_refs.py.ClassA | int]'] -Line 87: Unexpected errors ['./annotations_forward_refs.py:87:7: Unrecognized annotation (self: ./annotations_forward_refs.py.ClassD) -> None [invalid_annotation]'] -Line 96: Unexpected errors ['./annotations_forward_refs.py:96:12: Any[error] is not equivalent to int'] """ output = """ ./annotations_forward_refs.py:14:8: Undefined name: ClassA [undefined_name] @@ -41,8 +42,7 @@ output = """ ./annotations_forward_refs.py:53:0: Invalid type annotation [invalid_annotation] ./annotations_forward_refs.py:54:0: Invalid type annotation [invalid_annotation] ./annotations_forward_refs.py:55:9: Invalid type annotation [invalid_annotation] -./annotations_forward_refs.py:80:12: Undefined name: ClassF [undefined_name] -./annotations_forward_refs.py:87:7: Unrecognized annotation (self: ./annotations_forward_refs.py.ClassD) -> None [invalid_annotation] -./annotations_forward_refs.py:89:7: Unrecognized annotation (self: ./annotations_forward_refs.py.ClassD) -> None [invalid_annotation] -./annotations_forward_refs.py:96:12: Any[error] is not equivalent to int +./annotations_forward_refs.py:79:12: Undefined name: ClassF [undefined_name] +./annotations_forward_refs.py:88:7: Unrecognized annotation (self: ./annotations_forward_refs.py.ClassD) -> None [invalid_annotation] +./annotations_forward_refs.py:90:7: Unrecognized annotation (self: ./annotations_forward_refs.py.ClassD) -> None [invalid_annotation] """ diff --git a/conformance/results/pyrefly/annotations_forward_refs.toml b/conformance/results/pyrefly/annotations_forward_refs.toml index 8807cb346..3f380851b 100644 --- a/conformance/results/pyrefly/annotations_forward_refs.toml +++ b/conformance/results/pyrefly/annotations_forward_refs.toml @@ -1,12 +1,11 @@ conformant = "Partial" notes = """ -Types in quotes incorrectly refer to shadowing class member. -Does not reject some type forms that require quotes. +Resolves forward references in type annotations at the point of definition instead of end-of-scope """ conformance_automated = "Fail" errors_diff = """ -Line 87: Unexpected errors ['Expected a type form, got instance of `(self: Self@ClassD) -> None` [not-a-type]'] -Line 96: Unexpected errors ['assert_type(Unknown, int) failed [assert-type]'] +Line 81: Expected 1 errors +Line 83: Expected 1 errors """ output = """ ERROR annotations_forward_refs.py:23:12-18: `ClassA` is uninitialized [unbound-name] @@ -28,8 +27,7 @@ ERROR annotations_forward_refs.py:52:11-13: Unary operation cannot be used in an ERROR annotations_forward_refs.py:53:11-21: Boolean operation cannot be used in annotations [invalid-annotation] ERROR annotations_forward_refs.py:54:11-17: F-string cannot be used in annotations [invalid-annotation] ERROR annotations_forward_refs.py:55:11-16: Expected a type form, got instance of `Module[types]` [not-a-type] -ERROR annotations_forward_refs.py:80:14-20: Could not find name `ClassF` [unknown-name] -ERROR annotations_forward_refs.py:87:9-12: Expected a type form, got instance of `(self: Self@ClassD) -> None` [not-a-type] -ERROR annotations_forward_refs.py:89:8-11: Expected a type form, got instance of `(self: Self@ClassD) -> None` [not-a-type] -ERROR annotations_forward_refs.py:96:12-27: assert_type(Unknown, int) failed [assert-type] +ERROR annotations_forward_refs.py:79:14-20: Could not find name `ClassF` [unknown-name] +ERROR annotations_forward_refs.py:88:8-11: Expected a type form, got instance of `(self: Self@ClassD) -> None` [not-a-type] +ERROR annotations_forward_refs.py:90:9-12: Expected a type form, got instance of `(self: Self@ClassD) -> None` [not-a-type] """ diff --git a/conformance/results/pyright/annotations_forward_refs.toml b/conformance/results/pyright/annotations_forward_refs.toml index ca35b5bb1..35e57ca5e 100644 --- a/conformance/results/pyright/annotations_forward_refs.toml +++ b/conformance/results/pyright/annotations_forward_refs.toml @@ -1,4 +1,7 @@ -conformant = "Pass" +conformant = "Partial" +notes = """ +Resolves forward references in type annotations at the point of definition instead of end-of-scope +""" output = """ annotations_forward_refs.py:22:7 - error: "ClassA" is not defined (reportUndefinedVariable) annotations_forward_refs.py:23:12 - error: "ClassA" is not defined (reportUndefinedVariable) @@ -29,11 +32,14 @@ annotations_forward_refs.py:52:11 - error: Unary operator not allowed in type ex annotations_forward_refs.py:53:11 - error: Binary operator not allowed in type expression (reportInvalidTypeForm) annotations_forward_refs.py:54:11 - error: Type expressions cannot use format string literals (f-strings) (reportGeneralTypeIssues) annotations_forward_refs.py:55:10 - error: Module cannot be used as a type (reportGeneralTypeIssues) -annotations_forward_refs.py:66:26 - error: "ClassB" is not defined (reportUndefinedVariable) -annotations_forward_refs.py:80:14 - error: Type of "ClassF" could not be determined because it refers to itself (reportGeneralTypeIssues) -annotations_forward_refs.py:80:14 - error: Variable not allowed in type expression (reportInvalidTypeForm) -annotations_forward_refs.py:89:8 - error: Expected class but received "(self: Self@ClassD) -> None" (reportGeneralTypeIssues) +annotations_forward_refs.py:65:26 - error: "ClassB" is not defined (reportUndefinedVariable) +annotations_forward_refs.py:79:14 - error: Type of "ClassF" could not be determined because it refers to itself (reportGeneralTypeIssues) +annotations_forward_refs.py:79:14 - error: Variable not allowed in type expression (reportInvalidTypeForm) +annotations_forward_refs.py:88:8 - error: Expected class but received "(self: Self@ClassD) -> None" (reportGeneralTypeIssues) """ -conformance_automated = "Pass" +conformance_automated = "Fail" errors_diff = """ +Line 81: Expected 1 errors +Line 83: Expected 1 errors +Line 90: Expected 1 errors """ diff --git a/conformance/results/results.html b/conformance/results/results.html index 7915147fd..53d1756cf 100644 --- a/conformance/results/results.html +++ b/conformance/results/results.html @@ -294,36 +294,36 @@

Python Type System Conformance Test Results

Partial Partial - Pass Partial Partial + Pass annotations_generators @@ -377,9 +377,9 @@

Python Type System Conformance Test Results

4 / 5 • 80.0% 4 / 5 • 80.0% 4.5 / 5 • 90.0% - 5 / 5 • 100.0% 4.5 / 5 • 90.0% 4.5 / 5 • 90.0% + 5 / 5 • 100.0% @@ -2610,9 +2610,9 @@

Python Type System Conformance Test Results

108.5 / 145 • 74.8% 138 / 145 • 95.2% 140.5 / 145 • 96.9% - 135.5 / 145 • 93.4% + 135 / 145 • 93.1% 132 / 145 • 91.0% - 144.5 / 145 • 99.7% + 145 / 145 • 100.0% diff --git a/conformance/results/ty/annotations_forward_refs.toml b/conformance/results/ty/annotations_forward_refs.toml index d176548df..f5d5cbb1a 100644 --- a/conformance/results/ty/annotations_forward_refs.toml +++ b/conformance/results/ty/annotations_forward_refs.toml @@ -1,12 +1,10 @@ conformance_automated = "Fail" conformant = "Partial" notes = """ -Resolves references in type annotations as referring to end-of-scope types (, ) +Fails to reject `str: "str"` forward references """ errors_diff = """ -Line 87: Unexpected errors ['annotations_forward_refs.py:87:9: error[invalid-type-form] Function `int` is not valid in a type expression'] -Line 95: Unexpected errors ['annotations_forward_refs.py:95:1: error[type-assertion-failure] Type `Divergent` does not match asserted type `str`'] -Line 96: Unexpected errors ['annotations_forward_refs.py:96:1: error[type-assertion-failure] Type `Unknown` does not match asserted type `int`'] +Line 81: Expected 1 errors """ output = """ annotations_forward_refs.py:22:7: error[unresolved-reference] Name `ClassA` used when not defined @@ -28,10 +26,9 @@ annotations_forward_refs.py:52:11: error[invalid-type-form] Unary operations are annotations_forward_refs.py:53:11: error[invalid-type-form] Boolean operations are not allowed in parameter annotations annotations_forward_refs.py:54:11: error[invalid-type-form] F-strings are not allowed in parameter annotations annotations_forward_refs.py:55:11: error[invalid-type-form] Module `types` is not valid in a parameter annotation -annotations_forward_refs.py:66:26: error[unresolved-reference] Name `ClassB` used when not defined -annotations_forward_refs.py:80:14: error[unresolved-reference] Name `ClassF` used when not defined -annotations_forward_refs.py:87:9: error[invalid-type-form] Function `int` is not valid in a type expression -annotations_forward_refs.py:89:8: error[invalid-type-form] Function `int` is not valid in a type expression -annotations_forward_refs.py:95:1: error[type-assertion-failure] Type `Divergent` does not match asserted type `str` -annotations_forward_refs.py:96:1: error[type-assertion-failure] Type `Unknown` does not match asserted type `int` +annotations_forward_refs.py:65:26: error[unresolved-reference] Name `ClassB` used when not defined +annotations_forward_refs.py:79:14: error[unresolved-reference] Name `ClassF` used when not defined +annotations_forward_refs.py:83:9: error[invalid-type-form] Function `int` is not valid in a type expression +annotations_forward_refs.py:88:8: error[invalid-type-form] Function `int` is not valid in a type expression +annotations_forward_refs.py:90:9: error[invalid-type-form] Function `int` is not valid in a type expression """ diff --git a/conformance/results/zuban/annotations_forward_refs.toml b/conformance/results/zuban/annotations_forward_refs.toml index 9dce37fac..6d77f1a34 100644 --- a/conformance/results/zuban/annotations_forward_refs.toml +++ b/conformance/results/zuban/annotations_forward_refs.toml @@ -1,13 +1,5 @@ -conformant = "Partial" -notes = """ -Incorrectly generates error for quoted type defined in class scope. -""" -conformance_automated = "Fail" +conformance_automated = "Pass" errors_diff = """ -Line 82: Unexpected errors ['annotations_forward_refs.py:82: error: Name "str" is not defined [name-defined]'] -Line 87: Unexpected errors ['annotations_forward_refs.py:87: error: Function "tests.annotations_forward_refs.ClassD.int" is not valid as a type [valid-type]'] -Line 95: Unexpected errors ['annotations_forward_refs.py:95: error: Expression is of type "Any", not "str" [misc]'] -Line 96: Unexpected errors ['annotations_forward_refs.py:96: error: Expression is of type "Any", not "int" [misc]'] """ output = """ annotations_forward_refs.py:24: error: Forward reference unions cause runtime errors, consider wrapping the whole annotation with a string [misc] @@ -31,12 +23,12 @@ annotations_forward_refs.py:53: error: Invalid type comment or annotation [vali annotations_forward_refs.py:54: error: Invalid type comment or annotation [valid-type] annotations_forward_refs.py:55: error: Module "types" is not valid as a type [valid-type] annotations_forward_refs.py:55: note: Perhaps you meant to use a protocol matching the module structure? -annotations_forward_refs.py:80: error: Name "ClassF" is not defined [name-defined] -annotations_forward_refs.py:82: error: Name "str" is not defined [name-defined] -annotations_forward_refs.py:87: error: Function "tests.annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] -annotations_forward_refs.py:87: note: Perhaps you need "Callable[...]" or a callback protocol? -annotations_forward_refs.py:89: error: Function "tests.annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] -annotations_forward_refs.py:89: note: Perhaps you need "Callable[...]" or a callback protocol? -annotations_forward_refs.py:95: error: Expression is of type "Any", not "str" [misc] -annotations_forward_refs.py:96: error: Expression is of type "Any", not "int" [misc] +annotations_forward_refs.py:79: error: Name "ClassF" is not defined [name-defined] +annotations_forward_refs.py:81: error: Name "str" is not defined [name-defined] +annotations_forward_refs.py:83: error: Function "tests.annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] +annotations_forward_refs.py:83: note: Perhaps you need "Callable[...]" or a callback protocol? +annotations_forward_refs.py:88: error: Function "tests.annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] +annotations_forward_refs.py:88: note: Perhaps you need "Callable[...]" or a callback protocol? +annotations_forward_refs.py:90: error: Function "tests.annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] +annotations_forward_refs.py:90: note: Perhaps you need "Callable[...]" or a callback protocol? """ diff --git a/conformance/tests/annotations_forward_refs.py b/conformance/tests/annotations_forward_refs.py index 70c644700..cdc92a2f4 100644 --- a/conformance/tests/annotations_forward_refs.py +++ b/conformance/tests/annotations_forward_refs.py @@ -7,7 +7,7 @@ import types -from typing import assert_type +from typing import assert_type, Any def func1( @@ -57,10 +57,9 @@ def invalid_annotations( pass -# > It should evaluate without errors once the module has been fully loaded. -# > The local and global namespace in which it is evaluated should be the same -# > namespaces in which default arguments to the same function would be evaluated. - +# > Names within the expression are looked up in the same way as they would be +# > looked up at runtime in Python 3.14 and higher if the annotation was not +# > enclosed in a string literal. class ClassB: def method1(self) -> ClassB: # E?: Runtime error prior to 3.14 @@ -79,23 +78,21 @@ class ClassD: ClassF: "ClassF" # E: circular reference - str: "str" = "" # OK + str: "str" = "" # E: circular reference + + z: "int" = 0 # E: Refers to the local int function def int(self) -> None: # OK ... - x: "int" = 0 # OK + y: int = 0 # E: Refers to the local int function, which isn't a legal type expression - y: int = 0 # E: Refers to local int, which isn't a legal type expression + x: "int" = 0 # E: Refers to a local int function def __init__(self) -> None: self.ClassC = ClassC() -assert_type(ClassD.str, str) -assert_type(ClassD.x, int) - - # > If a triple quote is used, the string should be parsed as though it is implicitly # > surrounded by parentheses. This allows newline characters to be # > used within the string literal. diff --git a/docs/spec/annotations.rst b/docs/spec/annotations.rst index 558b9b700..7ff8e0f70 100644 --- a/docs/spec/annotations.rst +++ b/docs/spec/annotations.rst @@ -223,32 +223,14 @@ String annotations When a type hint cannot be evaluated at runtime, that definition may be expressed as a string literal, to be resolved later. -A situation where this occurs commonly is the definition of a -container class, where the class being defined occurs in the signature -of some of the methods. For example, the following code (the start of -a simple binary tree implementation) does not work:: - - class Tree: - def __init__(self, left: Tree, right: Tree): - self.left = left - self.right = right - -To address this, we write:: - - class Tree: - def __init__(self, left: 'Tree', right: 'Tree'): - self.left = left - self.right = right - -The string literal should contain a valid Python expression (i.e., -``compile(lit, '', 'eval')`` should be a valid code object) and it -should evaluate without errors once the module has been fully loaded. -The local and global namespace in which it is evaluated should be the -same namespaces in which default arguments to the same function would -be evaluated. - -Moreover, the expression should be parseable as a valid type hint, i.e., -it is constrained by the rules from :ref:`the expression grammar `. +The string literal should contain a syntactically valid Python expression +(i.e., ``compile(lit, '', 'eval')`` should succeed) that is a valid +:term:`annotation expression`. Regardless of the Python version used, names +within the expression are looked up in the same way as they would be looked up +at runtime in Python 3.14 and higher if the annotation was not enclosed in a +string literal. Thus, name lookup follows general rules (e.g., the current +function, class, or module scope first, and the builtin scope last), but names +defined later within the same scope can be used in an earlier annotation. If a triple quote is used, the string should be parsed as though it is implicitly surrounded by parentheses. This allows newline characters to be