From 2c83fc51a69396d72e5ebe2630c4c54c79dc5048 Mon Sep 17 00:00:00 2001 From: Kevin Vance Date: Thu, 16 Jun 2016 15:44:33 -0400 Subject: [PATCH] Lazily get the name of hyperlinked models Add support for Hyperlink names to be callable. This allows the call to get_name() in HyperlinkedRelatedField to be deferred until the property is accessed. --- rest_framework/relations.py | 18 +++++++++++++++--- tests/test_relations.py | 12 ++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 2e7c51b22..2d7508418 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -35,15 +35,27 @@ class Hyperlink(six.text_type): A string like object that additionally has an associated name. We use this for hyperlinked URLs that may render as a named link in some contexts, or render as a plain URL in others. + + If name is callable, it will be called when the name property is + accessed. """ def __new__(self, url, name): ret = six.text_type.__new__(self, url) - ret.name = name + if callable(name): + ret._name_callable = name + else: + ret.name = name return ret def __getnewargs__(self): return(str(self), self.name,) + def __getattr__(self, key): + if key == 'name' and hasattr(self, '_name_callable'): + f = getattr(self, '_name_callable') + return f() + raise AttributeError + is_hyperlink = True @@ -368,8 +380,8 @@ class HyperlinkedRelatedField(RelatedField): if url is None: return None - name = self.get_name(value) - return Hyperlink(url, name) + name_getter = lambda: self.get_name(value) + return Hyperlink(url, name_getter) class HyperlinkedIdentityField(HyperlinkedRelatedField): diff --git a/tests/test_relations.py b/tests/test_relations.py index a070ad6de..edf955b20 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -98,6 +98,11 @@ class TestHyperlinkedRelatedField(APISimpleTestCase): representation = self.field.to_representation(MockObject(pk='')) assert representation is None + def test_representation_does_not_call_get_name(self): + self.field.get_name = lambda _: pytest.fail( + "HyperlinkedRelatedField.get_name() called eagerly") + self.field.to_representation(MockObject(pk=1)) + class TestHyperlinkedIdentityField(APISimpleTestCase): def setUp(self): @@ -233,9 +238,16 @@ class TestManyRelatedField(APISimpleTestCase): class TestHyperlink: def setup(self): self.default_hyperlink = serializers.Hyperlink('http://example.com', 'test') + self.callable_hyperlink = serializers.Hyperlink('http://example.com', lambda: 'called') def test_can_be_pickled(self): import pickle upkled = pickle.loads(pickle.dumps(self.default_hyperlink)) assert upkled == self.default_hyperlink assert upkled.name == self.default_hyperlink.name + + def test_simple_name_property(self): + assert 'test' == self.default_hyperlink.name + + def test_callable_name_property(self): + assert 'called' == self.callable_hyperlink.name