Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Philip Douglas 2013-10-03 11:12:55 +01:00
commit a2ae469f6b
19 changed files with 120 additions and 73 deletions

View File

@ -195,9 +195,9 @@ For more details on using filter sets see the [django-filter documentation][djan
## SearchFilter ## SearchFilter
The `SearchFilterBackend` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. The `SearchFilter` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin].
The `SearchFilterBackend` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`.
class UserListView(generics.ListAPIView): class UserListView(generics.ListAPIView):
queryset = User.objects.all() queryset = User.objects.all()

View File

@ -59,7 +59,7 @@ using the `APIView` class based views.
Or, if you're using the `@api_view` decorator with function based views. Or, if you're using the `@api_view` decorator with function based views.
@api_view('GET') @api_view('GET')
@throttle_classes(UserRateThrottle) @throttle_classes([UserRateThrottle])
def example_view(request, format=None): def example_view(request, format=None):
content = { content = {
'status': 'request was permitted' 'status': 'request was permitted'

View File

@ -6,7 +6,7 @@
## Javascript clients ## Javascript clients
If your building a javascript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers. If youre building a JavaScript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers.
AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website. AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website.

View File

@ -115,6 +115,7 @@ The context that's available to the template:
* `name` : The name of the resource * `name` : The name of the resource
* `post_form` : A form instance for use by the POST form (if allowed) * `post_form` : A form instance for use by the POST form (if allowed)
* `put_form` : A form instance for use by the PUT form (if allowed) * `put_form` : A form instance for use by the PUT form (if allowed)
* `display_edit_forms` : A boolean indicating whether or not POST, PUT and PATCH forms will be displayed
* `request` : The request object * `request` : The request object
* `response` : The response object * `response` : The response object
* `version` : The version of Django REST Framework * `version` : The version of Django REST Framework
@ -122,6 +123,8 @@ The context that's available to the template:
* `FORMAT_PARAM` : The view can accept a format override * `FORMAT_PARAM` : The view can accept a format override
* `METHOD_PARAM` : The view can accept a method override * `METHOD_PARAM` : The view can accept a method override
You can override the `BrowsableAPIRenderer.get_context()` method to customise the context that gets passed to the template.
#### Not using base.html #### Not using base.html
For more advanced customization, such as not having a Bootstrap basis or tighter integration with the rest of your site, you can simply choose not to have `api.html` extend `base.html`. Then the page content and capabilities are entirely up to you. For more advanced customization, such as not having a Bootstrap basis or tighter integration with the rest of your site, you can simply choose not to have `api.html` extend `base.html`. Then the page content and capabilities are entirely up to you.

View File

@ -168,6 +168,8 @@ The following people have helped make REST framework great.
* Mathieu Pillard - [diox] * Mathieu Pillard - [diox]
* Edmond Wong - [edmondwong] * Edmond Wong - [edmondwong]
* Ben Reilly - [bwreilly] * Ben Reilly - [bwreilly]
* Tai Lee - [mrmachine]
* Markus Kaiserswerth - [mkai]
Many thanks to everyone who's contributed to the project. Many thanks to everyone who's contributed to the project.
@ -372,3 +374,5 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[diox]: https://github.com/diox [diox]: https://github.com/diox
[edmondwong]: https://github.com/edmondwong [edmondwong]: https://github.com/edmondwong
[bwreilly]: https://github.com/bwreilly [bwreilly]: https://github.com/bwreilly
[mrmachine]: https://github.com/mrmachine
[mkai]: https://github.com/mkai

View File

@ -40,6 +40,11 @@ You can determine your currently installed version using `pip freeze`:
## 2.3.x series ## 2.3.x series
### Master
* Added JSON renderer support for numpy scalars.
* Added `get_context` hook in `BrowsableAPIRenderer`.
### 2.3.8 ### 2.3.8
**Date**: 11th September 2013 **Date**: 11th September 2013
@ -53,7 +58,7 @@ You can determine your currently installed version using `pip freeze`:
* 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views. * 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views.
* Bugfix: `required=True` argument fixed for boolean serializer fields. * Bugfix: `required=True` argument fixed for boolean serializer fields.
* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists.
* Bugfix: Client sending emptry string instead of file now clears `FileField`. * Bugfix: Client sending empty string instead of file now clears `FileField`.
* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. * Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`.
### 2.3.7 ### 2.3.7

