From fa9f00982382045c587c1abc801833f36e36c017 Mon Sep 17 00:00:00 2001 From: Alejandro Gonzalez Date: Sat, 15 Jun 2019 17:30:35 +0900 Subject: [PATCH 1/4] test_abc.py: Added tests for __module__ attribute in dynamically defined ABCs --- Lib/test/test_abc.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 9f5afb241aea3a..e0b55fbc591247 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -469,6 +469,14 @@ class C(with_metaclass(abc_ABCMeta, A, B)): pass self.assertEqual(C.__class__, abc_ABCMeta) + def test_module_of_dynamically_defined_ABC_is_caller_module(self): + ABC = abc_ABCMeta('ABC', (), {}) + self.assertEqual(ABC.__module__, __name__) + + # user-defined __module__ is honored over the value set via caller inspection + ABC = abc_ABCMeta('ABC', (), {'__module__': 'some.module'}) + self.assertEqual(ABC.__module__, 'some.module') + class TestABCWithInitSubclass(unittest.TestCase): def test_works_with_init_subclass(self): @@ -479,9 +487,11 @@ class ReceivesClassKwargs: def __init_subclass__(cls, **kwargs): super().__init_subclass__() saved_kwargs.update(kwargs) - class Receiver(ReceivesClassKwargs, abc_ABC, x=1, y=2, z=3): + class Receiver(ReceivesClassKwargs, abc_ABC, x=1, y=2, __module__='some.module'): pass - self.assertEqual(saved_kwargs, dict(x=1, y=2, z=3)) + # __module__ passed via __init_subclass__ is honored over + # the value set via caller inspection + self.assertEqual(saved_kwargs, dict(x=1, y=2, __module__='some.module')) return TestLegacyAPI, TestABC, TestABCWithInitSubclass TestLegacyAPI_Py, TestABC_Py, TestABCWithInitSubclass_Py = test_factory(abc.ABCMeta, From ccdf280fe9fe694f0c74ece31fb9db2f778a1589 Mon Sep 17 00:00:00 2001 From: Alejandro Gonzalez Date: Sat, 15 Jun 2019 17:31:39 +0900 Subject: [PATCH 2/4] Set __module__ attribute of created class to caller frame in ABCMeta.__new__. This is to make dynamically-defined classes pickleable. --- Lib/_py_abc.py | 13 +++++++++++++ Lib/abc.py | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Lib/_py_abc.py b/Lib/_py_abc.py index c870ae9048b4f1..2a44f875550fea 100644 --- a/Lib/_py_abc.py +++ b/Lib/_py_abc.py @@ -1,3 +1,4 @@ +import sys from _weakrefset import WeakSet @@ -33,6 +34,18 @@ class ABCMeta(type): _abc_invalidation_counter = 0 def __new__(mcls, name, bases, namespace, /, **kwargs): + + if '__module__' not in namespace: + try: + # Set __module__ to the frame where the class is created + module = sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + # Bypass for environments where sys._getframe is not defined (e.g. Jython) + # or sys._getframe is not defined for arguments greater than 0 (IronPython) + pass + else: + namespace['__module__'] = module + cls = super().__new__(mcls, name, bases, namespace, **kwargs) # Compute set of abstract method names abstracts = {name diff --git a/Lib/abc.py b/Lib/abc.py index 431b64040a66e8..79d9a0d8fdbd53 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -3,6 +3,7 @@ """Abstract Base Classes (ABCs) according to PEP 3119.""" +import sys def abstractmethod(funcobj): """A decorator indicating abstract methods. @@ -82,6 +83,18 @@ class ABCMeta(type): even via super()). """ def __new__(mcls, name, bases, namespace, **kwargs): + + if '__module__' not in namespace: + try: + # Set __module__ to the frame where the class is created + module = sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + # Bypass for environments where sys._getframe is not defined (e.g. Jython) + # or sys._getframe is not defined for arguments greater than 0 (IronPython) + pass + else: + namespace['__module__'] = module + cls = super().__new__(mcls, name, bases, namespace, **kwargs) _abc_init(cls) return cls From 93f52bb5ea8ed83ad49ba7430f254f33d51bdb38 Mon Sep 17 00:00:00 2001 From: Alejandro Gonzalez Date: Sun, 16 Jun 2019 12:21:50 +0900 Subject: [PATCH 3/4] test_abc.py: Added tests for pickling of instances of dynamically defined ABCs --- Lib/test/test_abc.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index e0b55fbc591247..a4af145f5a25db 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -9,10 +9,11 @@ import unittest import abc +import pickle import _py_abc from inspect import isabstract -def test_factory(abc_ABCMeta, abc_get_cache_token): +def test_factory(abc_ABCMeta, abc_get_cache_token, abc_to_pickle): class TestLegacyAPI(unittest.TestCase): def test_abstractproperty_basics(self): @@ -477,6 +478,12 @@ def test_module_of_dynamically_defined_ABC_is_caller_module(self): ABC = abc_ABCMeta('ABC', (), {'__module__': 'some.module'}) self.assertEqual(ABC.__module__, 'some.module') + def test_pickling_instance_of_dynamically_defined_ABC(self): + obj = abc_to_pickle() + obj_unpickled = pickle.loads(pickle.dumps(obj)) + self.assertEqual(type(obj_unpickled), type(obj)) + self.assertDictEqual(obj_unpickled.__dict__, obj.__dict__) + class TestABCWithInitSubclass(unittest.TestCase): def test_works_with_init_subclass(self): @@ -494,10 +501,16 @@ class Receiver(ReceivesClassKwargs, abc_ABC, x=1, y=2, __module__='some.module') self.assertEqual(saved_kwargs, dict(x=1, y=2, __module__='some.module')) return TestLegacyAPI, TestABC, TestABCWithInitSubclass +# types used for pickle test +ABCToPickle_C = abc.ABCMeta('ABCToPickle_C', (), {'data': 123}) +ABCToPickle_Py = _py_abc.ABCMeta('ABCToPickle_Py', (), {'data': 123}) + TestLegacyAPI_Py, TestABC_Py, TestABCWithInitSubclass_Py = test_factory(abc.ABCMeta, - abc.get_cache_token) + abc.get_cache_token, + ABCToPickle_C) TestLegacyAPI_C, TestABC_C, TestABCWithInitSubclass_C = test_factory(_py_abc.ABCMeta, - _py_abc.get_cache_token) + _py_abc.get_cache_token, + ABCToPickle_Py) if __name__ == "__main__": unittest.main() From e3f8e026f77bdbe2f6d91bbb05df1a0268271078 Mon Sep 17 00:00:00 2001 From: Alejandro Gonzalez Date: Sun, 16 Jun 2019 19:07:15 +0900 Subject: [PATCH 4/4] Temporarily added a script for benchmarking ABCMeta. This script will be removed in a future commit. --- abc_bench.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 abc_bench.py diff --git a/abc_bench.py b/abc_bench.py new file mode 100644 index 00000000000000..c43c30997a5226 --- /dev/null +++ b/abc_bench.py @@ -0,0 +1,56 @@ +import csv +import timeit + + +def median(x): + n = len(x) + middle = n // 2 + sx = sorted(x) + if n % 2: + return sx[middle] + else: + return (sx[middle] + sx[middle - 1]) / 2 + + +implementations = {"C": "import abc", + "Py": "import _py_abc"} +statements = { + "C": { + "master": "abc.ABCMeta('ABC_C', (), {'__module__': __name__})", + "fix": "abc.ABCMeta('ABC_C', (), {})" + }, + "Py": { + "master": "_py_abc.ABCMeta('ABC_Py', (), {'__module__': __name__})", + "fix": "_py_abc.ABCMeta('ABC_Py', (), {})" + } +} + +repeat = 50000 +number = 1 + +data = {} +for imp, setup in implementations.items(): + for branch, stmt in statements[imp].items(): + print("timing {} - {} implementation of ABCMeta...".format(branch, imp)) + times = timeit.repeat(stmt, setup=setup, repeat=repeat, number=number) + header = "{}_{}".format(branch, imp) + data[header] = times + + +for imp in implementations: + t_master = median(data["master_{}".format(imp)]) + t_fix = median(data["fix_{}".format(imp)]) + + absdiff = t_fix - t_master + slowdown = (t_fix - t_master) / t_master + + print("{} implementation".format(imp)) + print(" Absolute difference:", 1000 * absdiff, "ms") + print(" Slowdown:", 100 * slowdown, "%") + +print("Dumping to CSV file...") +with open("abc_bench.csv", "w") as csvfile: + writer = csv.writer(csvfile) + data = [[header] + times for header, times in data.items()] + for row in zip(*data): + writer.writerow(row)