diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 8e2281b524f..571c145579d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -47,10 +47,24 @@ class empty: pass +class BuiltinSignatureError(Exception): + """ + Built-in function signatures are not inspectable. This exception is raised + so the serializer can raise a helpful error message. + """ + pass + + def is_simple_callable(obj): """ True if the object is a callable that takes no arguments. """ + # Bail early since we cannot inspect built-in function signatures. + if inspect.isbuiltin(obj): + raise BuiltinSignatureError( + 'Built-in function signatures are not inspectable. ' + 'Wrap the function call in a simple, pure Python function.') + if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)): return False @@ -427,6 +441,18 @@ def get_attribute(self, instance): """ try: return get_attribute(instance, self.source_attrs) + except BuiltinSignatureError as exc: + msg = ( + 'Field source for `{serializer}.{field}` maps to a built-in ' + 'function type and is invalid. Define a property or method on ' + 'the `{instance}` instance that wraps the call to the built-in ' + 'function.'.format( + serializer=self.parent.__class__.__name__, + field=self.field_name, + instance=instance.__class__.__name__, + ) + ) + raise type(exc)(msg) except (KeyError, AttributeError) as exc: if self.default is not empty: return self.get_default() diff --git a/tests/test_fields.py b/tests/test_fields.py index 5313aa3952f..ba516403143 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -14,7 +14,9 @@ import rest_framework from rest_framework import exceptions, serializers from rest_framework.compat import ProhibitNullCharactersValidator -from rest_framework.fields import DjangoImageField, is_simple_callable +from rest_framework.fields import ( + BuiltinSignatureError, DjangoImageField, is_simple_callable +) # Tests for helper functions. # --------------------------- @@ -86,6 +88,18 @@ class Meta: assert is_simple_callable(ChoiceModel().get_choice_field_display) + def test_builtin_function(self): + # Built-in function signatures are not easily inspectable, so the + # current expectation is to just raise a helpful error message. + timestamp = datetime.datetime.now() + + with pytest.raises(BuiltinSignatureError) as exc_info: + is_simple_callable(timestamp.date) + + assert str(exc_info.value) == ( + 'Built-in function signatures are not inspectable. Wrap the ' + 'function call in a simple, pure Python function.') + def test_type_annotation(self): # The annotation will otherwise raise a syntax error in python < 3.5 locals = {} @@ -206,6 +220,18 @@ def example_callable(self): assert 'method call failed' in str(exc_info.value) + def test_builtin_callable_source_raises(self): + class BuiltinSerializer(serializers.Serializer): + date = serializers.ReadOnlyField(source='timestamp.date') + + with pytest.raises(BuiltinSignatureError) as exc_info: + BuiltinSerializer({'timestamp': datetime.datetime.now()}).data + + assert str(exc_info.value) == ( + 'Field source for `BuiltinSerializer.date` maps to a built-in ' + 'function type and is invalid. Define a property or method on ' + 'the `dict` instance that wraps the call to the built-in function.') + class TestReadOnly: def setup(self):