Определите @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"