Home > Enterprise >  Define a @property and @classproperty with the same name (Python, Django)
Define a @property and @classproperty with the same name (Python, Django)

Time:10-28

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"
  • Related