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.