From e16dee59f938a0cc2b9d3382c6f6c1e27cbbc72f Mon Sep 17 00:00:00 2001 From: Michael H Date: Sun, 31 Mar 2024 18:13:37 -0400 Subject: [PATCH 1/8] spec: clarify the mro linearization of Any See discussion here: https://discuss.python.org/t/take-2-rules-for-subclassing-any/47981 - This matches the current behavior of mypy and pyright, which support subclassing of Any. To summarize the rationales possible for this in terms of type theory, this would either be - The lower known bound of the type and type checkers only allowing the lower known bound - Type checkers picking a definition of compatibility of Any based on LSP substitution - Or type checkers decide to prefer known information in the face of unknown potential diamond patterns treating them as rare. Given the relatively low priority on this outside of it being one of the only unresolved prerequisite questions for the intersection draft, I don't think we need to set in stone a reason at this point in time as long as the behavior intended is clear and agreed upon. If you'd prefer a reason to be chosen, the lower bound is most flexible into the future, see https://discuss.python.org/t/take-2-rules-for-subclassing-any/47981/3 and https://discuss.python.org/t/take-2-rules-for-subclassing-any/47981/7 for discussion of this approach. --- docs/spec/special-types.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/spec/special-types.rst b/docs/spec/special-types.rst index 76ac2cdec..e30e322fb 100644 --- a/docs/spec/special-types.rst +++ b/docs/spec/special-types.rst @@ -47,7 +47,8 @@ to ``tuple[Any, ...]``. As well, a bare ``Any`` can also be used as a base class. This can be useful for avoiding type checker errors with classes that can duck type anywhere or -are highly dynamic. +are highly dynamic. When ``Any`` is present in the bases of a type, +it should be considered only after all other known types in the MRO. .. _`none`: From f38876b1ea3f522301dffd977a13f1905e45e79c Mon Sep 17 00:00:00 2001 From: Michael H Date: Sun, 31 Mar 2024 19:46:05 -0400 Subject: [PATCH 2/8] add basic conformance tests --- conformance/tests/specialtypes_any.py | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/conformance/tests/specialtypes_any.py b/conformance/tests/specialtypes_any.py index 420920563..07ad70cbd 100644 --- a/conformance/tests/specialtypes_any.py +++ b/conformance/tests/specialtypes_any.py @@ -87,4 +87,35 @@ def method1(self) -> int: assert_type(a.method2(), Any) assert_type(ClassA.method3(), Any) +# > When ``Any`` is present in the bases of a type, +# > it should be considered only after all other known types in the MRO. + +class ClassKnown: + + def __init__(self): + self.attr1: str = "" + + def method1(self) -> str: + return "" + +class AnyFirst(Any, ClassKnown): + + def method2(self) -> str: + return "" + +class AnyLast(ClassKnown, Any): + def method2(self) -> str: + return "" + + +af = AnyFirst() +assert_type(af.method1(), str) +assert_type(af.method2(), str) +assert_type(af.non_exist_method(), Any) +assert_type(af.non_exist_attr, Any) +al = AnyLast() +assert_type(al.method1(), str) +assert_type(al.method2(), str) +assert_type(al.non_exist_method(), Any) +assert_type(al.non_exist_attr, Any) From 8a1633f02255be412e459cacad21d4ad9ddefae3 Mon Sep 17 00:00:00 2001 From: Michael H Date: Sun, 31 Mar 2024 19:49:48 -0400 Subject: [PATCH 3/8] missing attr check --- conformance/tests/specialtypes_any.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conformance/tests/specialtypes_any.py b/conformance/tests/specialtypes_any.py index 07ad70cbd..a74830b7a 100644 --- a/conformance/tests/specialtypes_any.py +++ b/conformance/tests/specialtypes_any.py @@ -111,11 +111,13 @@ def method2(self) -> str: af = AnyFirst() assert_type(af.method1(), str) assert_type(af.method2(), str) +assert_type(af.attr1, str) assert_type(af.non_exist_method(), Any) assert_type(af.non_exist_attr, Any) al = AnyLast() assert_type(al.method1(), str) assert_type(al.method2(), str) +assert_type(al.attr1, str) assert_type(al.non_exist_method(), Any) assert_type(al.non_exist_attr, Any) From 911c02468792abe382af1d10f37886ab308ecc02 Mon Sep 17 00:00:00 2001 From: Michael H Date: Mon, 1 Apr 2024 05:55:42 -0400 Subject: [PATCH 4/8] Add more test cases around Any --- conformance/tests/specialtypes_any.py | 46 ++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/conformance/tests/specialtypes_any.py b/conformance/tests/specialtypes_any.py index a74830b7a..06a8080b8 100644 --- a/conformance/tests/specialtypes_any.py +++ b/conformance/tests/specialtypes_any.py @@ -6,6 +6,7 @@ # > Every type is consistent with Any. +from collections.abc import Iterator from typing import Any, Callable, assert_type @@ -92,6 +93,11 @@ def method1(self) -> int: class ClassKnown: + classvar1 = "" + + def __iter__(self) -> Iterator[str]: + yield from self.attr1 + def __init__(self): self.attr1: str = "" @@ -107,6 +113,18 @@ class AnyLast(ClassKnown, Any): def method2(self) -> str: return "" +class GetattrKnown(ClassKnown): + def __getattr__(self, name: str) -> int: + return 1 + +class AnyFirstGetAttr(Any, GetattrKnown): + def method2(self) -> str: + return "" + +class AnyLastGetAttr(GetattrKnown, Any): + def method2(self) -> str: + return "" + af = AnyFirst() assert_type(af.method1(), str) @@ -114,10 +132,36 @@ def method2(self) -> str: assert_type(af.attr1, str) assert_type(af.non_exist_method(), Any) assert_type(af.non_exist_attr, Any) +assert_type(af.classvar1, str) +assert_type(AnyFirst.classvar1, str) +assert_type(iter(af()), Iterator[str]) + al = AnyLast() assert_type(al.method1(), str) assert_type(al.method2(), str) assert_type(al.attr1, str) assert_type(al.non_exist_method(), Any) assert_type(al.non_exist_attr, Any) - +assert_type(al.classvar1, str) +assert_type(AnyLast.classvar1, str) +assert_type(iter(al()), Iterator[str]) + +af_getattr = AnyFirstGetAttr() + +assert_type(af_getattr.method1(), str) +assert_type(af_getattr.method2(), str) +assert_type(af_getattr.attr1, str) +assert_type(af_getattr.triggers_getattr, int) +assert_type(af_getattr.classvar1, str) +assert_type(AnyFirstGetAttr.classvar1, str) +assert_type(iter(af_getattr()), Iterator[str]) + +al_getattr = AnyLastGetAttr() + +assert_type(al_getattr.method1(), str) +assert_type(al_getattr.method2(), str) +assert_type(al_getattr.attr1, str) +assert_type(al_getattr.triggers_getattr, int) +assert_type(al_getattr.classvar1, str) +assert_type(AnyLastGetAttr.classvar1, str) +assert_type(iter(al_getattr()), Iterator[str]) \ No newline at end of file From 0b9461240302d2fc19cc50172992dc33643f670e Mon Sep 17 00:00:00 2001 From: Michael H Date: Wed, 3 Apr 2024 17:18:20 -0400 Subject: [PATCH 5/8] Address Feedback from carljm --- conformance/tests/specialtypes_any.py | 21 ++++++++++++++++++++- docs/spec/special-types.rst | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/conformance/tests/specialtypes_any.py b/conformance/tests/specialtypes_any.py index 06a8080b8..fe29417ee 100644 --- a/conformance/tests/specialtypes_any.py +++ b/conformance/tests/specialtypes_any.py @@ -125,6 +125,15 @@ class AnyLastGetAttr(GetattrKnown, Any): def method2(self) -> str: return "" +class AnySub(Any): + ... + +# primarily included to demonstrate intent that this is for the full MRO +class AnySubFirst(AnySub, ClassKnown): + def method2(self): + ... + + af = AnyFirst() assert_type(af.method1(), str) @@ -163,5 +172,15 @@ def method2(self) -> str: assert_type(al_getattr.attr1, str) assert_type(al_getattr.triggers_getattr, int) assert_type(al_getattr.classvar1, str) +assert_type(AnySubFirst.classvar1, str) +assert_type(iter(al_getattr()), Iterator[str]) + +full_mro_checked = AnySubFirst() + +assert_type(full_mro_checked.method1(), str) +assert_type(full_mro_checked.method2(), str) +assert_type(full_mro_checked.attr1, str) +assert_type(full_mro_checked.triggers_getattr, int) +assert_type(full_mro_checked.classvar1, str) assert_type(AnyLastGetAttr.classvar1, str) -assert_type(iter(al_getattr()), Iterator[str]) \ No newline at end of file +assert_type(iter(full_mro_checked()), Iterator[str]) \ No newline at end of file diff --git a/docs/spec/special-types.rst b/docs/spec/special-types.rst index e30e322fb..f46a9fd64 100644 --- a/docs/spec/special-types.rst +++ b/docs/spec/special-types.rst @@ -47,7 +47,7 @@ to ``tuple[Any, ...]``. As well, a bare ``Any`` can also be used as a base class. This can be useful for avoiding type checker errors with classes that can duck type anywhere or -are highly dynamic. When ``Any`` is present in the bases of a type, +are highly dynamic. When ``Any`` is present in the MRO of a type, it should be considered only after all other known types in the MRO. .. _`none`: From 09ea47b92c8c17f6d2125f87ed451a9762342d61 Mon Sep 17 00:00:00 2001 From: Michael H Date: Wed, 3 Apr 2024 17:34:03 -0400 Subject: [PATCH 6/8] Update conformance/tests/specialtypes_any.py Co-authored-by: Carl Meyer --- conformance/tests/specialtypes_any.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conformance/tests/specialtypes_any.py b/conformance/tests/specialtypes_any.py index fe29417ee..1e246e539 100644 --- a/conformance/tests/specialtypes_any.py +++ b/conformance/tests/specialtypes_any.py @@ -143,7 +143,7 @@ def method2(self): assert_type(af.non_exist_attr, Any) assert_type(af.classvar1, str) assert_type(AnyFirst.classvar1, str) -assert_type(iter(af()), Iterator[str]) +assert_type(iter(af), Iterator[str]) al = AnyLast() assert_type(al.method1(), str) From 9db07eb7abf2eb4dc8f5af1db0b6f01c4d4b220c Mon Sep 17 00:00:00 2001 From: Michael H Date: Wed, 3 Apr 2024 17:37:35 -0400 Subject: [PATCH 7/8] Fixing issues with iter examples, + swap a placement --- conformance/tests/specialtypes_any.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/conformance/tests/specialtypes_any.py b/conformance/tests/specialtypes_any.py index 1e246e539..5ececb9bc 100644 --- a/conformance/tests/specialtypes_any.py +++ b/conformance/tests/specialtypes_any.py @@ -153,7 +153,7 @@ def method2(self): assert_type(al.non_exist_attr, Any) assert_type(al.classvar1, str) assert_type(AnyLast.classvar1, str) -assert_type(iter(al()), Iterator[str]) +assert_type(iter(al), Iterator[str]) af_getattr = AnyFirstGetAttr() @@ -163,7 +163,7 @@ def method2(self): assert_type(af_getattr.triggers_getattr, int) assert_type(af_getattr.classvar1, str) assert_type(AnyFirstGetAttr.classvar1, str) -assert_type(iter(af_getattr()), Iterator[str]) +assert_type(iter(af_getattr), Iterator[str]) al_getattr = AnyLastGetAttr() @@ -172,8 +172,8 @@ def method2(self): assert_type(al_getattr.attr1, str) assert_type(al_getattr.triggers_getattr, int) assert_type(al_getattr.classvar1, str) -assert_type(AnySubFirst.classvar1, str) -assert_type(iter(al_getattr()), Iterator[str]) +assert_type(AnyLastGetAttr.classvar1, str) +assert_type(iter(al_getattr), Iterator[str]) full_mro_checked = AnySubFirst() @@ -182,5 +182,5 @@ def method2(self): assert_type(full_mro_checked.attr1, str) assert_type(full_mro_checked.triggers_getattr, int) assert_type(full_mro_checked.classvar1, str) -assert_type(AnyLastGetAttr.classvar1, str) -assert_type(iter(full_mro_checked()), Iterator[str]) \ No newline at end of file +assert_type(AnySubFirst.classvar1, str) +assert_type(iter(full_mro_checked), Iterator[str]) \ No newline at end of file From 7f011ca3bee03474bef7b62791e9c2ca4668f51f Mon Sep 17 00:00:00 2001 From: Michael H Date: Wed, 3 Apr 2024 18:54:57 -0400 Subject: [PATCH 8/8] Add notes on some of the examples, fix an incongruence --- conformance/tests/specialtypes_any.py | 31 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/conformance/tests/specialtypes_any.py b/conformance/tests/specialtypes_any.py index 5ececb9bc..5c7286adb 100644 --- a/conformance/tests/specialtypes_any.py +++ b/conformance/tests/specialtypes_any.py @@ -130,7 +130,7 @@ class AnySub(Any): # primarily included to demonstrate intent that this is for the full MRO class AnySubFirst(AnySub, ClassKnown): - def method2(self): + def method2(self) -> str: ... @@ -155,6 +155,23 @@ def method2(self): assert_type(AnyLast.classvar1, str) assert_type(iter(al), Iterator[str]) + +# this example has Any deeper in the inheritence than the bases, specifically as +# an ancestor of the first base, with other known methods and attributes in the second. +full_mro_checked = AnySubFirst() + +assert_type(full_mro_checked.method1(), str) +assert_type(full_mro_checked.method2(), str) +assert_type(full_mro_checked.attr1, str) +assert_type(al.non_exist_method(), Any) +assert_type(al.non_exist_attr, Any) +assert_type(full_mro_checked.classvar1, str) +assert_type(AnySubFirst.classvar1, str) +assert_type(iter(full_mro_checked), Iterator[str]) + +# Note The next two examples check different bases, one of which provides a `__getattr__` +# ensuring the behavior is the same for unaffected cases + af_getattr = AnyFirstGetAttr() assert_type(af_getattr.method1(), str) @@ -173,14 +190,4 @@ def method2(self): assert_type(al_getattr.triggers_getattr, int) assert_type(al_getattr.classvar1, str) assert_type(AnyLastGetAttr.classvar1, str) -assert_type(iter(al_getattr), Iterator[str]) - -full_mro_checked = AnySubFirst() - -assert_type(full_mro_checked.method1(), str) -assert_type(full_mro_checked.method2(), str) -assert_type(full_mro_checked.attr1, str) -assert_type(full_mro_checked.triggers_getattr, int) -assert_type(full_mro_checked.classvar1, str) -assert_type(AnySubFirst.classvar1, str) -assert_type(iter(full_mro_checked), Iterator[str]) \ No newline at end of file +assert_type(iter(al_getattr), Iterator[str]) \ No newline at end of file