From a3534925d96fcc45ba243a7b100ddd421703d41e Mon Sep 17 00:00:00 2001 From: Nate Soares Date: Wed, 15 Mar 2017 10:07:13 -0700 Subject: [PATCH 1/8] fix inspect.isabstract() during __init_subclass__ At the time when an abstract base class' __init_subclass__ runs, ABCMeta.__new__ has not yet finished running, so in the presence of __init_subclass__, inspect.isabstract() can no longer depend only on TPFLAGS_IS_ABSTRACT. --- Lib/inspect.py | 20 +++++++++++++++++++- Lib/test/test_inspect.py | 21 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 4d56ef5d41b47f..e4a6ba41683193 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -31,6 +31,7 @@ __author__ = ('Ka-Ping Yee ', 'Yury Selivanov ') +import abc import ast import dis import collections.abc @@ -285,7 +286,24 @@ def isroutine(object): def isabstract(object): """Return true if the object is an abstract base class (ABC).""" - return bool(isinstance(object, type) and object.__flags__ & TPFLAGS_IS_ABSTRACT) + if isinstance(object, type): + if object.__flags__ & TPFLAGS_IS_ABSTRACT: + return True + elif (issubclass(type(object), abc.ABCMeta) + and not hasattr(object, '__abstractmethods__')): + # We're likely in the __init_subclass__ of an ABC. ABCMeta.__new__ + # hasn't finished running yet. We have to search for abstractmethods + # by hand. Code copied from ABCMeta.__new__. + abstracts = {name + for name, value in object.__dict__.items() + if getattr(value, "__isabstractmethod__", False)} + for base in object.__bases__: + for name in getattr(base, "__abstractmethods__", frozenset()): + value = getattr(object, name, None) + if getattr(value, "__isabstractmethod__", False): + abstracts.add(name) + return bool(abstracts) + return False def getmembers(object, predicate=None): """Return all members of an object as (name, value) pairs sorted by name. diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 9d9fedc21e5795..af1053a12e3ce1 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -229,6 +229,27 @@ def foo(self): self.assertFalse(inspect.isabstract(int)) self.assertFalse(inspect.isabstract(5)) + def test_isabstract_during_init_subclass(self): + from abc import ABCMeta, abstractmethod + + isabstract_checks = [] + + class AbstractChecker(metaclass=ABCMeta): + def __init_subclass__(cls): + isabstract_checks.append(inspect.isabstract(cls)) + + class AbstractClassExample(AbstractChecker): + + @abstractmethod + def foo(self): + pass + + class ClassExample(AbstractClassExample): + def foo(self): + pass + + self.assertEqual(isabstract_checks, [True, False]) + class TestInterpreterStack(IsTestBase): def __init__(self, *args, **kwargs): From ebbbf583fc42d4ec382e66d8cdbb1e4f87701e73 Mon Sep 17 00:00:00 2001 From: Nate Soares Date: Wed, 15 Mar 2017 10:11:45 -0700 Subject: [PATCH 2/8] Added NEWS item --- Misc/NEWS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Misc/NEWS b/Misc/NEWS index b7990c62e4f744..15310dc119ea3a 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -764,6 +764,9 @@ Library - Issue #29581: ABCMeta.__new__ now accepts **kwargs, allowing abstract base classes to use keyword parameters in __init_subclass__. Patch by Nate Soares. +- Issue #29822: inspect.isabstract() now works during __init_subclass__. Patch + by Nate Soares. + Windows ------- From 4ba6c4d10ca0994875b862e23f4166e40faa10b9 Mon Sep 17 00:00:00 2001 From: Nate Soares Date: Fri, 17 Mar 2017 23:52:16 -0700 Subject: [PATCH 3/8] integrate review comments - return earlier to decrease indentation levels - return immediately upon finding an abstractmethod, instead of collecting the whole set --- Lib/inspect.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index e4a6ba41683193..be095c22953b6f 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -286,23 +286,26 @@ def isroutine(object): def isabstract(object): """Return true if the object is an abstract base class (ABC).""" - if isinstance(object, type): - if object.__flags__ & TPFLAGS_IS_ABSTRACT: + if not isinstance(object, type): + return False + if object.__flags__ & TPFLAGS_IS_ABSTRACT: + return True + if not issubclass(type(object), abc.ABCMeta): + return False + if hasattr(object, '__abstractmethods__'): + # It looks like ABCMeta.__new__ has finished running; + # TPFLAGS_IS_ABSTRACT should have been accurate. + return False + # It looks like ABCMeta.__new__ has not finished running yet; we're + # probably in __init_subcalss_. We'll look for abstractmethods manually. + for name, value in object.__dict__.items(): + if getattr(value, "__isabstractmethod__", False): return True - elif (issubclass(type(object), abc.ABCMeta) - and not hasattr(object, '__abstractmethods__')): - # We're likely in the __init_subclass__ of an ABC. ABCMeta.__new__ - # hasn't finished running yet. We have to search for abstractmethods - # by hand. Code copied from ABCMeta.__new__. - abstracts = {name - for name, value in object.__dict__.items() - if getattr(value, "__isabstractmethod__", False)} - for base in object.__bases__: - for name in getattr(base, "__abstractmethods__", frozenset()): - value = getattr(object, name, None) - if getattr(value, "__isabstractmethod__", False): - abstracts.add(name) - return bool(abstracts) + for base in object.__bases__: + for name in getattr(base, "__abstractmethods__", frozenset()): + value = getattr(object, name, None) + if getattr(value, "__isabstractmethod__", False): + return True return False def getmembers(object, predicate=None): From b92b49630fe1c39ba5ca3e8ee37a66cf02eef4be Mon Sep 17 00:00:00 2001 From: Nate Soares Date: Sat, 18 Mar 2017 08:44:35 -0700 Subject: [PATCH 4/8] Integrated more review comments. - Added a new test. - Cleaned up some code style here and there --- Lib/inspect.py | 2 +- Lib/test/test_inspect.py | 15 +++++++++------ Misc/NEWS | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index be095c22953b6f..fee5f8d016454a 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -302,7 +302,7 @@ def isabstract(object): if getattr(value, "__isabstractmethod__", False): return True for base in object.__bases__: - for name in getattr(base, "__abstractmethods__", frozenset()): + for name in getattr(base, "__abstractmethods__", ()): value = getattr(object, name, None) if getattr(value, "__isabstractmethod__", False): return True diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index af1053a12e3ce1..5e1acda1c20951 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -231,25 +231,28 @@ def foo(self): def test_isabstract_during_init_subclass(self): from abc import ABCMeta, abstractmethod - isabstract_checks = [] - class AbstractChecker(metaclass=ABCMeta): def __init_subclass__(cls): isabstract_checks.append(inspect.isabstract(cls)) - class AbstractClassExample(AbstractChecker): - @abstractmethod def foo(self): pass - class ClassExample(AbstractClassExample): def foo(self): pass - self.assertEqual(isabstract_checks, [True, False]) + isabstract_checks.clear() + class AbstractChild(AbstractClassExample): + pass + class AbstractGrandchild(AbstractClassExample): + pass + class ConcreteGrandchild(ClassExample): + pass + self.assertEqual(isabstract_checks, [True, True, False]) + class TestInterpreterStack(IsTestBase): def __init__(self, *args, **kwargs): diff --git a/Misc/NEWS b/Misc/NEWS index 15310dc119ea3a..b49bb557f2cc49 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -281,6 +281,9 @@ Extension Modules Library ------- +- bpo-29822: inspect.isabstract() now works during __init_subclass__. Patch + by Nate Soares. + - bpo-29800: Fix crashes in partial.__repr__ if the keys of partial.keywords are not strings. Patch by Michael Seifert. @@ -764,9 +767,6 @@ Library - Issue #29581: ABCMeta.__new__ now accepts **kwargs, allowing abstract base classes to use keyword parameters in __init_subclass__. Patch by Nate Soares. -- Issue #29822: inspect.isabstract() now works during __init_subclass__. Patch - by Nate Soares. - Windows ------- From abf6861f82550e76ec0f52a94cceb2b53aba999c Mon Sep 17 00:00:00 2001 From: Nate Soares Date: Sat, 18 Mar 2017 10:41:26 -0700 Subject: [PATCH 5/8] fixed typo in the tests --- Lib/test/test_inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 5e1acda1c20951..a5a151974bf855 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -247,7 +247,7 @@ def foo(self): isabstract_checks.clear() class AbstractChild(AbstractClassExample): pass - class AbstractGrandchild(AbstractClassExample): + class AbstractGrandchild(AbstractChild): pass class ConcreteGrandchild(ClassExample): pass From 35828953fea420e1728dfcdee4036771e6a276a6 Mon Sep 17 00:00:00 2001 From: Nate Soares Date: Mon, 3 Apr 2017 17:58:28 -0300 Subject: [PATCH 6/8] Typo fix --- Lib/inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index fee5f8d016454a..b890adfeb70e49 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -297,7 +297,7 @@ def isabstract(object): # TPFLAGS_IS_ABSTRACT should have been accurate. return False # It looks like ABCMeta.__new__ has not finished running yet; we're - # probably in __init_subcalss_. We'll look for abstractmethods manually. + # probably in __init_subcalss__. We'll look for abstractmethods manually. for name, value in object.__dict__.items(): if getattr(value, "__isabstractmethod__", False): return True From cd15f76ed9fe1d10f7a4bedccfeb7b213b8ebbd0 Mon Sep 17 00:00:00 2001 From: Nate Date: Sun, 23 Apr 2017 11:21:16 -0700 Subject: [PATCH 7/8] Typo fix Fixed "1?\n====" that I accidentally killed during the resolution of a merge conflict. --- Misc/NEWS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS b/Misc/NEWS index 766b1905b7a490..bf0c015eeb54c9 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -1866,7 +1866,8 @@ Tests - Issue #28217: Adds _testconsole module to test console input. -What's New in Python 3.6.0 beta ============================== +What's New in Python 3.6.0 beta 1? +================================== *Release date: 2016-09-12* From 8a2b5d2feba52811ebe91c02a8f2b40cf61b4197 Mon Sep 17 00:00:00 2001 From: Nate Date: Sun, 23 Apr 2017 13:03:15 -0700 Subject: [PATCH 8/8] Typo fix in comment (calss -> class) --- Lib/inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index c8c3b4d88dc091..2894672f501e8b 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -303,7 +303,7 @@ def isabstract(object): # TPFLAGS_IS_ABSTRACT should have been accurate. return False # It looks like ABCMeta.__new__ has not finished running yet; we're - # probably in __init_subcalss__. We'll look for abstractmethods manually. + # probably in __init_subclass__. We'll look for abstractmethods manually. for name, value in object.__dict__.items(): if getattr(value, "__isabstractmethod__", False): return True