View File

@ -147,7 +147,7 @@ Similarly, we can control the format of the request that we send, using the `Con
# POST using form data # POST using form data
curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123" curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123"
{"id": 3, "title": "", "code": "123", "linenos": false, "language": "python", "style": "friendly"} {"id": 3, "title": "", "code": "print 123", "linenos": false, "language": "python", "style": "friendly"}
# POST using JSON # POST using JSON
curl -X POST http://127.0.0.1:8000/snippets/ -d '{"code": "print 456"}' -H "Content-Type: application/json" curl -X POST http://127.0.0.1:8000/snippets/ -d '{"code": "print 456"}' -H "Content-Type: application/json"

View File

@ -85,7 +85,7 @@ Right, we'd better write some views then. Open `quickstart/views.py` and get ty
queryset = Group.objects.all() queryset = Group.objects.all()
serializer_class = GroupSerializer serializer_class = GroupSerializer
Rather that write multiple views we're grouping together all the common behavior into classes called `ViewSets`. Rather than write multiple views we're grouping together all the common behavior into classes called `ViewSets`.
We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise. We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise.

View File

@ -80,6 +80,14 @@ except ImportError:
Image = None Image = None
def get_model_name(model_cls):
try:
return model_cls._meta.model_name
except AttributeError:
# < 1.6 used module_name instead of model_name
return model_cls._meta.module_name
def get_concrete_model(model_cls): def get_concrete_model(model_cls):
try: try:
return model_cls._meta.concrete_model return model_cls._meta.concrete_model

View File

@ -4,7 +4,7 @@ returned by list views.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models from django.db import models
from rest_framework.compat import django_filters, six, guardian from rest_framework.compat import django_filters, six, guardian, get_model_name
from functools import reduce from functools import reduce
import operator import operator
@ -158,7 +158,7 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend):
model_cls = queryset.model model_cls = queryset.model
kwargs = { kwargs = {
'app_label': model_cls._meta.app_label, 'app_label': model_cls._meta.app_label,
'model_name': model_cls._meta.module_name 'model_name': get_model_name(model_cls)
} }
permission = self.perm_format % kwargs permission = self.perm_format % kwargs
return guardian.shortcuts.get_objects_for_user(user, permission, queryset) return guardian.shortcuts.get_objects_for_user(user, permission, queryset)

View File

@ -8,7 +8,8 @@ import warnings
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
from django.http import Http404 from django.http import Http404
from rest_framework.compat import oauth2_provider_scope, oauth2_constants from rest_framework.compat import (get_model_name, oauth2_provider_scope,
oauth2_constants)
class BasePermission(object): class BasePermission(object):
@ -116,7 +117,7 @@ class DjangoModelPermissions(BasePermission):
""" """
kwargs = { kwargs = {
'app_label': model_cls._meta.app_label, 'app_label': model_cls._meta.app_label,
'model_name': model_cls._meta.module_name 'model_name': get_model_name(model_cls)
} }
return [perm % kwargs for perm in self.perms_map[method]] return [perm % kwargs for perm in self.perms_map[method]]
@ -177,7 +178,7 @@ class DjangoObjectPermissions(DjangoModelPermissions):
def get_required_object_permissions(self, method, model_cls): def get_required_object_permissions(self, method, model_cls):
kwargs = { kwargs = {
'app_label': model_cls._meta.app_label, 'app_label': model_cls._meta.app_label,
'model_name': model_cls._meta.module_name 'model_name': get_model_name(model_cls)
} }
return [perm % kwargs for perm in self.perms_map[method]] return [perm % kwargs for perm in self.perms_map[method]]

View File

@ -272,7 +272,9 @@ class TemplateHTMLRenderer(BaseRenderer):
return [self.template_name] return [self.template_name]
elif hasattr(view, 'get_template_names'): elif hasattr(view, 'get_template_names'):
return view.get_template_names() return view.get_template_names()
raise ImproperlyConfigured('Returned a template response with no template_name') elif hasattr(view, 'template_name'):
return [view.template_name]
raise ImproperlyConfigured('Returned a template response with no `template_name` attribute set on either the view or response')
def get_exception_template(self, response): def get_exception_template(self, response):
template_names = [name % {'status_code': response.status_code} template_names = [name % {'status_code': response.status_code}
@ -388,7 +390,7 @@ class HTMLFormRenderer(BaseRenderer):
# likely change at some point. # likely change at some point.
self.renderer_context = renderer_context or {} self.renderer_context = renderer_context or {}
request = renderer_context['request'] request = self.renderer_context['request']
# Creating an on the fly form see: # Creating an on the fly form see:
# http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
@ -419,8 +421,13 @@ class BrowsableAPIRenderer(BaseRenderer):
""" """
renderers = [renderer for renderer in view.renderer_classes renderers = [renderer for renderer in view.renderer_classes
if not issubclass(renderer, BrowsableAPIRenderer)] if not issubclass(renderer, BrowsableAPIRenderer)]
non_template_renderers = [renderer for renderer in renderers
if not hasattr(renderer, 'get_template_names')]
if not renderers: if not renderers:
return None return None
elif non_template_renderers:
return non_template_renderers[0]()
return renderers[0]() return renderers[0]()
def get_content(self, renderer, data, def get_content(self, renderer, data,
@ -564,6 +571,49 @@ class BrowsableAPIRenderer(BaseRenderer):
def get_breadcrumbs(self, request): def get_breadcrumbs(self, request):
return get_breadcrumbs(request.path) return get_breadcrumbs(request.path)
def get_context(self, data, accepted_media_type, renderer_context):
"""
Returns the context used to render.
"""
view = renderer_context['view']
request = renderer_context['request']
response = renderer_context['response']
renderer = self.get_default_renderer(view)
raw_data_put_form = self.get_raw_data_form(view, 'PUT', request)
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request)
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
context = {
'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
'view': view,
'request': request,
'response': response,
'description': self.get_description(view),
'name': self.get_name(view),
'version': VERSION,
'breadcrumblist': self.get_breadcrumbs(request),
'allowed_methods': view.allowed_methods,
'available_formats': [renderer.format for renderer in view.renderer_classes],
'put_form': self.get_rendered_html_form(view, 'PUT', request),
'post_form': self.get_rendered_html_form(view, 'POST', request),
'patch_form': self.get_rendered_html_form(view, 'PATCH', request),
'delete_form': self.get_rendered_html_form(view, 'DELETE', request),
'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),
'raw_data_put_form': raw_data_put_form,
'raw_data_post_form': self.get_raw_data_form(view, 'POST', request),
'raw_data_patch_form': raw_data_patch_form,
'raw_data_put_or_patch_form': raw_data_put_or_patch_form,
'display_edit_forms': bool(response.status_code != 403),
'api_settings': api_settings
}
return context
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
""" """
Render the HTML for the browsable API representation. Render the HTML for the browsable API representation.
@ -571,60 +621,15 @@ class BrowsableAPIRenderer(BaseRenderer):
self.accepted_media_type = accepted_media_type or '' self.accepted_media_type = accepted_media_type or ''
self.renderer_context = renderer_context or {} self.renderer_context = renderer_context or {}
view = renderer_context['view']
request = renderer_context['request']
response = renderer_context['response']
renderer = self.get_default_renderer(view)
content = self.get_content(renderer, data, accepted_media_type, renderer_context)
put_form = self.get_rendered_html_form(view, 'PUT', request)
post_form = self.get_rendered_html_form(view, 'POST', request)
patch_form = self.get_rendered_html_form(view, 'PATCH', request)
delete_form = self.get_rendered_html_form(view, 'DELETE', request)
options_form = self.get_rendered_html_form(view, 'OPTIONS', request)
raw_data_put_form = self.get_raw_data_form(view, 'PUT', request)
raw_data_post_form = self.get_raw_data_form(view, 'POST', request)
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request)
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
name = self.get_name(view)
description = self.get_description(view)
breadcrumb_list = self.get_breadcrumbs(request)
template = loader.get_template(self.template) template = loader.get_template(self.template)
context = RequestContext(request, { context = self.get_context(data, accepted_media_type, renderer_context)
'content': content, context = RequestContext(renderer_context['request'], context)
'view': view,
'request': request,
'response': response,
'description': description,
'name': name,
'version': VERSION,
'breadcrumblist': breadcrumb_list,
'allowed_methods': view.allowed_methods,
'available_formats': [renderer.format for renderer in view.renderer_classes],
'put_form': put_form,
'post_form': post_form,
'patch_form': patch_form,
'delete_form': delete_form,
'options_form': options_form,
'raw_data_put_form': raw_data_put_form,
'raw_data_post_form': raw_data_post_form,
'raw_data_patch_form': raw_data_patch_form,
'raw_data_put_or_patch_form': raw_data_put_or_patch_form,
'api_settings': api_settings
})
ret = template.render(context) ret = template.render(context)
# Munge DELETE Response code to allow us to return content # Munge DELETE Response code to allow us to return content
# (Do this *after* we've rendered the template so that we include # (Do this *after* we've rendered the template so that we include
# the normal deletion response code in the output) # the normal deletion response code in the output)
response = renderer_context['response']
if response.status_code == status.HTTP_204_NO_CONTENT: if response.status_code == status.HTTP_204_NO_CONTENT:
response.status_code = status.HTTP_200_OK response.status_code = status.HTTP_200_OK

View File

@ -518,6 +518,9 @@ class BaseSerializer(WritableField):
""" """
Save the deserialized object and return it. Save the deserialized object and return it.
""" """
# Clear cached _data, which may be invalidated by `save()`
self._data = None
if isinstance(self.object, list): if isinstance(self.object, list):
[self.save_object(item, **kwargs) for item in self.object] [self.save_object(item, **kwargs) for item in self.object]

View File

@ -122,7 +122,7 @@
</div> </div>
</div> </div>
{% if response.status_code != 403 %} {% if display_edit_forms %}
{% if post_form or raw_data_post_form %} {% if post_form or raw_data_post_form %}
<div {% if post_form %}class="tabbable"{% endif %}> <div {% if post_form %}class="tabbable"{% endif %}>

View File

@ -113,7 +113,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
Integration tests for filtered list views. Integration tests for filtered list views.
""" """
@unittest.skipUnless(django_filters, 'django-filters not installed') @unittest.skipUnless(django_filters, 'django-filter not installed')
def test_get_filtered_fields_root_view(self): def test_get_filtered_fields_root_view(self):
""" """
GET requests to paginated ListCreateAPIView should return paginated results. GET requests to paginated ListCreateAPIView should return paginated results.
@ -142,7 +142,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
expected_data = [f for f in self.data if f['date'] == search_date] expected_data = [f for f in self.data if f['date'] == search_date]
self.assertEqual(response.data, expected_data) self.assertEqual(response.data, expected_data)
@unittest.skipUnless(django_filters, 'django-filters not installed') @unittest.skipUnless(django_filters, 'django-filter not installed')
def test_filter_with_queryset(self): def test_filter_with_queryset(self):
""" """
Regression test for #814. Regression test for #814.
@ -157,7 +157,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
expected_data = [f for f in self.data if f['decimal'] == search_decimal] expected_data = [f for f in self.data if f['decimal'] == search_decimal]
self.assertEqual(response.data, expected_data) self.assertEqual(response.data, expected_data)
@unittest.skipUnless(django_filters, 'django-filters not installed') @unittest.skipUnless(django_filters, 'django-filter not installed')
def test_filter_with_get_queryset_only(self): def test_filter_with_get_queryset_only(self):
""" """
Regression test for #834. Regression test for #834.
@ -168,7 +168,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
# Used to raise "issubclass() arg 2 must be a class or tuple of classes" # Used to raise "issubclass() arg 2 must be a class or tuple of classes"
# here when neither `model' nor `queryset' was specified. # here when neither `model' nor `queryset' was specified.
@unittest.skipUnless(django_filters, 'django-filters not installed') @unittest.skipUnless(django_filters, 'django-filter not installed')
def test_get_filtered_class_root_view(self): def test_get_filtered_class_root_view(self):
""" """
GET requests to filtered ListCreateAPIView that have a filter_class set GET requests to filtered ListCreateAPIView that have a filter_class set
@ -216,7 +216,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
f['decimal'] < search_decimal] f['decimal'] < search_decimal]
self.assertEqual(response.data, expected_data) self.assertEqual(response.data, expected_data)
@unittest.skipUnless(django_filters, 'django-filters not installed') @unittest.skipUnless(django_filters, 'django-filter not installed')
def test_incorrectly_configured_filter(self): def test_incorrectly_configured_filter(self):
""" """
An error should be displayed when the filter class is misconfigured. An error should be displayed when the filter class is misconfigured.
@ -226,7 +226,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
request = factory.get('/') request = factory.get('/')
self.assertRaises(AssertionError, view, request) self.assertRaises(AssertionError, view, request)
@unittest.skipUnless(django_filters, 'django-filters not installed') @unittest.skipUnless(django_filters, 'django-filter not installed')
def test_unknown_filter(self): def test_unknown_filter(self):
""" """
GET requests with filters that aren't configured should return 200. GET requests with filters that aren't configured should return 200.
@ -248,7 +248,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):
def _get_url(self, item): def _get_url(self, item):
return reverse('detail-view', kwargs=dict(pk=item.pk)) return reverse('detail-view', kwargs=dict(pk=item.pk))
@unittest.skipUnless(django_filters, 'django-filters not installed') @unittest.skipUnless(django_filters, 'django-filter not installed')
def test_get_filtered_detail_view(self): def test_get_filtered_detail_view(self):
""" """
GET requests to filtered RetrieveAPIView that have a filter_class set GET requests to filtered RetrieveAPIView that have a filter_class set

