[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