Repository: encode/django-rest-framework

Issue:
UUIDField validation does not catch AttributeError
I am using a UUIDField in a serializer as such:

```
    class ExampleSerializer(serializers.Serializer):
        some_id = serializers.UUIDField(required = False)


    class ExampleView(APIView):

        def post(self, request):
            example_serializer = ExampleSerializer(data = request.data)
            example_serializer.is_valid()
```

If I make a request to this view with the parameter some_id mapped to a list, e.g. `{"some_id": [1, 2, 3]}`, I get an `AttributeError: list object has no attribute replace`, even though the expected behavior is that the exception is caught by `is_valid` and its return value is `False` (since a list is clearly not valid input to a UUIDField)

The `AttributeError` is thrown by Python's uuid.py (I am on Python 3.4.3) at the following line of UUID's `__init__` method:

```
    if hex is not None:
        hex = hex.replace('urn:', '').replace('uuid:', '')
```

I believe the issue lies in the `to_internal_value` method of UUIDField when it calls the UUID constructor to construct the field from the input data (code from https://github.com/tomchristie/django-rest-framework/blob/a8deb380ff70b5df8d3c62c9be981b60e363c7f9/rest_framework/fields.py#L767):

```
def to_internal_value(self, data):
    if not isinstance(data, uuid.UUID):
        try:
            if isinstance(data, six.integer_types):
                return uuid.UUID(int=data)
            else:
                return uuid.UUID(hex=data)
        except (ValueError, TypeError):
            self.fail('invalid', value=data)
    return data
```

Shouldn't `AttributeError` also be caught here before calling `self.fail` (to register validation failure)?
I an on djangorestframework version 3.3.1

Thanks for taking a look!



Discussion:
Looks to me like we should ensure data is a string when calling UUID(hex=data).

Yes. Binary string prob also okay.

And then only catch ValueError

Alt: push upstream and ensure Django raises a TypeError in this case (it probably should, really)

We may want to resolve both in REST framework, and upstream in Django.

> Yes. Binary string prob also okay.

No, it's not. There's a bytes argument for binary.

See https://docs.python.org/2/library/uuid.html

Got it!

> Alt: push upstream and ensure Django raises a TypeError in this case (it probably should, really)

Correct me but uuid doesn't seem to be using Django's field for that one right ?

Right yup, ignore my upstream comments! :p


Resolving patch:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -769,9 +769,11 @@ def to_internal_value(self, data):
             try:
                 if isinstance(data, six.integer_types):
                     return uuid.UUID(int=data)
-                else:
+                elif isinstance(data, six.string_types):
                     return uuid.UUID(hex=data)
-            except (ValueError, TypeError):
+                else:
+                    self.fail('invalid', value=data)
+            except (ValueError):
                 self.fail('invalid', value=data)
         return data
 


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -609,7 +609,8 @@ class TestUUIDField(FieldValues):
         284758210125106368185219588917561929842: uuid.UUID('d63a6fb6-88d5-40c7-a91c-9edf73283072')
     }
     invalid_inputs = {
-        '825d7aeb-05a9-45b5-a5b7': ['"825d7aeb-05a9-45b5-a5b7" is not a valid UUID.']
+        '825d7aeb-05a9-45b5-a5b7': ['"825d7aeb-05a9-45b5-a5b7" is not a valid UUID.'],
+        (1, 2, 3): ['"(1, 2, 3)" is not a valid UUID.']
     }
     outputs = {
         uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda'): '825d7aeb-05a9-45b5-a5b7-05df87923cda'