View File

@ -122,7 +122,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
for obj in self.objects.all() for obj in self.objects.all()
] ]
@unittest.skipUnless(django_filters, 'django-filters not installed') @unittest.skipUnless(django_filters, 'django-filter not installed')
def test_get_django_filter_paginated_filtered_root_view(self): def test_get_django_filter_paginated_filtered_root_view(self):
""" """
GET requests to paginated filtered ListCreateAPIView should return GET requests to paginated filtered ListCreateAPIView should return

View File

@ -4,7 +4,7 @@ from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING
from rest_framework.compat import guardian from rest_framework.compat import guardian, get_model_name
from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.filters import DjangoObjectPermissionsFilter
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import BasicModel from rest_framework.tests.models import BasicModel
@ -202,7 +202,7 @@ class ObjectPermissionsIntegrationTests(TestCase):
# give everyone model level permissions, as we are not testing those # give everyone model level permissions, as we are not testing those
everyone = Group.objects.create(name='everyone') everyone = Group.objects.create(name='everyone')
model_name = BasicPermModel._meta.module_name model_name = get_model_name(BasicPermModel)
app_label = BasicPermModel._meta.app_label app_label = BasicPermModel._meta.app_label
f = '{0}_{1}'.format f = '{0}_{1}'.format
perms = { perms = {

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models from django.db import models
from django.db.models.fields import BLANK_CHOICE_DASH from django.db.models.fields import BLANK_CHOICE_DASH
@ -136,6 +137,7 @@ class BasicTests(TestCase):
'Happy new year!', 'Happy new year!',
datetime.datetime(2012, 1, 1) datetime.datetime(2012, 1, 1)
) )
self.actionitem = ActionItem(title='Some to do item',)
self.data = { self.data = {
'email': 'tom@example.com', 'email': 'tom@example.com',
'content': 'Happy new year!', 'content': 'Happy new year!',
@ -264,6 +266,20 @@ class BasicTests(TestCase):
""" """
self.assertRaises(AssertionError, PersonSerializerInvalidReadOnly, []) self.assertRaises(AssertionError, PersonSerializerInvalidReadOnly, [])
def test_serializer_data_is_cleared_on_save(self):
"""
Check _data attribute is cleared on `save()`
Regression test for #1116
 id field is not populated if `data` is accessed prior to `save()`
"""
serializer = ActionItemSerializer(self.actionitem)
self.assertIsNone(serializer.data.get('id',None), 'New instance. `id` should not be set.')
serializer.save()
self.assertIsNotNone(serializer.data.get('id',None), 'Model is saved. `id` should be set.')
class DictStyleSerializer(serializers.Serializer): class DictStyleSerializer(serializers.Serializer):
""" """

View File

@ -42,6 +42,8 @@ class JSONEncoder(json.JSONEncoder):
return str(o.total_seconds()) return str(o.total_seconds())
elif isinstance(o, decimal.Decimal): elif isinstance(o, decimal.Decimal):
return str(o) return str(o)
elif hasattr(o, 'tolist'):
return o.tolist()
elif hasattr(o, '__iter__'): elif hasattr(o, '__iter__'):
return [i for i in o] return [i for i in o]
return super(JSONEncoder, self).default(o) return super(JSONEncoder, self).default(o)