Background
The Django LiveServerTestCase
class has a live_server_url
method with a @classproperty
decorator. (django.utils.functional.classproperty
.) The class starts its test server before any tests run, and thus knows the test server's URL before any tests run.
I have a similar MyTestCase
class that has a live_server_url
@property
. It starts a new test server before each test, and thus its live_server_url
property can't be a @classproperty
because it doesn't know its port until it is instantiated.
To make the API for both consistent, so that all the test utility functions etc. in the code base can be used with both classes, the tests could be written to never reference live_server_url
in setUpClass()
, before all the tests run. But this would slow down many tests.
Instead, I want MyTestCase.live_server_url
to raise a helpful error if it is referenced from the class object rather than an instance object.
Since MyTestCase.live_server_url
is a @property
, MyTestCase().live_server_url
returns a string, but MyTestCase.live_server_url
returns a property object. This causes cryptic errors like "TypeError: unsupported operand type(s) for : 'property' and 'str'"
.
Actual question
If I could define a @classproperty
and @property
on the same class, then I would define a MyTestCase.live_server_url
@classproperty
that raises an error with a helpful message like "live_server_url can only be called on an instance of MyTestCase, not on the MyTestCase class".
But when you define 2 methods with the same name in a Python class then the earlier one gets discarded, so I can't do that.
How can I make the behavior of MyTestCase.live_server_url
different depending on whether it is called on the class or on an instance?
CodePudding user response:
One option might be to use a metaclass. (Example)
But since I read that metaclasses should usually be avoided, here is what I did:
from django.utils.functional import classproperty
class MyTestCase(TransactionTestCase):
def __getattribute__(self, attr: str):
if attr == 'live_server_url':
return "http://%s:%s" % (self.host, self._port)
return super().__getattribute__(attr)
@classproperty
def live_server_url(self):
raise ValueError('live_server_url can only be called on an instance of MyTestCase, not on the MyTestCase class')
MyTestCase().live_server_url
calls __getattribute__()
.
MyTestCase.live_server_url
calls the @classproperty
definition of live_server_url
.
CodePudding user response:
Just subclass property
and implement __get__
to raise on error if it is not accessed through an instance :
class OnlyInstanceProperty(property):
def __get__(self, obj, objtype=None):
if obj is None:
raise ValueError('live_server_url can only be called on an instance of MyTestCase, not on the MyTestCase class')
return super().__get__(obj, objtype)
Perhaps you can come up with a better name for it. But in any case, you can then use it like so:
class MyTestClass:
def __init__(self):
self.host = "foo"
self._port = "bar"
@OnlyInstanceProperty
def live_server_url(self):
return "http://%s:%s" % (self.host, self._port)
Then:
print(MyTestClass.live_server_url) # http://foo:bar
print(MyTestClass.live_server_url) # ValueError: live_server_url can only be called on an instance of MyTestCase, not on the MyTestCase class
And it will work like any other property, so you can use live_server_url.setter
and live_server_url.deleter
If you are unfamiliar with __get__
, read this HOWTO and it should tell you all you need to know about descriptors, which are an intermediate/advanced Python technique. The descriptor protocol is behind a lot of seemingly magical behavior, and that HOWTO shows you how various things like classmethod
, staticmethod
, property
could be implemented in pure python using the descriptor protocol.
Note, you don't have to inherit from property
, a simple bespoke (albeit tightly coupled) approach could be something like:
class LiveServerUrl:
def __get__(self, obj, objtype=None):
if obj is None:
raise ValueError('live_server_url can only be called on an instance of MyTestCase, not on the MyTestCase class')
return "http://%s:%s" % (obj.host, obj._port)
class MyTestClass:
live_server_url = LiveServerUrl()
def __init__(self):
self.host = "foo"
self._port = "bar"