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 :) _______________________________________________ Tutor maillist - Tutor@python.org To unsubscribe or change subscription options: https://mail.python.org/mailman/listinfo/tutor