diff --git a/conformance/results/mypy/annotations_forward_refs.toml b/conformance/results/mypy/annotations_forward_refs.toml index 15211054e..b74ef2246 100644 --- a/conformance/results/mypy/annotations_forward_refs.toml +++ b/conformance/results/mypy/annotations_forward_refs.toml @@ -2,7 +2,14 @@ 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 some forward references in type annotations in classes at the point of definition instead of end-of-scope +""" +conformance_automated = "Fail" +errors_diff = """ +Line 24: Expected 1 errors +Line 25: Expected 1 errors +Line 94: Expected 1 errors +Line 96: Expected 1 errors """ output = """ annotations_forward_refs.py:41: error: Invalid type comment or annotation [valid-type] @@ -22,17 +29,9 @@ 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] -""" -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]'] +annotations_forward_refs.py:92: error: Name "ClassF" is not defined; did you mean "ClassA", "ClassB", or "ClassC"? [name-defined] +annotations_forward_refs.py:101: error: Function "annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] +annotations_forward_refs.py:101: note: Perhaps you need "Callable[...]" or a callback protocol? +annotations_forward_refs.py:103: error: Function "annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] +annotations_forward_refs.py:103: note: Perhaps you need "Callable[...]" or a callback protocol? """ diff --git a/conformance/results/pycroscope/annotations_forward_refs.toml b/conformance/results/pycroscope/annotations_forward_refs.toml index 63fbfc009..1ae6c941a 100644 --- a/conformance/results/pycroscope/annotations_forward_refs.toml +++ b/conformance/results/pycroscope/annotations_forward_refs.toml @@ -1,19 +1,22 @@ conformant = "Partial" notes = """ Fails to reject `"x" | int` annotations that fail at runtime. +Resolves forward references in type annotations in classes 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 94: Expected 1 errors +Line 96: 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'] +Line 84: Unexpected errors ['./annotations_forward_refs.py:84:12: Undefined name: ClassInner [undefined_name]'] +Line 111: Unexpected errors ['./annotations_forward_refs.py:111:16: Any[error] is not equivalent to ./annotations_forward_refs.py.ClassD.ClassInner'] """ output = """ ./annotations_forward_refs.py:14:8: Undefined name: ClassA [undefined_name] @@ -41,8 +44,10 @@ 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:83:12: Undefined name: ClassInner [undefined_name] +./annotations_forward_refs.py:84:12: Undefined name: ClassInner [undefined_name] +./annotations_forward_refs.py:92:12: Undefined name: ClassF [undefined_name] +./annotations_forward_refs.py:101:7: Unrecognized annotation (self: ./annotations_forward_refs.py.ClassD) -> None [invalid_annotation] +./annotations_forward_refs.py:103:7: Unrecognized annotation (self: ./annotations_forward_refs.py.ClassD) -> None [invalid_annotation] +./annotations_forward_refs.py:111:16: Any[error] is not equivalent to ./annotations_forward_refs.py.ClassD.ClassInner """ diff --git a/conformance/results/pyrefly/annotations_forward_refs.toml b/conformance/results/pyrefly/annotations_forward_refs.toml index 8807cb346..83f996a3e 100644 --- a/conformance/results/pyrefly/annotations_forward_refs.toml +++ b/conformance/results/pyrefly/annotations_forward_refs.toml @@ -1,12 +1,14 @@ 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 in classes at the point of definition instead of end-of-scope +Fails to resolve a forward reference to a nested class """ 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 94: Expected 1 errors +Line 96: Expected 1 errors +Line 84: Unexpected errors ['Could not find name `ClassInner` [unknown-name]'] +Line 111: Unexpected errors ['assert_type(Unknown, ClassD.ClassInner) failed [assert-type]'] """ output = """ ERROR annotations_forward_refs.py:23:12-18: `ClassA` is uninitialized [unbound-name] @@ -28,8 +30,10 @@ 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:83:13-23: Could not find name `ClassInner` [unknown-name] +ERROR annotations_forward_refs.py:84:14-24: Could not find name `ClassInner` [unknown-name] +ERROR annotations_forward_refs.py:92:14-20: Could not find name `ClassF` [unknown-name] +ERROR annotations_forward_refs.py:101:8-11: Expected a type form, got instance of `(self: Self@ClassD) -> None` [not-a-type] +ERROR annotations_forward_refs.py:103:9-12: Expected a type form, got instance of `(self: Self@ClassD) -> None` [not-a-type] +ERROR annotations_forward_refs.py:111:16-45: assert_type(Unknown, ClassD.ClassInner) failed [assert-type] """ diff --git a/conformance/results/pyright/annotations_forward_refs.toml b/conformance/results/pyright/annotations_forward_refs.toml index ca35b5bb1..b28b77967 100644 --- a/conformance/results/pyright/annotations_forward_refs.toml +++ b/conformance/results/pyright/annotations_forward_refs.toml @@ -1,4 +1,13 @@ -conformant = "Pass" +conformant = "Partial" +notes = """ +Prefers to resolve forward references in classes outside of the class instead of end-of-scope +""" +conformance_automated = "Fail" +errors_diff = """ +Line 94: Expected 1 errors +Line 96: Expected 1 errors +Line 103: Expected 1 errors +""" 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 +38,9 @@ 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) -""" -conformance_automated = "Pass" -errors_diff = """ +annotations_forward_refs.py:65:26 - error: "ClassB" is not defined (reportUndefinedVariable) +annotations_forward_refs.py:83:13 - error: "ClassInner" is not defined (reportUndefinedVariable) +annotations_forward_refs.py:92:14 - error: Type of "ClassF" could not be determined because it refers to itself (reportGeneralTypeIssues) +annotations_forward_refs.py:92:14 - error: Variable not allowed in type expression (reportInvalidTypeForm) +annotations_forward_refs.py:101:8 - error: Expected class but received "(self: Self@ClassD) -> None" (reportGeneralTypeIssues) """ diff --git a/conformance/results/results.html b/conformance/results/results.html index 9ec07bb1c..8f43170ee 100644 --- a/conformance/results/results.html +++ b/conformance/results/results.html @@ -294,34 +294,41 @@

Python Type System Conformance Test Results

Partial Partial - Pass Partial Partial + + + Partial + @@ -377,7 +384,7 @@

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% 4.5 / 5 • 90.0% @@ -2571,7 +2578,7 @@

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% 135.5 / 145 • 93.4% 144.5 / 145 • 99.7% diff --git a/conformance/results/ty/annotations_forward_refs.toml b/conformance/results/ty/annotations_forward_refs.toml index d176548df..84c1c29e3 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 94: Expected 1 errors """ output = """ annotations_forward_refs.py:22:7: error[unresolved-reference] Name `ClassA` used when not defined @@ -28,10 +26,10 @@ 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:83:13: error[unresolved-reference] Name `ClassInner` used when not defined +annotations_forward_refs.py:92:14: error[unresolved-reference] Name `ClassF` used when not defined +annotations_forward_refs.py:96:9: error[invalid-type-form] Function `int` is not valid in a type expression +annotations_forward_refs.py:101:8: error[invalid-type-form] Function `int` is not valid in a type expression +annotations_forward_refs.py:103: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..26dfc7e66 100644 --- a/conformance/results/zuban/annotations_forward_refs.toml +++ b/conformance/results/zuban/annotations_forward_refs.toml @@ -1,13 +1,13 @@ conformant = "Partial" notes = """ -Incorrectly generates error for quoted type defined in class scope. +Fails to resolve forward references in classes from builtins +Incorrectly resolves type parameter syntax names in forward references """ conformance_automated = "Fail" 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]'] +Line 82: Unexpected errors ['annotations_forward_refs.py:82: error: Name "bytes" is not defined [name-defined]'] +Line 110: Unexpected errors ['annotations_forward_refs.py:110: error: Expression is of type "Any", not "bytes" [misc]'] +Line 128: Unexpected errors ['annotations_forward_refs.py:128: error: Expression is of type "T", not "int" [misc]', 'annotations_forward_refs.py:128: error: Argument 1 to "identity2" of "ClassE" has incompatible type "int"; expected "T" [arg-type]'] """ output = """ annotations_forward_refs.py:24: error: Forward reference unions cause runtime errors, consider wrapping the whole annotation with a string [misc] @@ -31,12 +31,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 [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:82: error: Name "bytes" is not defined [name-defined] +annotations_forward_refs.py:92: error: Name "ClassF" is not defined [name-defined] +annotations_forward_refs.py:94: error: Name "str" is not defined [name-defined] +annotations_forward_refs.py:96: error: Function "tests.annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] +annotations_forward_refs.py:96: note: Perhaps you need "Callable[...]" or a callback protocol? +annotations_forward_refs.py:101: error: Function "tests.annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] +annotations_forward_refs.py:101: note: Perhaps you need "Callable[...]" or a callback protocol? +annotations_forward_refs.py:103: error: Function "tests.annotations_forward_refs.ClassD.int" is not valid as a type [valid-type] +annotations_forward_refs.py:103: note: Perhaps you need "Callable[...]" or a callback protocol? +annotations_forward_refs.py:110: error: Expression is of type "Any", not "bytes" [misc] +annotations_forward_refs.py:128: error: Expression is of type "T", not "int" [misc] +annotations_forward_refs.py:128: error: Argument 1 to "identity2" of "ClassE" has incompatible type "int"; expected "T" [arg-type] """ diff --git a/conformance/tests/annotations_forward_refs.py b/conformance/tests/annotations_forward_refs.py index 70c644700..6262aa4da 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 @@ -75,25 +74,58 @@ class ClassC: class ClassD: - ClassC: "ClassC" # OK + # OK + simple_attr: ClassA + ClassB: ClassB + ClassC: "ClassC" + bytes_direct: bytes + bytes: "bytes" + inner1: ClassInner # E?: Runtime error prior to 3.14: requires quotes + inner2: "ClassInner" + + class ClassInner: + ... + + inner_after1: ClassInner + inner_after2: "ClassInner" ClassF: "ClassF" # E: circular reference - str: "str" = "" # OK + str: "str" = "" # E: circular reference + + z: "int" = 0 # E: Refers to the local int function, which isn't a valid type def int(self) -> None: # OK ... - x: "int" = 0 # OK + y: int = 0 # E: Refers to the local int function, which isn't a valid type - y: int = 0 # E: Refers to local int, which isn't a legal type expression + x: "int" = 0 # E: Refers to the local int function, which isn't a valid type def __init__(self) -> None: self.ClassC = ClassC() -assert_type(ClassD.str, str) -assert_type(ClassD.x, int) +def check_valid_attributes(d: ClassD) -> None: + assert_type(d.bytes, bytes) + assert_type(d.inner2, ClassD.ClassInner) + assert_type(d.inner_after2, ClassD.ClassInner) + + +class T: + ... + + +class ClassE[T]: + def identity1(self, value: T) -> T: + return value + + def identity2(self, value: "T") -> "T": + return value + + +assert_type(ClassE[int]().identity1(1), int) +assert_type(ClassE[int]().identity2(1), int) # > If a triple quote is used, the string should be parsed as though it is implicitly diff --git a/docs/spec/annotations.rst b/docs/spec/annotations.rst index 558b9b700..f422af186 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 @@ -308,6 +290,14 @@ _module_._class_ name:: from models.a import A from models.b import B +from __future__ import annotations +---------------------------------- + +The presence of the import `from __future__ import annotations` must not +influence type checking. Annotations must be resolved in the exact same way as +if the import was not present. + + Annotating generator functions and coroutines ---------------------------------------------