Home > Blockchain >  How do I avoid redundant python code when implementing 2 identical classes with different inheritanc
How do I avoid redundant python code when implementing 2 identical classes with different inheritanc

Time:09-14

I have 2 classes that are identical aside from their inheritance. Each class overrides the same methods. I tried to avoid redundancy by using a common global method, but it wasn't enough to avoid a JSCPD error.

I'm not quite sure how to arrange things so that I only have 1 instance of the over-ridden methods, but each over-riding the methods of different base classes...

Probably if I removed the doc strings (which I've removed below), I'd avoid the linting error, but I'd prefer knowing how to accomplish this correctly.

import time

from django.test import TestCase, TransactionTestCase

LONG_TEST_THRESH_SECS = 20
LONG_TEST_ALERT_STR = f" [ALERT > {LONG_TEST_THRESH_SECS}]"


class TracebaseTestCase(TestCase):

    maxDiff = None
    databases = "__all__"

    def setUp(self):
        self.testStartTime = time.time()

    def tearDown(self):
        _reportRunTime(self.id(), self.testStartTime)

    def setUpClass(self):
        self.classStartTime = time.time()

    def setUpTestData(self):
        _reportRunTime(f"{self.__class__.__name__}.setUpTestData", self.classStartTime)

    class Meta:
        abstract = True


class TracebaseTransactionTestCase(TransactionTestCase):

    maxDiff = None
    databases = "__all__"

    def setUp(self):
        self.testStartTime = time.time()

    def tearDown(self):
        _reportRunTime(self.id(), self.testStartTime)

    def setUpClass(self):
        self.classStartTime = time.time()

    def setUpTestData(self):
        _reportRunTime(f"{self.__class__.__name__}.setUpTestData", self.classStartTime)

    class Meta:
        abstract = True


def _reportRunTime(id, startTime):
    t = time.time() - startTime
    heads_up = ""  # String to include for tests that run too long

    if t > LONG_TEST_THRESH_SECS:
        heads_up = LONG_TEST_ALERT_STR

    print("TEST TIME%s: %s: %.3f" % (heads_up, id, t))

I have a vague recollection back in my C school days about creating classes where you could supply a class as input for it's "inheritance" but I don't remember what that was called. I recall it used angle brackets in its declaration. I feel like that's what I need. Something like:

class abstractBaseClass(<base class input>):
    # This would be where the methods would be defined
    # I would also make reportRunTime be a member function here if I knew how to implement this "class template"

class TracebaseTestCase(abstractBaseClass(TestCase))
    pass

class TracebaseTransactionTestCase(abstractBaseClass(TransactionTestCase))
    pass

Am I close?

UPDATE: I fleshed out the rest of my source code, since there seemed to be some question about if it would affect the answer.

The point is that in each case, I am either over-riding the methods of TestCase or TransactionTestCase. And wherever I inherit from (for example) TracebaseTestCase, TestCase is determining when to run setUp, tearDown, setUpClass, and setUpTestData.

The code works as it is. I just want to avoid the jscpd linting error and reduce the redundancy.

CodePudding user response:

You might consider using a mix-in.

class TestSkeleton:
    maxDiff = None
    databases = "__all__"

    def setUp(self):
        self.testStartTime = time.time()

    def tearDown(self):
        _reportRunTime(self.id(), self.testStartTime)

    def setUpClass(self):
        self.classStartTime = time.time()

    def setUpTestData(self):
        _reportRunTime(f"{self.__class__.__name__}.setUpTestData", self.classStartTime)

    class Meta:
        abstract = True


class TracebaseTestCase(TestSkeleton, TestCase):
    pass


class TracebaseTransactionTestCase(TestSkeleton, TransactionTestCase):
    pass

tearDown and setUpTestData probably need some adjustment depending on exactly what _reportRunTime is and what it's first argument is supposed to represent.

CodePudding user response:

@chepner's answer is correct, though I didn't understand why or how it worked because I never understood the concept of mixins. I couldn't map it to my naive notion of multiple inheritance. So I did some testing with trial and error and now have more confidence that I have a better conceptual (though perhaps technically inaccurate) understanding of how they work. I had previously been conceptually dissuaded by the notion of "multiple inheritance" to imply multiple independent parent classes/objects. However (to use a geeky analogy) I now see it more like the parent being "Tuvix" from Star Trek Voyager. The parents (Tuvok and Neelix) are not independent individuals. There's only 1 parent: Tuvix, who is a merging of the 2 parents.

And from my testing, I have come to understand that the order of the superclasses establishes whose characteristics dominate. Precedence goes from left to right. Anything you "override" in the left-side class is what gets set/called when a derived class calls/gets it.

I don't need to reiterate @chepner's answer, but I will provide an example to demonstrate why it works... Take these classes as an example:

class mybaseclass():
    """a.k.a. TestCase"""

    classvar = "base"

    def member_override_test(self):
        print(f"member_override_test in mybaseclass, classvar: [{self.classvar}]")

    @classmethod
    def classmethod_override_test(cls):
        print(f"classmethod_override_test in mybaseclass, classvar: [{cls.classvar}]")

    def run_member_super_test(self):
        print(f"classvar: {self.classvar}")
        print("Calling member_override_test from mybaseclass:")
        self.member_override_test()
        print("Calling classmethod_override_test from mybaseclass:")
        self.classmethod_override_test()
        print("Calling run_test from mybaseclass:")
        self.run_test()


class mymixinclass():
    """a.k.a. TestSkeleton"""

    classvar = "mixin"

    def member_override_test(self):
        print(f"member_override_test in mymixinclass, classvar: [{self.classvar}]")

    @classmethod
    def classmethod_override_test(cls):
        print(f"classmethod_override_test in mymixinclass, classvar: [{cls.classvar}]")


class mypseudoderivedclass(mymixinclass, mybaseclass):
    """a.k.a. TracebaseTestCase"""

    def member_override_test(self):
        print(f"member_override_test in mypseudoderivedclass, classvar: [{self.classvar}]")
        print(f"Calling super.member_override_test")
        super().member_override_test()

    def classmethod_override_test(self):
        print(f"classmethod_override_test in mypseudoderivedclass, classvar: [{self.classvar}]")
        print(f"Calling super.classmethod_override_test")
        super().classmethod_override_test()

    def run_test(self):
        print(f"classvar: {self.classvar}")
        print("Calling member_override_test from mypseudoderivedclass object:")
        self.member_override_test()
        print("Calling classmethod_override_test from mypseudoderivedclass object:")
        self.classmethod_override_test()


class reversemypseudoderivedclass(mybaseclass, mymixinclass):
    """a.k.a. TracebaseTestCase - reversing the order of the mixins"""

    def member_override_test(self):
        print(f"member_override_test in reversemypseudoderivedclass, classvar: [{self.classvar}]")
        print(f"Calling super.member_override_test")
        super().member_override_test()

    def classmethod_override_test(self):
        print(f"classmethod_override_test in reversemypseudoderivedclass, classvar: [{self.classvar}]")
        print(f"Calling super.classmethod_override_test")
        super().classmethod_override_test()

    def run_test(self):
        print(f"classvar: {self.classvar}")
        print("Calling member_override_test from mypseudoderivedclass object:")
        self.member_override_test()
        print("Calling classmethod_override_test from mypseudoderivedclass object:")
        self.classmethod_override_test()

And here's what you see when you play with those classes in the python shell:

In [1]: from DataRepo.tests.tracebase_test_case import mypseudoderivedclass, reversemypseudoderivedclass
   ...: mpdc = mypseudoderivedclass()

In [2]: mpdc.run_test()
   ...: 
classvar: mixin
Calling member_override_test from mypseudoderivedclass object:
member_override_test in mypseudoderivedclass, classvar: [mixin]
Calling super.member_override_test
member_override_test in mymixinclass, classvar: [mixin]
Calling classmethod_override_test from mypseudoderivedclass object:
classmethod_override_test in mypseudoderivedclass, classvar: [mixin]
Calling super.classmethod_override_test
classmethod_override_test in mymixinclass, classvar: [mixin]

In [3]: mpdc.member_override_test()
member_override_test in mypseudoderivedclass, classvar: [mixin]
Calling super.member_override_test
member_override_test in mymixinclass, classvar: [mixin]

In [4]: mpdc.classmethod_override_test()
classmethod_override_test in mypseudoderivedclass, classvar: [mixin]
Calling super.classmethod_override_test
classmethod_override_test in mymixinclass, classvar: [mixin]

In [5]: mpdc.run_member_super_test()
classvar: mixin
Calling member_override_test from mybaseclass:
member_override_test in mypseudoderivedclass, classvar: [mixin]
Calling super.member_override_test
member_override_test in mymixinclass, classvar: [mixin]
Calling classmethod_override_test from mybaseclass:
classmethod_override_test in mypseudoderivedclass, classvar: [mixin]
Calling super.classmethod_override_test
classmethod_override_test in mymixinclass, classvar: [mixin]
Calling run_test from mybaseclass:
classvar: mixin
Calling member_override_test from mypseudoderivedclass object:
member_override_test in mypseudoderivedclass, classvar: [mixin]
Calling super.member_override_test
member_override_test in mymixinclass, classvar: [mixin]
Calling classmethod_override_test from mypseudoderivedclass object:
classmethod_override_test in mypseudoderivedclass, classvar: [mixin]
Calling super.classmethod_override_test
classmethod_override_test in mymixinclass, classvar: [mixin]

In [6]: rmpdc = reversemypseudoderivedclass()
   ...: rmpdc.run_test()
classvar: base
Calling member_override_test from mypseudoderivedclass object:
member_override_test in reversemypseudoderivedclass, classvar: [base]
Calling super.member_override_test
member_override_test in mybaseclass, classvar: [base]
Calling classmethod_override_test from mypseudoderivedclass object:
classmethod_override_test in reversemypseudoderivedclass, classvar: [base]
Calling super.classmethod_override_test
classmethod_override_test in mybaseclass, classvar: [base]

In [7]: rmpdc.member_override_test()
member_override_test in reversemypseudoderivedclass, classvar: [base]
Calling super.member_override_test
member_override_test in mybaseclass, classvar: [base]

In [8]: rmpdc.classmethod_override_test()
classmethod_override_test in reversemypseudoderivedclass, classvar: [base]
Calling super.classmethod_override_test
classmethod_override_test in mybaseclass, classvar: [base]

In [9]: rmpdc.run_member_super_test()
classvar: base
Calling member_override_test from mybaseclass:
member_override_test in reversemypseudoderivedclass, classvar: [base]
Calling super.member_override_test
member_override_test in mybaseclass, classvar: [base]
Calling classmethod_override_test from mybaseclass:
classmethod_override_test in reversemypseudoderivedclass, classvar: [base]
Calling super.classmethod_override_test
classmethod_override_test in mybaseclass, classvar: [base]
Calling run_test from mybaseclass:
classvar: base
Calling member_override_test from mypseudoderivedclass object:
member_override_test in reversemypseudoderivedclass, classvar: [base]
Calling super.member_override_test
member_override_test in mybaseclass, classvar: [base]
Calling classmethod_override_test from mypseudoderivedclass object:
classmethod_override_test in reversemypseudoderivedclass, classvar: [base]
Calling super.classmethod_override_test
classmethod_override_test in mybaseclass, classvar: [base]

Note that the class variable classvar's value and the method handling the (super) calls depend on the order of the mixins in the class's multiple inheritances.

Factory Alternative

Before I understood the mixins, I tried another solution that solves the sample problem in a different way. You can indeed parameterize the base class, which I learned from another answer, by creating a factory function, then vivify the derived classes by making factory calls.

Note, in this answer, I realized I could avoid overriding setUpClass by setting the "class start time" in the member data:

import time

from django.test import TestCase, TransactionTestCase

LONG_TEST_THRESH_SECS = 20
LONG_TEST_ALERT_STR = f" [ALERT > {LONG_TEST_THRESH_SECS}]"


def test_case_class_factory(base_class):
    class TracebaseTestCaseTemplate(base_class):
        maxDiff = None
        databases = "__all__"
        classStartTime = time.time()

        def setUp(self):
            self.testStartTime = time.time()

        def tearDown(self):
            reportRunTime(self.id(), self.testStartTime)

        @classmethod
        def setUpTestData(cls):
            super().setUpTestData()
            reportRunTime(f"{cls.__name__}.setUpTestData", cls.classStartTime)

        class Meta:
            abstract = True

    return TracebaseTestCaseTemplate


def reportRunTime(id, startTime):
    t = time.time() - startTime
    heads_up = ""  # String to include for tests that run too long

    if t > LONG_TEST_THRESH_SECS:
        heads_up = LONG_TEST_ALERT_STR

    print("TEST TIME%s: %s: %.3f" % (heads_up, id, t))


# Classes created by the factory with different base classes:
TracebaseTestCase = test_case_class_factory(TestCase)
TracebaseTransactionTestCase = test_case_class_factory(TransactionTestCase)

Both solutions work, and while I like the preserved inheritance of the factory method, once you understand mixins, I feel like @chepner's code is easier to read, so I will select his answer.

  • Related