[Tutor] Do not understand code snippet from "26.8. test — Regression tests package for Python"
Peter Otten
__peter__ at web.de
Mon Apr 17 04:37:20 EDT 2017
boB Stepp wrote:
> It is here that I am struggling. If the mixin class does not inherit
> from unittest.TestCase, then how is test_func ever seen?
Perhaps it becomes clearer if we build our own class discovery / method
runner system. Given T as the baseclass for classes that provide foo_...()
methods that we want to run, and M as the mix-in that provides such methods
but isn't itself a subclass of T...
class T:
pass
class M:
def foo_one(self):
print(self.__class__.__name__, "one")
def foo_two(self):
print(self.__class__.__name__, "one")
class X(T):
def foo_x(self):
print(self.__class__.__name__, "x")
class Y(M, T):
pass
we want our discovery function to find the classes X and Y.
First attempt:
def discover_Ts():
for C in globals().values():
if issubclass(C, T):
print("found", C)
discover_Ts()
globals().values() gives all toplevel objects in the script and issubclass
checks if we got a subclass of T. Let's try:
$ python3 discovery.py
Traceback (most recent call last):
File "discovery.py", line 23, in <module>
discover_Ts()
File "discovery.py", line 19, in discover_Ts
if issubclass(C, T):
TypeError: issubclass() arg 1 must be a class
It turns out that issubclass() raises a TypeError if the object we want to
check is not a class:
>>> issubclass(int, float)
False
>>> issubclass(42, float)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: issubclass() arg 1 must be a class
For our purposes False is the better answer, so let's write our own
issubclass:
def safe_issubclass(S, B):
try:
return issubclass(S, B)
except TypeError:
return False
def discover_Ts():
for C in globals().values():
if safe_issubclass(C, T):
print("found", C)
$ python3 discovery2.py
found <class '__main__.Y'>
found <class '__main__.X'>
found <class '__main__.T'>
That's close, we only need to reject T:
def discover_Ts():
for C in globals().values():
if safe_issubclass(C, T) and C is not T:
print("found", C)
Easy. Now we have the classes we can look for the methods:
def discover_Ts():
for C in globals().values():
if safe_issubclass(C, T) and C is not T:
print("found", C, "with foo_... methods")
for name in dir(C):
if name.startswith("foo_"):
print(" ", name)
$ python3 discovery4.py
found <class '__main__.X'> with foo_... methods
foo_x
found <class '__main__.Y'> with foo_... methods
foo_one
foo_two
As you can see, to the discovery algorithm it doesn't matter where the
method is defined, it suffices that it's part of the class and can be found
by dir() or vars().
As a bonus, now that we have the class and the method let's invoke them:
def discover_Ts():
for C in globals().values():
if safe_issubclass(C, T) and C is not T:
print("found", C, "with foo_... methods")
for name in dir(C):
if name.startswith("foo_"):
yield C, name
def run_method(cls, methodname):
inst = cls()
method = getattr(inst, methodname)
method()
for cls, methodname in discover_Ts():
run_method(cls, methodname)
While you could invoke run_method() inside discover_Ts() I turned
discover_Ts() into a generator that produces class/methodname pairs which
looks a bit more pythonic to me. Let's run it:
$ python3 discovery5.py
found <class '__main__.X'> with foo_... methods
X x
Traceback (most recent call last):
File "discovery5.py", line 36, in <module>
for cls, methodname in discover_Ts():
File "discovery5.py", line 24, in discover_Ts
for C in globals().values():
RuntimeError: dictionary changed size during iteration
Oops, as the global names cls, and methodname spring into existence they
torpedize our test discovery. We could (and should when we need a moderate
amount of robustness) take a snapshot of the global variables with
list(globals().values()), but for demonstration purposes we'll just move the
toplevel loop into a function:
$ cat discovery6.py
class T:
pass
class M:
def foo_one(self):
print(self.__class__.__name__, "one")
def foo_two(self):
print(self.__class__.__name__, "two")
class X(T):
def foo_x(self):
print(self.__class__.__name__, "x")
class Y(M, T):
pass
def safe_issubclass(S, B):
try:
return issubclass(S, B)
except TypeError:
return False
def discover_Ts():
for C in globals().values():
if safe_issubclass(C, T) and C is not T:
print("found", C, "with foo_... methods")
for name in dir(C):
if name.startswith("foo_"):
yield C, name
def run_method(cls, methodname):
inst = cls()
method = getattr(inst, methodname)
method()
def main():
for cls, methodname in discover_Ts():
run_method(cls, methodname)
if __name__ == "__main__":
main()
$ python3 discovery6.py
found <class '__main__.Y'> with foo_... methods
Y one
Y two
found <class '__main__.X'> with foo_... methods
X x
That was easy. We have replicated something similar to the unit test
framework with very little code.
Now you can go and find the equivalent parts in the unittest source code :)
More information about the Tutor
mailing list