diff --git a/django/utils/decorators.py b/django/utils/decorators.py index 10ad1ce506..1ea12b32d2 100644 --- a/django/utils/decorators.py +++ b/django/utils/decorators.py @@ -144,3 +144,15 @@ if ContextDecorator is None: with self: return func(*args, **kwargs) return inner + + +class classproperty(object): + def __init__(self, method=None): + self.fget = method + + def __get__(self, instance, owner): + return self.fget(owner) + + def getter(self, method): + self.fget = method + return self diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index e2ad15d5b5..7aaeff97af 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -789,6 +789,12 @@ via the :djadminopt:`--liveserver` option, for example: $ ./manage.py test --liveserver=localhost:8082 +.. versionchanged:: 1.9 + + In older versions ``live_server_url`` could only be accessed from an + instance. It now is a class property and can be accessed from class methods + like ``setUpClass()``. + Another way of changing the default server address is by setting the `DJANGO_LIVE_TEST_SERVER_ADDRESS` environment variable somewhere in your code (for example, in a :ref:`custom test runner`):: diff --git a/tests/servers/tests.py b/tests/servers/tests.py index e84b8788c0..3ba80cc23d 100644 --- a/tests/servers/tests.py +++ b/tests/servers/tests.py @@ -11,6 +11,7 @@ from django.core.exceptions import ImproperlyConfigured from django.test import LiveServerTestCase, override_settings from django.utils._os import upath from django.utils.http import urlencode +from django.utils.six import text_type from django.utils.six.moves.urllib.error import HTTPError from django.utils.six.moves.urllib.request import urlopen @@ -71,6 +72,9 @@ class LiveServerAddress(LiveServerBase): else: del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] + # put it in a list to prevent descriptor lookups in test + cls.live_server_url_test = [cls.live_server_url] + @classmethod def tearDownClass(cls): # skip it, as setUpClass doesn't call its parent either @@ -87,10 +91,9 @@ class LiveServerAddress(LiveServerBase): finally: super(LiveServerAddress, cls).tearDownClass() - def test_test_test(self): - # Intentionally empty method so that the test is picked up by the - # test runner and the overridden setUpClass() method is executed. - pass + def test_live_server_url_is_class_property(self): + self.assertIsInstance(self.live_server_url_test[0], text_type) + self.assertEqual(self.live_server_url_test[0], self.live_server_url) class LiveServerViews(LiveServerBase): diff --git a/tests/utils_tests/test_decorators.py b/tests/utils_tests/test_decorators.py index 10531937ad..6135eb1994 100644 --- a/tests/utils_tests/test_decorators.py +++ b/tests/utils_tests/test_decorators.py @@ -2,7 +2,7 @@ from django.http import HttpResponse from django.template import engines from django.template.response import TemplateResponse from django.test import RequestFactory, SimpleTestCase -from django.utils.decorators import decorator_from_middleware +from django.utils.decorators import classproperty, decorator_from_middleware class ProcessViewMiddleware(object): @@ -107,3 +107,41 @@ class DecoratorFromMiddlewareTests(SimpleTestCase): self.assertTrue(getattr(request, 'process_response_reached', False)) # Check that process_response saw the rendered content self.assertEqual(request.process_response_content, b"Hello world") + + +class ClassPropertyTest(SimpleTestCase): + def test_getter(self): + class Foo(object): + foo_attr = 123 + + def __init__(self): + self.foo_attr = 456 + + @classproperty + def foo(cls): + return cls.foo_attr + + class Bar(object): + bar = classproperty() + + @bar.getter + def bar(cls): + return 123 + + self.assertEqual(Foo.foo, 123) + self.assertEqual(Foo().foo, 123) + self.assertEqual(Bar.bar, 123) + self.assertEqual(Bar().bar, 123) + + def test_override_getter(self): + class Foo(object): + @classproperty + def foo(cls): + return 123 + + @foo.getter + def foo(cls): + return 456 + + self.assertEqual(Foo.foo, 456) + self.assertEqual(Foo().foo, 456)