From e7d5a529dbe15a03c18ad0ed12bdf4fa843ecf7c Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 28 Jan 2022 12:09:31 +0200 Subject: [PATCH 01/12] stubtest: error if a class should be decorated with @final --- mypy/stubtest.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 4ca088d8aa3e..82829fde9549 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -254,6 +254,15 @@ def verify_typeinfo( yield Error(object_path, "is not a type", stub, runtime, stub_desc=repr(stub)) return + try: + class SubClass(runtime): + pass + except Exception: + if not stub.is_final: + yield Error( + object_path, "cannot be subclassed at runtime, but isn't marked with @final in the stub", + stub, runtime, stub_desc=repr(stub)) + # Check everything already defined in the stub to_check = set(stub.names) # There's a reasonable case to be made that we should always check all dunders, but it's From ff1feea33d5335533ad547e992d95bd5db145c07 Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 28 Jan 2022 12:21:23 +0200 Subject: [PATCH 02/12] lint --- mypy/stubtest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 82829fde9549..6f94ab44864c 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -260,8 +260,12 @@ class SubClass(runtime): except Exception: if not stub.is_final: yield Error( - object_path, "cannot be subclassed at runtime, but isn't marked with @final in the stub", - stub, runtime, stub_desc=repr(stub)) + object_path, + "cannot be subclassed at runtime, but isn't marked with @final in the stub", + stub, + runtime, + stub_desc=repr(stub), + ) # Check everything already defined in the stub to_check = set(stub.names) From a77c149d5ee8a6ca4f979d2eca61492c60a23d4b Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 28 Jan 2022 12:21:59 +0200 Subject: [PATCH 03/12] type ignore --- mypy/stubtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 6f94ab44864c..eae5238f8b54 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -255,7 +255,7 @@ def verify_typeinfo( return try: - class SubClass(runtime): + class SubClass(runtime): # type: ignore pass except Exception: if not stub.is_final: From c9e07a6156f79a1f926d3318e60edeac038391ad Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 28 Jan 2022 12:24:18 +0200 Subject: [PATCH 04/12] enum check --- mypy/stubtest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index eae5238f8b54..ccbc3880bd5a 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -258,7 +258,8 @@ def verify_typeinfo( class SubClass(runtime): # type: ignore pass except Exception: - if not stub.is_final: + # Enum classes are implicitly @final + if not stub.is_final and not issubclass(runtime, enum.Enum): yield Error( object_path, "cannot be subclassed at runtime, but isn't marked with @final in the stub", From 3ab85377f030eb781883151c223943c5cdb53741 Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 28 Jan 2022 13:03:30 +0200 Subject: [PATCH 05/12] add test --- mypy/test/teststubtest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 30ed953d7390..7cc29976709f 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -702,6 +702,17 @@ def test_special_dunders(self) -> Iterator[Case]: error=None, ) + def test_not_subclassable(self) -> None: + output = run_stubtest( + "class CantBeSubclassed:\n pass", + "class CantBeSubclassed:\n def __init_subclass__(cls): raise RuntimeError('nope')", + [], + ) + assert ( + "CantBeSubclassed cannot be subclassed at runtime," + " but isn't marked with @final in the stub" + ) in output + @collect_cases def test_name_mangling(self) -> Iterator[Case]: yield Case( From dc3369c2e3308a5989630bdefc33d855d3ac6773 Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 28 Jan 2022 13:06:09 +0200 Subject: [PATCH 06/12] Make the __init_subclass__ test pass --- mypy/stubtest.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index ccbc3880bd5a..bf4c95a776cc 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -258,8 +258,15 @@ def verify_typeinfo( class SubClass(runtime): # type: ignore pass except Exception: - # Enum classes are implicitly @final - if not stub.is_final and not issubclass(runtime, enum.Enum): + if ( + not stub.is_final + # Enum classes are implicitly @final + and not issubclass(runtime, enum.Enum) + # If __init_subclass__ is defined in the class itself (but not in a + # base class), it clearly is meant to be subclass, but you + # apparently have to do something special to create a subclass without errors + and "__init_subclass__" not in runtime.__dict__ + ): yield Error( object_path, "cannot be subclassed at runtime, but isn't marked with @final in the stub", From f2ca1f17f2a8dc19e6fc9cdd70890276221105ad Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 28 Jan 2022 13:11:41 +0200 Subject: [PATCH 07/12] Update mypy/stubtest.py Co-authored-by: Alex Waygood --- mypy/stubtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index bf4c95a776cc..031a0cff5cfe 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -263,7 +263,7 @@ class SubClass(runtime): # type: ignore # Enum classes are implicitly @final and not issubclass(runtime, enum.Enum) # If __init_subclass__ is defined in the class itself (but not in a - # base class), it clearly is meant to be subclass, but you + # base class), it clearly is meant to be subclassable, but you # apparently have to do something special to create a subclass without errors and "__init_subclass__" not in runtime.__dict__ ): From cee8a4e551ebda142a58ea8d67fbef9e6d03671b Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 28 Jan 2022 13:59:27 +0200 Subject: [PATCH 08/12] Revert "Update mypy/stubtest.py" This reverts commit f2ca1f17f2a8dc19e6fc9cdd70890276221105ad. --- mypy/stubtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 031a0cff5cfe..bf4c95a776cc 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -263,7 +263,7 @@ class SubClass(runtime): # type: ignore # Enum classes are implicitly @final and not issubclass(runtime, enum.Enum) # If __init_subclass__ is defined in the class itself (but not in a - # base class), it clearly is meant to be subclassable, but you + # base class), it clearly is meant to be subclass, but you # apparently have to do something special to create a subclass without errors and "__init_subclass__" not in runtime.__dict__ ): From 0f7ebfd26e3d489441153a07c468bb4dfb72bf3c Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 28 Jan 2022 13:52:52 +0200 Subject: [PATCH 09/12] Revert "Make the __init_subclass__ test pass" This reverts commit dc3369c2e3308a5989630bdefc33d855d3ac6773. --- mypy/stubtest.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index bf4c95a776cc..ccbc3880bd5a 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -258,15 +258,8 @@ def verify_typeinfo( class SubClass(runtime): # type: ignore pass except Exception: - if ( - not stub.is_final - # Enum classes are implicitly @final - and not issubclass(runtime, enum.Enum) - # If __init_subclass__ is defined in the class itself (but not in a - # base class), it clearly is meant to be subclass, but you - # apparently have to do something special to create a subclass without errors - and "__init_subclass__" not in runtime.__dict__ - ): + # Enum classes are implicitly @final + if not stub.is_final and not issubclass(runtime, enum.Enum): yield Error( object_path, "cannot be subclassed at runtime, but isn't marked with @final in the stub", From bf696b1f8accfa8eb346c7c0beb65f31a71fdf62 Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 28 Jan 2022 13:55:14 +0200 Subject: [PATCH 10/12] Maybe the tests will now pass... --- mypy/test/teststubtest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 7cc29976709f..16cfa916b9f5 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -691,8 +691,8 @@ def test_special_dunders(self) -> Iterator[Case]: ) if sys.version_info >= (3, 6): yield Case( - stub="class C:\n def __init_subclass__(cls, e: int, **kwargs: int) -> None: ...", - runtime="class C:\n def __init_subclass__(cls, e, **kwargs): pass", + stub="class C:\n def __init_subclass__(cls, e: int = ..., **kwargs: int) -> None: ...", + runtime="class C:\n def __init_subclass__(cls, e=1, **kwargs): pass", error=None, ) if sys.version_info >= (3, 9): @@ -704,7 +704,7 @@ def test_special_dunders(self) -> Iterator[Case]: def test_not_subclassable(self) -> None: output = run_stubtest( - "class CantBeSubclassed:\n pass", + "class CantBeSubclassed:\n def __init_subclass__(cls): pass", "class CantBeSubclassed:\n def __init_subclass__(cls): raise RuntimeError('nope')", [], ) From c7416e87b5792767694eafb399b118fa50863f6a Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 28 Jan 2022 14:04:24 +0200 Subject: [PATCH 11/12] make a mess with multiline strings to satisfy flake8 --- mypy/test/teststubtest.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 16cfa916b9f5..352a6ff4c60b 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -691,7 +691,12 @@ def test_special_dunders(self) -> Iterator[Case]: ) if sys.version_info >= (3, 6): yield Case( - stub="class C:\n def __init_subclass__(cls, e: int = ..., **kwargs: int) -> None: ...", + stub=( + "class C:\n" + " def __init_subclass__(\n" + " cls, e: int = ..., **kwargs: int\n" + " ) -> None: ...\n" + ), runtime="class C:\n def __init_subclass__(cls, e=1, **kwargs): pass", error=None, ) From 53c72d6c76ff30867ad5b80c83d45da09b47d512 Mon Sep 17 00:00:00 2001 From: Akuli Date: Sat, 29 Jan 2022 11:47:36 +0200 Subject: [PATCH 12/12] do not emit error if subclassing fails with other than TypeError --- mypy/stubtest.py | 6 +++++- mypy/test/teststubtest.py | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index ccbc3880bd5a..df36935a801f 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -257,7 +257,7 @@ def verify_typeinfo( try: class SubClass(runtime): # type: ignore pass - except Exception: + except TypeError: # Enum classes are implicitly @final if not stub.is_final and not issubclass(runtime, enum.Enum): yield Error( @@ -267,6 +267,10 @@ class SubClass(runtime): # type: ignore runtime, stub_desc=repr(stub), ) + except Exception: + # The class probably wants its subclasses to do something special. + # Examples: ctypes.Array, ctypes._SimpleCData + pass # Check everything already defined in the stub to_check = set(stub.names) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 352a6ff4c60b..2852299548ed 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -709,14 +709,25 @@ def test_special_dunders(self) -> Iterator[Case]: def test_not_subclassable(self) -> None: output = run_stubtest( - "class CantBeSubclassed:\n def __init_subclass__(cls): pass", - "class CantBeSubclassed:\n def __init_subclass__(cls): raise RuntimeError('nope')", - [], + stub=( + "class CanBeSubclassed: ...\n" + "class CanNotBeSubclassed:\n" + " def __init_subclass__(cls) -> None: ...\n" + ), + runtime=( + "class CanNotBeSubclassed:\n" + " def __init_subclass__(cls): raise TypeError('nope')\n" + # ctypes.Array can be subclassed, but subclasses must define a few + # special attributes, e.g. _length_ + "from ctypes import Array as CanBeSubclassed\n" + ), + options=[], ) assert ( - "CantBeSubclassed cannot be subclassed at runtime," + "CanNotBeSubclassed cannot be subclassed at runtime," " but isn't marked with @final in the stub" ) in output + assert "CanBeSubclassed cannot be subclassed" not in output @collect_cases def test_name_mangling(self) -> Iterator[Case]: