diff --git a/conformance/results/mypy/constructors_class_statements.toml b/conformance/results/mypy/constructors_class_statements.toml new file mode 100644 index 000000000..ec36fe665 --- /dev/null +++ b/conformance/results/mypy/constructors_class_statements.toml @@ -0,0 +1,27 @@ +conformant = "Partial" +notes = """ +Does not honor the evaluated return type of the implied metaclass call when it isn't a class. +Does not validate keyword arguments in a class statement against the metaclass `__new__` method. +Incorrectly rejects a metaclass `__new__` method annotated to return a type other than a class. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 56: Expected 1 errors +Line 60: Expected 1 errors +Line 77: Unexpected errors ['constructors_class_statements.py:77: error: Incompatible return type for "__new__" (returns "int", but must return a subtype of "type") [misc]'] +Line 85: Unexpected errors ['constructors_class_statements.py:85: error: Expression is of type "type[Class6]", not "int" [assert-type]'] +""" +output = """ +constructors_class_statements.py:30: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases [metaclass] +constructors_class_statements.py:30: note: "constructors_class_statements.MetaA" (metaclass of "constructors_class_statements.BaseA") conflicts with "constructors_class_statements.MetaB" (metaclass of "constructors_class_statements.BaseB") +constructors_class_statements.py:69: error: Argument "key" to "Meta1" has incompatible type "str"; expected "int" [arg-type] +constructors_class_statements.py:77: error: Incompatible return type for "__new__" (returns "int", but must return a subtype of "type") [misc] +constructors_class_statements.py:85: error: Expression is of type "type[Class6]", not "int" [assert-type] +constructors_class_statements.py:93: error: Signature of "__prepare__" incompatible with supertype "builtins.type" [override] +constructors_class_statements.py:93: note: Superclass: +constructors_class_statements.py:93: note: @classmethod +constructors_class_statements.py:93: note: def __prepare__(metacls, str, tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object] +constructors_class_statements.py:93: note: Subclass: +constructors_class_statements.py:93: note: @classmethod +constructors_class_statements.py:93: note: def __prepare__(mcls, name: str, bases: tuple[type, ...]) -> Any +""" diff --git a/conformance/results/mypy/constructors_init_subclass.toml b/conformance/results/mypy/constructors_init_subclass.toml new file mode 100644 index 000000000..d0ee1c849 --- /dev/null +++ b/conformance/results/mypy/constructors_init_subclass.toml @@ -0,0 +1,13 @@ +conformant = "Partial" +notes = """ +Does not validate keyword arguments against `__init_subclass__` when the metaclass defines an `__init__` method (but no `__new__` method). +""" +conformance_automated = "Fail" +errors_diff = """ +Line 58: Expected 1 errors +""" +output = """ +constructors_init_subclass.py:26: error: Argument "flag" to "__init_subclass__" of "Base" has incompatible type "str"; expected "bool" [arg-type] +constructors_init_subclass.py:30: error: Unexpected keyword argument "other" for "__init_subclass__" of "Base" [call-arg] +constructors_init_subclass.py:34: error: Unexpected keyword argument "other" for "__init_subclass__" of "object" [call-arg] +""" diff --git a/conformance/results/mypy/constructors_metaclass.toml b/conformance/results/mypy/constructors_metaclass.toml new file mode 100644 index 000000000..892fb934c --- /dev/null +++ b/conformance/results/mypy/constructors_metaclass.toml @@ -0,0 +1,19 @@ +conformant = "Partial" +notes = """ +Does not evaluate the `__call__` method of the metaclass's own metaclass. +Incorrectly rejects a metaclass `__new__` method annotated to return a type other than a class. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 28: Unexpected errors ['constructors_metaclass.py:28: error: Expression is of type "Meta1", not "Never" [assert-type]'] +Line 57: Unexpected errors ['constructors_metaclass.py:57: error: Incompatible return type for "__new__" (returns "int", but must return a subtype of "type") [misc]'] +Line 67: Unexpected errors ['constructors_metaclass.py:67: error: Expression is of type "Meta3", not "int" [assert-type]', 'constructors_metaclass.py:67: error: Too many arguments for "Meta3" [call-arg]'] +""" +output = """ +constructors_metaclass.py:28: error: Expression is of type "Meta1", not "Never" [assert-type] +constructors_metaclass.py:47: error: Missing named argument "key" for "Meta2" [call-arg] +constructors_metaclass.py:48: error: Argument "key" to "Meta2" has incompatible type "str"; expected "int" [arg-type] +constructors_metaclass.py:57: error: Incompatible return type for "__new__" (returns "int", but must return a subtype of "type") [misc] +constructors_metaclass.py:67: error: Expression is of type "Meta3", not "int" [assert-type] +constructors_metaclass.py:67: error: Too many arguments for "Meta3" [call-arg] +""" diff --git a/conformance/results/mypy/constructors_type_constructor.toml b/conformance/results/mypy/constructors_type_constructor.toml new file mode 100644 index 000000000..5184d8fde --- /dev/null +++ b/conformance/results/mypy/constructors_type_constructor.toml @@ -0,0 +1,15 @@ +conformant = "Partial" +notes = """ +Does not report a single-argument call to a subclass of `type`. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 29: Expected 1 errors +""" +output = """ +constructors_type_constructor.py:30: error: No overload variant of "type" matches argument types "str", "tuple[()]" [call-overload] +constructors_type_constructor.py:30: note: Possible overload variants: +constructors_type_constructor.py:30: note: def type(object, /) -> type +constructors_type_constructor.py:30: note: def type(str, tuple[type, ...], dict[str, Any], /, **kwds: Any) -> type +constructors_type_constructor.py:52: error: Argument 1 to "MetaSingle" has incompatible type "str"; expected "int" [arg-type] +""" diff --git a/conformance/results/pycroscope/constructors_class_statements.toml b/conformance/results/pycroscope/constructors_class_statements.toml new file mode 100644 index 000000000..9677f30a7 --- /dev/null +++ b/conformance/results/pycroscope/constructors_class_statements.toml @@ -0,0 +1,16 @@ +conformant = "Partial" +notes = """ +Does not honor the evaluated return type of the implied metaclass call when it isn't a class. +Does not validate keyword arguments in a class statement against the metaclass `__new__` method. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 56: Expected 1 errors +Line 60: Expected 1 errors +Line 85: Unexpected errors ["./constructors_class_statements.py:85:12: is not equivalent to int"] +""" +output = """ +./constructors_class_statements.py:69:28: Incompatible argument type for key: expected int but got Literal[''] [incompatible_argument] +./constructors_class_statements.py:85:12: is not equivalent to int +./constructors_class_statements.py:93:4: Value of __prepare__ incompatible with base class type [incompatible_override] +""" diff --git a/conformance/results/pycroscope/constructors_init_subclass.toml b/conformance/results/pycroscope/constructors_init_subclass.toml new file mode 100644 index 000000000..134767bc8 --- /dev/null +++ b/conformance/results/pycroscope/constructors_init_subclass.toml @@ -0,0 +1,17 @@ +conformant = "Partial" +notes = """ +Does not validate class statement keyword arguments against `__init_subclass__`. +Incorrectly rejects a call to `type` with three positional arguments and additional keyword arguments. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 26: Expected 1 errors +Line 30: Expected 1 errors +Line 34: Expected 1 errors +Line 58: Expected 1 errors +Line 67: Unexpected errors ['./constructors_init_subclass.py:67:0: Cannot call overloaded function [incompatible_call]'] +""" +output = """ +./constructors_init_subclass.py:67:0: Cannot call overloaded function [incompatible_call] +./constructors_init_subclass.py:68:0: Cannot call overloaded function [incompatible_call] +""" diff --git a/conformance/results/pycroscope/constructors_metaclass.toml b/conformance/results/pycroscope/constructors_metaclass.toml new file mode 100644 index 000000000..e6432f2fd --- /dev/null +++ b/conformance/results/pycroscope/constructors_metaclass.toml @@ -0,0 +1,8 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +./constructors_metaclass.py:47:0: Missing required argument 'key' [incompatible_call] +./constructors_metaclass.py:48:23: Incompatible argument type for key: expected int but got Literal[''] [incompatible_argument] +""" diff --git a/conformance/results/pycroscope/constructors_type_constructor.toml b/conformance/results/pycroscope/constructors_type_constructor.toml new file mode 100644 index 000000000..e966dea43 --- /dev/null +++ b/conformance/results/pycroscope/constructors_type_constructor.toml @@ -0,0 +1,15 @@ +conformant = "Partial" +notes = """ +Does not report a single-argument call to a subclass of `type`. +Does not evaluate the result of a single-argument call to `type` as `type[T]`. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 29: Expected 1 errors +Line 28: Unexpected errors ["./constructors_type_constructor.py:28:12: type 'int' is not equivalent to type[int]"] +""" +output = """ +./constructors_type_constructor.py:28:12: type 'int' is not equivalent to type[int] +./constructors_type_constructor.py:30:0: Cannot call overloaded function [incompatible_call] +./constructors_type_constructor.py:52:11: Incompatible argument type for x: expected int but got Literal[''] [incompatible_argument] +""" diff --git a/conformance/results/pyrefly/constructors_class_statements.toml b/conformance/results/pyrefly/constructors_class_statements.toml new file mode 100644 index 000000000..d41c5fa79 --- /dev/null +++ b/conformance/results/pyrefly/constructors_class_statements.toml @@ -0,0 +1,17 @@ +conformant = "Partial" +notes = """ +Does not honor the evaluated return type of the implied metaclass call when it isn't a class. +Does not validate keyword arguments in a class statement against the metaclass `__new__` method. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 56: Expected 1 errors +Line 60: Expected 1 errors +Line 85: Unexpected errors ['assert_type(type[Class6], int) failed [assert-type]'] +""" +output = """ +ERROR constructors_class_statements.py:30:7-20: Class `ClassConflict` has metaclass `MetaA` from base class `BaseA` which is not compatible with metaclass `MetaB` from base class `BaseB` [invalid-inheritance] +ERROR constructors_class_statements.py:69:29-31: Argument `Literal['']` is not assignable to parameter `key` with type `int` in function `Meta1.__new__` [bad-argument-type] +ERROR constructors_class_statements.py:85:12-25: assert_type(type[Class6], int) failed [assert-type] +ERROR constructors_class_statements.py:93:9-20: Class member `Meta3.__prepare__` overrides parent class `type` in an inconsistent manner [bad-override] +""" diff --git a/conformance/results/pyrefly/constructors_init_subclass.toml b/conformance/results/pyrefly/constructors_init_subclass.toml new file mode 100644 index 000000000..f9b1e8618 --- /dev/null +++ b/conformance/results/pyrefly/constructors_init_subclass.toml @@ -0,0 +1,14 @@ +conformant = "Partial" +notes = """ +Does not validate keyword arguments against `object.__init_subclass__` in a class statement with no explicit base classes. +Does not validate keyword arguments against `__init_subclass__` when the metaclass defines an `__init__` method (but no `__new__` method). +""" +conformance_automated = "Fail" +errors_diff = """ +Line 34: Expected 1 errors +Line 58: Expected 1 errors +""" +output = """ +ERROR constructors_init_subclass.py:26:25-27: Argument `Literal['']` is not assignable to parameter `flag` with type `bool` in function `Base.__init_subclass__` [bad-argument-type] +ERROR constructors_init_subclass.py:30:20-25: Unexpected keyword argument `other` in function `Base.__init_subclass__` [unexpected-keyword] +""" diff --git a/conformance/results/pyrefly/constructors_metaclass.toml b/conformance/results/pyrefly/constructors_metaclass.toml new file mode 100644 index 000000000..d9574d97b --- /dev/null +++ b/conformance/results/pyrefly/constructors_metaclass.toml @@ -0,0 +1,8 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +ERROR constructors_metaclass.py:47:6-19: Missing argument `key` in function `Meta2.__new__` [missing-argument] +ERROR constructors_metaclass.py:48:24-26: Argument `Literal['']` is not assignable to parameter `key` with type `int` in function `Meta2.__new__` [bad-argument-type] +""" diff --git a/conformance/results/pyrefly/constructors_type_constructor.toml b/conformance/results/pyrefly/constructors_type_constructor.toml new file mode 100644 index 000000000..3243d0fa9 --- /dev/null +++ b/conformance/results/pyrefly/constructors_type_constructor.toml @@ -0,0 +1,12 @@ +conformant = "Partial" +notes = """ +Does not report a single-argument call to a subclass of `type`. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 29: Expected 1 errors +""" +output = """ +ERROR constructors_type_constructor.py:30:5-14: No matching overload found for function `type.__new__` called with arguments: (type[type], Literal['A'], tuple[()]) [no-matching-overload] +ERROR constructors_type_constructor.py:52:12-14: Argument `Literal['']` is not assignable to parameter `x` with type `int` in function `MetaSingle.__new__` [bad-argument-type] +""" diff --git a/conformance/results/pyright/constructors_class_statements.toml b/conformance/results/pyright/constructors_class_statements.toml new file mode 100644 index 000000000..3999a182f --- /dev/null +++ b/conformance/results/pyright/constructors_class_statements.toml @@ -0,0 +1,20 @@ +conformant = "Partial" +notes = """ +Does not honor the evaluated return type of the implied metaclass call when it isn't a class. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 85: Unexpected errors ['constructors_class_statements.py:85:13 - error: "assert_type" mismatch: expected "int" but received "type[Class6]" (reportAssertTypeFailure)'] +""" +output = """ +constructors_class_statements.py:30:7 - error: The metaclass of a derived class must be a subclass of the metaclasses of all its base classes +  Metaclass "MetaA" conflicts with "MetaB" (reportGeneralTypeIssues) +constructors_class_statements.py:56:35 - error: Argument of type "Literal['']" cannot be assigned to parameter of type "int" in function "__new__" +  "Literal['']" is not assignable to "int" (reportArgumentType) +constructors_class_statements.py:60:7 - error: Argument missing for parameter "key" (reportGeneralTypeIssues) +constructors_class_statements.py:69:29 - error: Argument of type "Literal['']" cannot be assigned to parameter "key" of type "int" in function "__new__" +  "Literal['']" is not assignable to "int" (reportArgumentType) +constructors_class_statements.py:85:13 - error: "assert_type" mismatch: expected "int" but received "type[Class6]" (reportAssertTypeFailure) +constructors_class_statements.py:93:9 - error: Method "__prepare__" overrides class "type" in an incompatible manner +  Parameter "**kwds" has no corresponding parameter (reportIncompatibleMethodOverride) +""" diff --git a/conformance/results/pyright/constructors_init_subclass.toml b/conformance/results/pyright/constructors_init_subclass.toml new file mode 100644 index 000000000..060e4a024 --- /dev/null +++ b/conformance/results/pyright/constructors_init_subclass.toml @@ -0,0 +1,15 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +constructors_init_subclass.py:26:7 - error: Incorrect keyword arguments for __init_subclass__ method (reportGeneralTypeIssues) +constructors_init_subclass.py:26:25 - error: Argument of type "Literal['']" cannot be assigned to parameter "flag" of type "bool" in function "__init_subclass__" +  "Literal['']" is not assignable to "bool" (reportArgumentType) +constructors_init_subclass.py:30:7 - error: Incorrect keyword arguments for __init_subclass__ method (reportGeneralTypeIssues) +constructors_init_subclass.py:30:20 - error: No parameter named "other" (reportCallIssue) +constructors_init_subclass.py:34:7 - error: Incorrect keyword arguments for __init_subclass__ method (reportGeneralTypeIssues) +constructors_init_subclass.py:34:14 - error: No parameter named "other" (reportCallIssue) +constructors_init_subclass.py:58:7 - error: Incorrect keyword arguments for __init_subclass__ method (reportGeneralTypeIssues) +constructors_init_subclass.py:58:40 - error: No parameter named "key" (reportCallIssue) +""" diff --git a/conformance/results/pyright/constructors_metaclass.toml b/conformance/results/pyright/constructors_metaclass.toml new file mode 100644 index 000000000..93855ac70 --- /dev/null +++ b/conformance/results/pyright/constructors_metaclass.toml @@ -0,0 +1,9 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +constructors_metaclass.py:47:1 - error: Argument missing for parameter "key" (reportCallIssue) +constructors_metaclass.py:48:24 - error: Argument of type "Literal['']" cannot be assigned to parameter "key" of type "int" in function "__new__" +  "Literal['']" is not assignable to "int" (reportArgumentType) +""" diff --git a/conformance/results/pyright/constructors_type_constructor.toml b/conformance/results/pyright/constructors_type_constructor.toml new file mode 100644 index 000000000..d4f86d945 --- /dev/null +++ b/conformance/results/pyright/constructors_type_constructor.toml @@ -0,0 +1,16 @@ +conformant = "Partial" +notes = """ +Does not report a single-argument call to a subclass of `type`. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 29: Expected 1 errors +""" +output = """ +constructors_type_constructor.py:30:1 - error: No overloads for "__new__" match the provided arguments +  Argument types: (Literal['A'], tuple[()]) (reportCallIssue) +constructors_type_constructor.py:30:1 - error: No overloads for "__init__" match the provided arguments +  Argument types: (Literal['A'], tuple[()]) (reportCallIssue) +constructors_type_constructor.py:52:12 - error: Argument of type "Literal['']" cannot be assigned to parameter "x" of type "int" in function "__new__" +  "Literal['']" is not assignable to "int" (reportArgumentType) +""" diff --git a/conformance/results/results.html b/conformance/results/results.html index 3877f3c17..1e445a5bc 100644 --- a/conformance/results/results.html +++ b/conformance/results/results.html @@ -1615,6 +1615,51 @@

Python Type System Conformance Test Results

Pass + + constructors_class_statements + + Partial + + + + Partial + + + + Partial + + + + Partial + + + + Partial + + + + Partial + + + constructors_consistency @@ -1629,14 +1674,106 @@

Python Type System Conformance Test Results

Pass Pass + + constructors_init_subclass + + Partial + + + + Partial + + + + Partial + + + Pass + + Partial + + + + Partial + + + + + constructors_metaclass + + Partial + + + Pass + Pass + Pass + Pass + Pass + + + constructors_type_constructor + + Partial + + + + Partial + + + + Partial + + + + Partial + + + + Partial + + + + Partial + + + - 3 / 6 • 50.0% - 6 / 6 • 100.0% - 6 / 6 • 100.0% - 6 / 6 • 100.0% - 5 / 6 • 83.3% - 6 / 6 • 100.0% + 5 / 10 • 50.0% + 8.5 / 10 • 85.0% + 8.5 / 10 • 85.0% + 9 / 10 • 90.0% + 7.5 / 10 • 75.0% + 8.5 / 10 • 85.0% @@ -2583,12 +2720,12 @@

Python Type System Conformance Test Results

- 109 / 146 • 74.7% - 138.5 / 146 • 94.9% - 141 / 146 • 96.6% - 135.5 / 146 • 92.8% - 139.5 / 146 • 95.5% - 146 / 146 • 100.0% + 111 / 150 • 74.0% + 141 / 150 • 94.0% + 143.5 / 150 • 95.7% + 138.5 / 150 • 92.3% + 142 / 150 • 94.7% + 148.5 / 150 • 99.0% diff --git a/conformance/results/ty/constructors_class_statements.toml b/conformance/results/ty/constructors_class_statements.toml new file mode 100644 index 000000000..cf8dcc9da --- /dev/null +++ b/conformance/results/ty/constructors_class_statements.toml @@ -0,0 +1,17 @@ +conformant = "Partial" +notes = """ +Does not honor the evaluated return type of the implied metaclass call when it isn't a class. +Does not validate keyword arguments in a class statement against the metaclass `__new__` method. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 56: Expected 1 errors +Line 60: Expected 1 errors +Line 85: Unexpected errors ["constructors_class_statements.py:85:1: error[type-assertion-failure] Type `` does not match asserted type `int`"] +""" +output = """ +constructors_class_statements.py:30:1: error[conflicting-metaclass] The metaclass of a derived class (`ClassConflict`) must be a subclass of the metaclasses of all its bases, but `MetaA` (metaclass of base class `BaseA`) and `MetaB` (metaclass of base class `BaseB`) have no subclass relationship +constructors_class_statements.py:69:25: error[invalid-argument-type] Argument to constructor `Meta1.__new__` is incorrect: Expected `int`, found `Literal[""]` +constructors_class_statements.py:85:1: error[type-assertion-failure] Type `` does not match asserted type `int` +constructors_class_statements.py:93:9: error[invalid-method-override] Invalid override of method `__prepare__`: Definition is incompatible with `type.__prepare__` +""" diff --git a/conformance/results/ty/constructors_init_subclass.toml b/conformance/results/ty/constructors_init_subclass.toml new file mode 100644 index 000000000..d59949e2b --- /dev/null +++ b/conformance/results/ty/constructors_init_subclass.toml @@ -0,0 +1,14 @@ +conformant = "Partial" +notes = """ +Does not validate keyword arguments against `object.__init_subclass__` in a class statement with no explicit base classes. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 34: Expected 1 errors +""" +output = """ +constructors_init_subclass.py:26:20: error[invalid-argument-type] Argument to function `Base.__init_subclass__` is incorrect: Expected `bool`, found `Literal[""]` +constructors_init_subclass.py:30:20: error[unknown-argument] Argument `other` does not match any known parameter of function `Base.__init_subclass__` +constructors_init_subclass.py:58:40: error[unknown-argument] Argument `key` does not match any known parameter of function `Base.__init_subclass__` +constructors_init_subclass.py:94:42: error[unknown-argument] Argument `other` does not match any known parameter of function `Base.__init_subclass__` +""" diff --git a/conformance/results/ty/constructors_metaclass.toml b/conformance/results/ty/constructors_metaclass.toml new file mode 100644 index 000000000..4926810b9 --- /dev/null +++ b/conformance/results/ty/constructors_metaclass.toml @@ -0,0 +1,8 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +constructors_metaclass.py:47:1: error[missing-argument] No argument provided for required parameter `key` of constructor `Meta2.__new__` +constructors_metaclass.py:48:20: error[invalid-argument-type] Argument to constructor `Meta2.__new__` is incorrect: Expected `int`, found `Literal[""]` +""" diff --git a/conformance/results/ty/constructors_type_constructor.toml b/conformance/results/ty/constructors_type_constructor.toml new file mode 100644 index 000000000..271361490 --- /dev/null +++ b/conformance/results/ty/constructors_type_constructor.toml @@ -0,0 +1,12 @@ +conformant = "Partial" +notes = """ +Does not report a single-argument call to a subclass of `type`. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 29: Expected 1 errors +""" +output = """ +constructors_type_constructor.py:30:1: error[no-matching-overload] No overload of class `type` matches arguments +constructors_type_constructor.py:52:12: error[invalid-argument-type] Argument to constructor `MetaSingle.__new__` is incorrect: Expected `int`, found `Literal[""]` +""" diff --git a/conformance/results/zuban/constructors_class_statements.toml b/conformance/results/zuban/constructors_class_statements.toml new file mode 100644 index 000000000..36cc58b3e --- /dev/null +++ b/conformance/results/zuban/constructors_class_statements.toml @@ -0,0 +1,23 @@ +conformant = "Partial" +notes = """ +Does not honor the evaluated return type of the implied metaclass call when it isn't a class. +Does not validate keyword arguments in a class statement against the metaclass `__new__` method. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 56: Expected 1 errors +Line 60: Expected 1 errors +Line 85: Unexpected errors ['constructors_class_statements.py:85: error: Expression is of type "type[Class6]", not "int" [misc]'] +""" +output = """ +constructors_class_statements.py:30: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases [metaclass] +constructors_class_statements.py:69: error: Argument "key" to "Meta1" has incompatible type "str"; expected "int" [arg-type] +constructors_class_statements.py:85: error: Expression is of type "type[Class6]", not "int" [misc] +constructors_class_statements.py:93: error: Signature of "__prepare__" incompatible with supertype "builtins.type" [override] +constructors_class_statements.py:93: note: Superclass: +constructors_class_statements.py:93: note: @classmethod +constructors_class_statements.py:93: note: def __prepare__(cls, str, tuple[type[Any], ...], /, **kwds: Any) -> MutableMapping[str, object] +constructors_class_statements.py:93: note: Subclass: +constructors_class_statements.py:93: note: @classmethod +constructors_class_statements.py:93: note: def __prepare__(cls, name: str, bases: tuple[type[Any], ...]) -> dict[Any, Any] +""" diff --git a/conformance/results/zuban/constructors_init_subclass.toml b/conformance/results/zuban/constructors_init_subclass.toml new file mode 100644 index 000000000..d0ee1c849 --- /dev/null +++ b/conformance/results/zuban/constructors_init_subclass.toml @@ -0,0 +1,13 @@ +conformant = "Partial" +notes = """ +Does not validate keyword arguments against `__init_subclass__` when the metaclass defines an `__init__` method (but no `__new__` method). +""" +conformance_automated = "Fail" +errors_diff = """ +Line 58: Expected 1 errors +""" +output = """ +constructors_init_subclass.py:26: error: Argument "flag" to "__init_subclass__" of "Base" has incompatible type "str"; expected "bool" [arg-type] +constructors_init_subclass.py:30: error: Unexpected keyword argument "other" for "__init_subclass__" of "Base" [call-arg] +constructors_init_subclass.py:34: error: Unexpected keyword argument "other" for "__init_subclass__" of "object" [call-arg] +""" diff --git a/conformance/results/zuban/constructors_metaclass.toml b/conformance/results/zuban/constructors_metaclass.toml new file mode 100644 index 000000000..83ca58b5e --- /dev/null +++ b/conformance/results/zuban/constructors_metaclass.toml @@ -0,0 +1,8 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +constructors_metaclass.py:47: error: Missing named argument "key" for "Meta2" [call-arg] +constructors_metaclass.py:48: error: Argument "key" to "Meta2" has incompatible type "str"; expected "int" [arg-type] +""" diff --git a/conformance/results/zuban/constructors_type_constructor.toml b/conformance/results/zuban/constructors_type_constructor.toml new file mode 100644 index 000000000..ffa0543f2 --- /dev/null +++ b/conformance/results/zuban/constructors_type_constructor.toml @@ -0,0 +1,18 @@ +conformant = "Partial" +notes = """ +Does not report a single-argument call to a subclass of `type`. +Does not distribute the evaluated type of a single-argument call to `type` over union types. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 29: Expected 1 errors +Line 16: Unexpected errors ['constructors_type_constructor.py:16: error: Expression is of type "type[int | str]", not "type[int] | type[str]" [misc]'] +""" +output = """ +constructors_type_constructor.py:16: error: Expression is of type "type[int | str]", not "type[int] | type[str]" [misc] +constructors_type_constructor.py:30: error: No overload variant of "type" matches argument types "str", "tuple[()]" [call-overload] +constructors_type_constructor.py:30: note: Possible overload variants: +constructors_type_constructor.py:30: note: def __init__(self, object, /) -> type +constructors_type_constructor.py:30: note: def __init__(self, str, tuple[type[Any], ...], dict[str, Any], /, **kwds: Any) -> type +constructors_type_constructor.py:52: error: Argument 1 to "MetaSingle" has incompatible type "str"; expected "int" [arg-type] +""" diff --git a/conformance/tests/constructors_class_statements.py b/conformance/tests/constructors_class_statements.py new file mode 100644 index 000000000..6785b296d --- /dev/null +++ b/conformance/tests/constructors_class_statements.py @@ -0,0 +1,110 @@ +""" +Tests the evaluation of the implied metaclass call performed by class +statements. +""" + +from typing import Any, assert_type + +# Specification: https://typing.readthedocs.io/en/latest/spec/constructors.html#class-statements + +# > Type checkers may report an error for a class statement whose base classes +# > have incompatible metaclasses. + + +class MetaA(type): + pass + + +class MetaB(type): + pass + + +class BaseA(metaclass=MetaA): + pass + + +class BaseB(metaclass=MetaB): + pass + + +class ClassConflict(BaseA, BaseB): # E?: metaclass conflict + pass + + +# > Type checkers should validate keyword arguments in a class statement's +# > argument list (other than ``metaclass``) by evaluating the implied +# > metaclass call using the constructor call rules described in +# > :ref:`constructor-calls`. + + +class Meta1(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + +class Class1(metaclass=Meta1, key=3): # OK + pass + + +class Class2(metaclass=Meta1, key=""): # E: wrong type for "key" + pass + + +class Class3(metaclass=Meta1): # E: missing keyword argument "key" + pass + + +# > Keyword arguments in a direct metaclass call (such as the last two calls +# > in the example above) require no special handling: they are validated as +# > part of evaluating the call using the standard constructor call rules. + +Meta1("Class4", (), {}, key=3) # OK +Meta1("Class5", (), {}, key="") # E: wrong type for "key" + + +# > Type checkers should honor the evaluated return type of the implied +# > metaclass call, even if the evaluated type isn't a class. + + +class Meta2(type): + def __new__(mcls, *args: object, **kwargs: object) -> int: + return 1 + + +class Class6(metaclass=Meta2): + pass + + +assert_type(Class6, int) + + +# > Type checkers may validate the implied call to __prepare__(). + + +class Meta3(type): + @classmethod + def __prepare__( # E?: incompatible override of type.__prepare__() (out of scope for this test) + mcls, name: str, bases: tuple[type, ...] + ): # No **kwds + return {} + + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + +class Class7(metaclass=Meta3, key=3): # E?: "key" is not accepted by __prepare__() + pass diff --git a/conformance/tests/constructors_init_subclass.py b/conformance/tests/constructors_init_subclass.py new file mode 100644 index 000000000..8e00d1a3c --- /dev/null +++ b/conformance/tests/constructors_init_subclass.py @@ -0,0 +1,95 @@ +""" +Tests the validation of class statement keyword arguments against the +``__init_subclass__`` method of the parent class. +""" + +from typing import Any + +# Specification: https://typing.readthedocs.io/en/latest/spec/constructors.html#the-init-subclass-method + +# > If the metaclass of the class being defined does not define its own +# > __new__() method (including when no explicit metaclass is specified), +# > type checkers should validate the keyword arguments in a class statement's +# > argument list against the __init_subclass__() method of the parent +# > class. + + +class Base: + def __init_subclass__(cls, *, flag: bool = False) -> None: + super().__init_subclass__() + + +class Class1(Base, flag=True): # OK + pass + + +class Class2(Base, flag=""): # E: wrong type for "flag" + pass + + +class Class3(Base, other=1): # E: no parameter named "other" + pass + + +class Class4(other=1): # E: object.__init_subclass__() accepts no keyword arguments + pass + + +# > A metaclass __init__() method has no effect on this rule: when the +# > metaclass does not define its own __new__() method, type.__new__() +# > still forwards the keyword arguments to __init_subclass__(), so the +# > keyword arguments should satisfy both the metaclass __init__() method +# > (as part of validating the implied metaclass call) and the +# > __init_subclass__() method of the parent class. + + +class MetaInit(type): + def __init__( + cls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ) -> None: + super().__init__(name, bases, namespace) + + +class Class5(Base, metaclass=MetaInit, key=1): # E: "key" is not accepted by Base.__init_subclass__() + pass + + +# > The same forwarding occurs when the metaclass is called directly ... +# > Type checkers may validate keyword arguments in such calls against the +# > __init_subclass__() method of the parent class when the base classes can +# > be statically determined. + +type("ClassD", (Base,), {}, flag=True) # OK +type("ClassE", (Base,), {}, other=1) # E?: no parameter named "other" + + +# > If the metaclass defines its own __new__() method that accepts keyword +# > arguments only through a **kwargs parameter, whether these arguments +# > are forwarded to type.__new__() (and from there to +# > __init_subclass__()) cannot generally be determined statically. In this +# > situation, type checkers may additionally validate the keyword arguments +# > against the __init_subclass__() method of the parent class. + + +class MetaKwargs(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + **kwargs: Any, + ): + return super().__new__(mcls, name, bases, namespace, **kwargs) + + +class Class6(Base, metaclass=MetaKwargs, flag=True): # OK + pass + + +class Class7(Base, metaclass=MetaKwargs, other=1): # E?: no parameter named "other" + pass diff --git a/conformance/tests/constructors_metaclass.py b/conformance/tests/constructors_metaclass.py new file mode 100644 index 000000000..0e635e45e --- /dev/null +++ b/conformance/tests/constructors_metaclass.py @@ -0,0 +1,67 @@ +""" +Tests the evaluation of metaclass constructor calls (calls to a metaclass +that create a new class). +""" + +from typing import Any, Never, assert_type + +# Specification: https://typing.readthedocs.io/en/latest/spec/constructors.html#metaclass-constructors + +# > In both cases, the metaclass call should be evaluated using the same rules +# > described in the sections above: the __call__() method of the metaclass's +# > own metaclass (typically type.__call__()) is invoked, which in turn calls +# > the __new__() and __init__() methods of the metaclass. + + +class MetaMeta(type): + def __call__(cls, *args, **kwargs) -> Never: + raise TypeError("Classes cannot be created with this metaclass") + + +class Meta1(type, metaclass=MetaMeta): + pass + + +# This needs to be in a separate scope, because some type checkers might mark +# the statements after it as unreachable. +if bool(): + assert_type(Meta1("A", (), {}), Never) + + +class Meta2(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + +# The return type is an instance of the metaclass being called, but type +# checkers may infer a more precise type, so assignability is checked +# instead of using assert_type(). +meta2_instance: Meta2 = Meta2("B", (), {}, key=1) # OK +Meta2("B", (), {}) # E: missing keyword argument "key" +Meta2("B", (), {}, key="") # E: wrong type for "key" + + +# > If the evaluated return type of __new__() is not the class being +# > constructed (or a subclass thereof), a type checker should assume that the +# > __init__() method will not be called. + + +class Meta3(type): + def __new__( + mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any] + ) -> int: + return 0 + + # Not evaluated, as __new__() does not return an instance of Meta3: + def __init__(cls, x: str) -> None: + pass + + +assert_type(Meta3("C", (), {}), int) diff --git a/conformance/tests/constructors_type_constructor.py b/conformance/tests/constructors_type_constructor.py new file mode 100644 index 000000000..eb0bc8d45 --- /dev/null +++ b/conformance/tests/constructors_type_constructor.py @@ -0,0 +1,52 @@ +""" +Tests the special-case handling of calls to the ``type`` constructor. +""" + +from typing import assert_type + +# Specification: https://typing.readthedocs.io/en/latest/spec/constructors.html#the-type-constructor + +# > Although the single-argument form is typically declared with a return type +# > of `type`, type checkers should special-case this form and evaluate its +# > result as type[T], where T is the type of the argument. + + +def func1(x: int, y: int | str) -> None: + assert_type(type(x), type[int]) + assert_type(type(y), type[int] | type[str]) + + +# > At runtime, the single-argument form applies only when the class being +# > called is `type` itself, and is not inherited by metaclasses: a +# > single-argument call to a subclass of `type` raises a TypeError. + + +class Meta(type): + pass + + +assert_type(type(1), type[int]) # OK, uses the single-argument form +Meta(1) # E: single-argument form does not apply to subclasses +type("A", ()) # E: two-argument form does not exist + +# > The evaluated return type of the three-argument form is an instance of the +# > metaclass being called ... Type checkers may infer a more precise type for +# > the returned class object. + +meta_instance: Meta = Meta("A", (), {}) # OK, uses the three-argument form + + +# > This special-casing applies only to the __new__() and __init__() +# > methods inherited from `type`. If a metaclass defines its own +# > __new__() method that accepts a single argument, calls to it should be +# > evaluated using the rules for regular constructor calls described earlier +# > in this chapter. + + +class MetaSingle(type): + def __new__(mcls, x: int): + return super().__new__(mcls, "X", (), {}) + + +MetaSingle(1) # OK +MetaSingle("") # E: wrong type for "x" diff --git a/docs/conf.py b/docs/conf.py index f16401bae..cd155fffa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,4 +52,7 @@ html_static_path = [] extensions = ['sphinx.ext.intersphinx'] -intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'py315': ('https://docs.python.org/3.15', None), +} diff --git a/docs/spec/constructors.rst b/docs/spec/constructors.rst index c95ecc488..51c6d2f50 100644 --- a/docs/spec/constructors.rst +++ b/docs/spec/constructors.rst @@ -3,6 +3,8 @@ Constructors Calls to constructors require special handling within type checkers. +.. _`constructor-calls`: + Constructor Calls ----------------- @@ -488,3 +490,297 @@ callable. def __init__[V](self, x: T, y: list[V], z: V) -> None: ... reveal_type(accepts_callable(MyClass)) # ``def [T, V] (x: T, y: list[V], z: V) -> MyClass[T]`` + + +Metaclass Constructors +---------------------- + +A class object is itself an instance of its :term:`python:metaclass`, so the +creation of a class is also a constructor call, one made on the metaclass. While +the sections above describe how a metaclass participates in the construction of +*instances* of a class, the following sections describe the construction of class objects +themselves. + +A metaclass constructor is invoked in one of two ways: + +1. Directly, by calling the metaclass with a class name, a tuple of base + classes, and a namespace dictionary (for example, + ``Meta(name, bases, namespace)``), optionally along with additional keyword + arguments. +2. Implicitly, by a :keyword:`python:class` statement, which assembles these + three arguments from the statement and the class body and then calls the metaclass. + +In both cases, the metaclass call should be evaluated using the same rules +described in the sections above: the :meth:`!__call__` method of the metaclass's +own metaclass (typically :meth:`!type.__call__`) is invoked, which in turn calls +the :meth:`!__new__` and :meth:`!__init__` methods of the metaclass. These methods are +typically inherited from :class:`type`, whose type definitions require special +handling by type checkers, as described below. + +The following example illustrates these rules applied to metaclass calls: + + :: + + class MetaMeta(type): + def __call__(cls, *args, **kwargs) -> Never: + raise TypeError("Classes cannot be created with this metaclass") + + class Meta1(type, metaclass=MetaMeta): + pass + + # The __call__() method of the metaclass's own metaclass is evaluated first: + assert_type(Meta1("A", (), {}), Never) + + class Meta2(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + # Then, the __new__ and __init__ methods of the metaclass are evaluated: + Meta2("B", (), {}, key=1) # OK, evaluates to an instance of Meta2 + Meta2("B", (), {}) # Type error: missing argument "key" + + class Meta3(type): + def __new__( + mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any] + ) -> int: + return 0 + + # Not evaluated, as __new__ does not return an instance of Meta3: + def __init__(cls, x: str) -> None: + pass + + assert_type(Meta3("C", (), {}), int) + + +The ``type`` Constructor +------------------------ + +In addition to being the default metaclass, :class:`type` serves a second purpose: +when called with a single argument, it returns the type of that argument rather than +creating a new class. :class:`type` therefore supports two distinct call forms, +which are distinguished by the number of positional arguments: + +* ``type(obj, /)`` returns the class of ``obj``. +* ``type(name, bases, dict, /, **kwds)`` creates and returns a new class. + +These two forms are typically declared as overloads of the :meth:`!__new__` and +:meth:`!__init__` methods in the type definition of :class:`type`. Both forms require +special-case handling by type checkers. + +Although the single-argument form is typically declared with a return type of +``type``, type checkers should special-case this form and evaluate its result +as ``type[T]``, where ``T`` is the type of the argument. + + :: + + def func(x: int, y: int | str) -> None: + assert_type(type(x), type[int]) + assert_type(type(y), type[int] | type[str]) + +At runtime, the single-argument form applies only when the class being called +is :class:`type` itself, and is not inherited by metaclasses: a single-argument call +to a subclass of :class:`type` raises a :exc:`TypeError`. + + :: + + class Meta(type): + pass + + assert_type(type(1), type[int]) # OK, uses the single-argument form + Meta(1) # Type error: single-argument form does not apply to subclasses + + Meta("A", (), {}) # OK, uses the three-argument form + +This special-casing applies only to the :meth:`!__new__` and :meth:`!__init__` +methods inherited from :class:`type`. If a metaclass defines its own :meth:`!__new__` +method that accepts a single argument, calls to it should be evaluated +using the rules for regular constructor calls described earlier in this +chapter. + +The evaluated return type of the three-argument form is an instance of the +metaclass being called, consistent with the return type definition of +:meth:`!type.__new__`. Type checkers may infer a more precise type for the returned +class object, for example, one equivalent to a class defined by a :keyword:`class` +statement with the given name, base classes, and namespace. + + +Class Statements +---------------- + +When a :keyword:`class` statement is executed, the runtime performs the following +steps to create the new class object (see :ref:`python:metaclasses`): + +1. The metaclass is determined. If a ``metaclass`` keyword argument is present + in the class statement's argument list, it is used as a candidate; + otherwise, :class:`type` is. The most derived metaclass among the candidate and + the metaclasses of all base classes is selected. If no candidate is a + (non-strict) subclass of all of the others, a :exc:`TypeError` is raised + (see :ref:`py315:metaclass-determination`). +2. The class namespace is prepared. If the metaclass has a :attr:`!__prepare__` + attribute, it is called as ``Meta.__prepare__(name, bases, **kwds)``, and + its result is used as the namespace object. +3. The class body is executed within this namespace. +4. The metaclass is called as ``Meta(name, bases, namespace, **kwds)``, where + ``kwds`` consists of the keyword arguments that appear in the class + statement's argument list, excluding ``metaclass`` itself. + +Type checkers may report an error for a class statement whose base classes +have incompatible metaclasses. + +Type checkers should validate keyword arguments in a class statement's +argument list (other than ``metaclass``) by evaluating the implied metaclass +call using the constructor call rules described in :ref:`constructor-calls`. + + :: + + class Meta(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + class MyClass1(metaclass=Meta, key=3): # OK + pass + + class MyClass2(metaclass=Meta, key=""): # Type error: wrong type for "key" + pass + + class MyClass3(metaclass=Meta): # Type error: missing argument "key" + pass + + Meta("MyClass4", (), {}, key=3) # OK + Meta("MyClass5", (), {}, key="") # Type error: wrong type for "key" + +Keyword arguments in a direct metaclass call (such as the last two calls in the +example above) require no special handling: they are validated as part of +evaluating the call using the standard constructor call rules. + +Type checkers should honor the evaluated return type of the implied metaclass call, +even if the evaluated type isn't a class: + + :: + + class Meta(type): + def __new__(mcls, *args: object, **kwargs: object) -> int: + return 1 + + class MyClass6(metaclass=Meta): + pass + + assert_type(MyClass6, int) + +Type checkers may validate the implied call to :attr:`!__prepare__`: + + :: + + class Meta(type): + @classmethod + def __prepare__(mcls, name: str, bases: tuple[type, ...]): # No **kwds + return {} + + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + # The 'key' argument may result in a type checker error: + class MyClass7(metaclass=Meta, key=3): + pass + +The ``metaclass`` argument can also be an arbitrary callable that is not a subclass +of :class:`type`. Support for this pattern is currently unspecified. + + +The ``__init_subclass__()`` Method +---------------------------------- + +:meth:`!type.__new__` invokes the :meth:`~object.__init_subclass__` +method of the parent class (the class that follows the newly created class in +its :term:`python:method resolution order`) passing the newly created class as +``cls`` along with the keyword arguments supplied to the metaclass constructor +(see :ref:`python:class-customization`). + +:meth:`~object.__init_subclass__` is implicitly a class method: it is converted to +a :class:`classmethod` even when it is not explicitly decorated as one, and it is +not called for the class that defines it, only for its subclasses. Type checkers +should treat it as a classmethod if it isn't explicitly defined as one. + +If the metaclass of the class being defined does not define its own +:meth:`!__new__` method (including when no explicit metaclass +is specified), type checkers should validate the keyword arguments in a +class statement's argument list against the :meth:`~object.__init_subclass__` method of +the parent class. + + :: + + class Base: + def __init_subclass__(cls, *, flag: bool = False) -> None: + super().__init_subclass__() + + class MyClass1(Base, flag=True): # OK + pass + + class MyClass2(Base, flag=""): # Type error: wrong type for "flag" + pass + + class MyClass3(Base, other=1): # Type error: Base.__init_subclass__() got an unexpected keyword argument 'other' + pass + + class MyClass4(other=1): # Type error: MyClass4.__init_subclass__() takes no keyword arguments + pass + +A metaclass :meth:`!__init__` method has no effect on this rule: when the +metaclass does not define its own :meth:`!__new__` method, :meth:`!type.__new__` +still forwards the keyword arguments to :meth:`~object.__init_subclass__`, so +the keyword arguments should satisfy both the metaclass :meth:`!__init__` +method (as part of validating the implied metaclass call) and the +:meth:`~object.__init_subclass__` method of the parent class. + + :: + + class MetaInit(type): + def __init__( + cls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ) -> None: + super().__init__(name, bases, namespace) + + # Type error: "key" is accepted by MetaInit.__init__(), but type.__new__() + # forwards it to Base.__init_subclass__(), which does not accept it: + class MyClass5(Base, metaclass=MetaInit, key=1): + pass + +The same forwarding occurs when the metaclass is called directly: +``type("D", (Base,), {}, flag=True)`` passes ``flag`` to +:meth:`!Base.__init_subclass__`. Type checkers may validate keyword +arguments in such calls against the :meth:`~object.__init_subclass__` method of +the parent class when the base classes can be statically determined. + +If the metaclass defines its own :meth:`!__new__` method that accepts keyword +arguments only through a ``**kwargs`` parameter, whether these arguments are +forwarded to :meth:`!type.__new__` (and from there to +:meth:`~object.__init_subclass__`) cannot generally be determined statically. +In this situation, type checkers may additionally validate the keyword +arguments against the :meth:`~object.__init_subclass__` method of the parent +class.