diff --git a/.travis.yml b/.travis.yml index 996c3ae80..28ebfc00f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,10 +33,6 @@ env: matrix: fast_finish: true allow_failures: - - env: TOX_ENV=py34-django18alpha - - env: TOX_ENV=py33-django18alpha - - env: TOX_ENV=py32-django18alpha - - env: TOX_ENV=py27-django18alpha - env: TOX_ENV=py34-djangomaster - env: TOX_ENV=py33-djangomaster - env: TOX_ENV=py32-djangomaster diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index ba1145136..4b8110bd6 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -360,7 +360,7 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a [oauth]: http://oauth.net/2/ [permission]: permissions.md [throttling]: throttling.md -[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax +[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax [mod_wsgi_official]: http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization [custom-user-model]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#specifying-a-custom-user-model [south-dependencies]: http://south.readthedocs.org/en/latest/dependencies.html diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index dd0698402..7a51bb05a 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -200,7 +200,7 @@ class FileUploadParser(BaseParser): if 'filename*' in filename_parm: return self.get_encoded_filename(filename_parm) return force_text(filename_parm['filename']) - except (AttributeError, KeyError): + except (AttributeError, KeyError, ValueError): pass def get_encoded_filename(self, filename_parm): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index f970a3638..6256acdd7 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -305,6 +305,9 @@ class HTMLFormRenderer(BaseRenderer): }) def render_field(self, field, parent_style): + if isinstance(field, serializers.HiddenField): + return '' + style = dict(self.default_style[field]) style.update(field.style) if 'template_pack' not in style: diff --git a/rest_framework/response.py b/rest_framework/response.py index d6ca1aad4..7f90bae10 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -81,10 +81,13 @@ class Response(SimpleTemplateResponse): def __getstate__(self): """ - Remove attributes from the response that shouldn't be cached + Remove attributes from the response that shouldn't be cached. """ state = super(Response, self).__getstate__() - for key in ('accepted_renderer', 'renderer_context', 'data'): + for key in ( + 'accepted_renderer', 'renderer_context', 'resolver_match', + 'client', 'request', 'wsgi_request', '_closable_objects' + ): if key in state: del state[key] return state diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index b9fb6f678..dd92f8b67 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -121,12 +121,17 @@ def _get_reverse_relationships(opts): """ Returns an `OrderedDict` of field names to `RelationInfo`. """ + # Note that we have a hack here to handle internal API differences for + # this internal API across Django 1.7 -> Django 1.8. + # See: https://code.djangoproject.com/ticket/24208 + reverse_relations = OrderedDict() for relation in opts.get_all_related_objects(): accessor_name = relation.get_accessor_name() + related = getattr(relation, 'related_model', relation.model) reverse_relations[accessor_name] = RelationInfo( model_field=None, - related_model=relation.model, + related_model=related, to_many=relation.field.rel.multiple, has_through_model=False ) @@ -134,9 +139,10 @@ def _get_reverse_relationships(opts): # Deal with reverse many-to-many relationships. for relation in opts.get_all_related_many_to_many_objects(): accessor_name = relation.get_accessor_name() + related = getattr(relation, 'related_model', relation.model) reverse_relations[accessor_name] = RelationInfo( model_field=None, - related_model=relation.model, + related_model=related, to_many=True, has_through_model=( (getattr(relation.field.rel, 'through', None) is not None) diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index ab0578620..87bb3ac08 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -19,6 +19,11 @@ class ReturnDict(OrderedDict): def __repr__(self): return dict.__repr__(self) + def __reduce__(self): + # Pickling these objects will drop the .serializer backlink, + # but preserve the raw data. + return (dict, (dict(self),)) + class ReturnList(list): """ @@ -33,6 +38,11 @@ class ReturnList(list): def __repr__(self): return list.__repr__(self) + def __reduce__(self): + # Pickling these objects will drop the .serializer backlink, + # but preserve the raw data. + return (list, (list(self),)) + class BoundField(object): """ diff --git a/runtests.py b/runtests.py index abf15a623..0008bfae5 100755 --- a/runtests.py +++ b/runtests.py @@ -8,8 +8,8 @@ import subprocess PYTEST_ARGS = { - 'default': ['tests'], - 'fast': ['tests', '-q'], + 'default': ['tests', '--tb=short'], + 'fast': ['tests', '--tb=short', '-q'], } FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501'] diff --git a/tests/test_filters.py b/tests/test_filters.py index dc84dcbd0..5b1b6ca52 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -467,6 +467,7 @@ class DjangoFilterOrderingTests(TestCase): for d in data: DjangoFilterOrderingModel.objects.create(**d) + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_default_ordering(self): class DjangoFilterOrderingView(generics.ListAPIView): serializer_class = DjangoFilterOrderingSerializer diff --git a/tests/test_htmlrenderer.py b/tests/test_htmlrenderer.py index 2edc6b4bd..a33b832f5 100644 --- a/tests/test_htmlrenderer.py +++ b/tests/test_htmlrenderer.py @@ -56,7 +56,13 @@ class TemplateHTMLRendererTests(TestCase): return Template("example: {{ object }}") raise TemplateDoesNotExist(template_name) + def select_template(template_name_list, dirs=None, using=None): + if template_name_list == ['example.html']: + return Template("example: {{ object }}") + raise TemplateDoesNotExist(template_name_list[0]) + django.template.loader.get_template = get_template + django.template.loader.select_template = select_template def tearDown(self): """ diff --git a/tests/test_parsers.py b/tests/test_parsers.py index 54455cf67..8816065ab 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -101,7 +101,9 @@ class TestFileUploadParser(TestCase): self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8--ÀĥƦ.txt') filename = parser.get_filename(self.stream, None, self.parser_context) - self.assertEqual(filename, 'fallback.txt') + # Malformed. Either None or 'fallback.txt' will be acceptable. + # See also https://code.djangoproject.com/ticket/24209 + self.assertIn(filename, ('fallback.txt', None)) def __replace_content_disposition(self, disposition): self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 3e64d8fec..f68405f09 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -15,7 +15,6 @@ from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory from collections import MutableMapping import json -import pickle import re @@ -408,87 +407,27 @@ class CacheRenderTest(TestCase): urls = 'tests.test_renderers' - cache_key = 'just_a_cache_key' - - @classmethod - def _get_pickling_errors(cls, obj, seen=None): - """ Return any errors that would be raised if `obj' is pickled - Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897 - """ - if seen is None: - seen = [] - try: - state = obj.__getstate__() - except AttributeError: - return - if state is None: - return - if isinstance(state, tuple): - if not isinstance(state[0], dict): - state = state[1] - else: - state = state[0].update(state[1]) - result = {} - for i in state: - try: - pickle.dumps(state[i], protocol=2) - except pickle.PicklingError: - if not state[i] in seen: - seen.append(state[i]) - result[i] = cls._get_pickling_errors(state[i], seen) - return result - - def http_resp(self, http_method, url): - """ - Simple wrapper for Client http requests - Removes the `client' and `request' attributes from as they are - added by django.test.client.Client and not part of caching - responses outside of tests. - """ - method = getattr(self.client, http_method) - resp = method(url) - resp._closable_objects = [] - del resp.client, resp.request - try: - del resp.wsgi_request - except AttributeError: - pass - return resp - - def test_obj_pickling(self): - """ - Test that responses are properly pickled - """ - resp = self.http_resp('get', '/cache') - - # Make sure that no pickling errors occurred - self.assertEqual(self._get_pickling_errors(resp), {}) - - # Unfortunately LocMem backend doesn't raise PickleErrors but returns - # None instead. - cache.set(self.cache_key, resp) - self.assertTrue(cache.get(self.cache_key) is not None) - def test_head_caching(self): """ Test caching of HEAD requests """ - resp = self.http_resp('head', '/cache') - cache.set(self.cache_key, resp) - - cached_resp = cache.get(self.cache_key) - self.assertIsInstance(cached_resp, Response) + response = self.client.head('/cache') + cache.set('key', response) + cached_response = cache.get('key') + assert isinstance(cached_response, Response) + assert cached_response.content == response.content + assert cached_response.status_code == response.status_code def test_get_caching(self): """ Test caching of GET requests """ - resp = self.http_resp('get', '/cache') - cache.set(self.cache_key, resp) - - cached_resp = cache.get(self.cache_key) - self.assertIsInstance(cached_resp, Response) - self.assertEqual(cached_resp.content, resp.content) + response = self.client.get('/cache') + cache.set('key', response) + cached_response = cache.get('key') + assert isinstance(cached_response, Response) + assert cached_response.content == response.content + assert cached_response.status_code == response.status_code class TestJSONIndentationStyles: diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 68bbbe983..b7a0484bc 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from .utils import MockObject from rest_framework import serializers from rest_framework.compat import unicode_repr +import pickle import pytest @@ -278,3 +279,19 @@ class TestNotRequiredOutput: serializer = ExampleSerializer(instance) with pytest.raises(AttributeError): serializer.data + + +class TestCacheSerializerData: + def test_cache_serializer_data(self): + """ + Caching serializer data with pickle will drop the serializer info, + but does preserve the data itself. + """ + class ExampleSerializer(serializers.Serializer): + field1 = serializers.CharField() + field2 = serializers.CharField() + + serializer = ExampleSerializer({'field1': 'a', 'field2': 'b'}) + pickled = pickle.dumps(serializer.data) + data = pickle.loads(pickled) + assert data == {'field1': 'a', 'field2': 'b'} diff --git a/tox.ini b/tox.ini index 9be14bf89..b4b708bea 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ deps = djangomaster: https://github.com/django/django/zipball/master django-guardian==1.2.4 pytest-django==2.8.0 - django-filter==0.9.1 + {py26,py27,py32,py33,py34}-django{14,15,16,17}: django-filter==0.9.1 markdown>=2.1.0 [testenv:py27-flake8]