Определите @property и @classproperty с одинаковым именем (Python, Django)

Фон

Класс Django LiveServerTestCase имеет метод live_server_url с декоратором @classproperty. (django.utils.functional.classproperty.) Класс запускает свой тестовый сервер до запуска любых тестов и, таким образом, знает URL тестового сервера до запуска любых тестов.

У меня есть похожий MyTestCase класс, который имеет live_server_url @property. Он запускает новый тестовый сервер перед каждым тестом, и поэтому его свойство live_server_url не может быть @classproperty, поскольку он не знает своего порта до момента инстанцирования.

Чтобы сделать API для обоих классов согласованным, чтобы все тестовые функции и т.д. в кодовой базе могли использоваться с обоими классами, тесты можно написать так, чтобы они никогда не ссылались на live_server_url в setUpClass(), до выполнения всех тестов. Но это замедлило бы работу многих тестов.

Вместо этого я хочу, чтобы MyTestCase.live_server_url вызывал полезную ошибку, если на него ссылаются из объекта класса, а не объекта экземпляра.

Поскольку MyTestCase.live_server_url является @property, MyTestCase().live_server_url возвращает строку, но MyTestCase.live_server_url возвращает объект свойства. Это вызывает загадочные ошибки типа "TypeError: unsupported operand type(s) for +: 'property' and 'str'".

Актуальный вопрос

Если бы я мог определить @classproperty и @property на одном классе, то я бы определил MyTestCase.live_server_url @classproperty, который вызывает ошибку с полезным сообщением типа "live_server_url может быть вызван только на экземпляре MyTestCase, а не на классе MyTestCase".

Но когда вы определяете два метода с одинаковым именем в классе Python, то более ранний метод отбрасывается, поэтому я не могу этого сделать.

Как сделать поведение MyTestCase.live_server_url разным в зависимости от того, вызывается ли оно на классе или на экземпляре?

Одним из вариантов может быть использование метакласса. (Example)

Но поскольку я читал, что метаклассов обычно следует избегать, вот что я сделал:

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 вызывает __getattribute__().

MyTestCase.live_server_url вызывает @classproperty определение live_server_url.

Просто подкласс property и реализация __get__, чтобы поднимать ошибку, если к нему не обращаются через экземпляр :

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)

Возможно, вы сможете придумать для него более подходящее название. Но в любом случае, вы можете использовать его следующим образом:

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)

Тогда:

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

И оно будет работать как любое другое свойство, так что вы можете использовать live_server_url.setter и live_server_url.deleter

Если вы незнакомы с __get__, прочитайте это HOWTO, и оно расскажет вам все, что нужно знать о дескрипторах, которые являются промежуточной/продвинутой техникой Python. Протокол дескрипторов стоит за множеством кажущихся магическими действий, и это HOWTO показывает вам, как различные вещи, такие как classmethod, staticmethod, property могут быть реализованы на чистом Python с использованием протокола дескрипторов.

Обратите внимание, вы не обязаны наследоваться от property, простой индивидуальный (хотя и тесно связанный) подход может быть таким:

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"
Вернуться на верх