Merge branch 'master' into htmlr

This commit is contained in:
Asif Saifuddin Auvi 2017-01-14 12:55:10 +06:00 committed by GitHub
commit 752cf7b41d
25 changed files with 231 additions and 52 deletions

View File

@ -21,6 +21,7 @@ env:
- TOX_ENV=py27-djangomaster
- TOX_ENV=py34-djangomaster
- TOX_ENV=py35-djangomaster
- TOX_ENV=py36-djangomaster
matrix:
fast_finish: true
@ -28,10 +29,11 @@ matrix:
- env: TOX_ENV=py27-djangomaster
- env: TOX_ENV=py34-djangomaster
- env: TOX_ENV=py35-djangomaster
- env: TOX_ENV=py36-djangomaster
install:
# Virtualenv < 14 is required to keep the Python 3.2 builds running.
- pip install tox "virtualenv<14"
- pip install tox virtualenv
script:
- tox -e $TOX_ENV

View File

@ -113,7 +113,7 @@ The following third party packages provide additional metadata implementations.
[drf-schema-adapter][drf-schema-adapter] is a set of tools that makes it easier to provide schema information to frontend frameworks and libraries. It provides a metadata mixin as well as 2 metadata classes and several adapters suitable to generate [json-schema][json-schema] as well as schema information readable by various libraries.
You can also write your own adapter to work with your specific frontend.
If you whish to do so, it also provides an exporter that can export those schema information to json files.
If you wish to do so, it also provides an exporter that can export those schema information to json files.
[cite]: http://tools.ietf.org/html/rfc7231#section-4.3.7
[no-options]: https://www.mnot.net/blog/2012/10/29/NO_OPTIONS

View File

@ -259,7 +259,7 @@ The [REST Condition][rest-condition] package is another extension for building c
## DRY Rest Permissions
The [DRY Rest Permissions][dry-rest-permissions] package provides the ability to define different permissions for individual default and custom actions. This package is made for apps with permissions that are derived from relationships defined in the app's data model. It also supports permission checks being returned to a client app through the API's serializer. Additionally it supports adding permissions to the default and custom list actions to restrict the data they retrive per user.
The [DRY Rest Permissions][dry-rest-permissions] package provides the ability to define different permissions for individual default and custom actions. This package is made for apps with permissions that are derived from relationships defined in the app's data model. It also supports permission checks being returned to a client app through the API's serializer. Additionally it supports adding permissions to the default and custom list actions to restrict the data they retrieve per user.
## Django Rest Framework Roles

View File

@ -241,7 +241,7 @@ Default:
If set, this maps the `'pk'` identifier in the URL conf onto the actual field
name when generating a schema path parameter. Typically this will be `'id'`.
This gives a more suitable representation as "primary key" is an implementation
detail, wheras "identifier" is a more general concept.
detail, whereas "identifier" is a more general concept.
Default: `True`

View File

@ -153,7 +153,7 @@ For more information, see the documentation on [customizing field mappings][cust
We've now moved a number of packages out of the core of REST framework, and into separately installable packages. If you're currently using these you don't need to worry, you simply need to `pip install` the new packages, and change any import paths.
We're making this change in order to help distribute the maintainance workload, and keep better focus of the core essentials of the framework.
We're making this change in order to help distribute the maintenance workload, and keep better focus of the core essentials of the framework.
The change also means we can be more flexible with which external packages we recommend. For example, the excellently maintained [Django OAuth toolkit](https://github.com/evonove/django-oauth-toolkit) has now been promoted as our recommended option for integrating OAuth support.

View File

@ -89,7 +89,7 @@ Name | Support | PyPI pa
---------------------------------|-------------------------------------|--------------------------------
[Core JSON][core-json] | Schema generation & client support. | Built-in support in `coreapi`.
[Swagger / OpenAPI][swagger] | Schema generation & client support. | The `openapi-codec` package.
[JSON Hyper-Schema][hyperschema] | Currrently client support only. | The `hyperschema-codec` package.
[JSON Hyper-Schema][hyperschema] | Currently client support only. | The `hyperschema-codec` package.
[API Blueprint][api-blueprint] | Not yet available. | Not yet available.
---

View File

@ -217,7 +217,7 @@ credentials, headers and bookmarks:
# Python client library
The `coreapi` Python package allows you to programatically interact with any
The `coreapi` Python package allows you to programmatically interact with any
API that exposes a supported schema format.
## Getting started

View File

@ -102,7 +102,7 @@ Our gold sponsors include companies large and small. Many thanks for their signi
### Silver sponsors
The serious financial contribution that our silver sponsors have made is very much appreciated. I'd like to say a particular thank&nbsp;you to individuals who have choosen to privately support the project at this level.
The serious financial contribution that our silver sponsors have made is very much appreciated. I'd like to say a particular thank&nbsp;you to individuals who have chosen to privately support the project at this level.
<ul class="sponsor silver">
<li><a href="http://www.imtapps.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-imt_computer_services.png);">IMT Computer Services</a></li>

View File

@ -228,7 +228,7 @@ You can determine your currently installed version using `pip freeze`:
* Fixed use of deprecated Query.aggregates. ([#4003][gh4003])
* Fix blank lines around docstrings. ([#4002][gh4002])
* Fixed admin pagination when limit is 0. ([#3990][gh3990])
* OrderingFilter adjustements. ([#3983][gh3983])
* OrderingFilter adjustments. ([#3983][gh3983])
* Non-required serializer related fields. ([#3976][gh3976])
* Using safer calling way of "@api_view" in tutorial. ([#3971][gh3971])
* ListSerializer doesn't handle unique_together constraints. ([#3970][gh3970])

View File

@ -62,7 +62,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be
That's looking good. Again, it's still pretty similar to the function based view right now.
We'll also need to refactor our `urls.py` slightly now we're using class-based views.
We'll also need to refactor our `urls.py` slightly now that we're using class-based views.
from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns

View File

@ -1,4 +1,4 @@
# PyTest for running the tests.
pytest==2.9.1
pytest-django==2.9.1
pytest-cov==1.8.1
pytest==3.0.5
pytest-django==3.1.2
pytest-cov==2.4.0

View File

@ -149,7 +149,7 @@ def to_choices_dict(choices):
# choices = [('Category', ((1, 'First'), (2, 'Second'))), (3, 'Third')]
ret = OrderedDict()
for choice in choices:
if (not isinstance(choice, (list, tuple))):
if not isinstance(choice, (list, tuple)):
# single choice
ret[choice] = choice
else:
@ -1614,7 +1614,7 @@ class JSONField(Field):
def get_value(self, dictionary):
if html.is_html_input(dictionary) and self.field_name in dictionary:
# When HTML form input is used, mark up the input
# as being a JSON string, rather than a JSON primative.
# as being a JSON string, rather than a JSON primitive.
class JSONString(six.text_type):
def __new__(self, value):
ret = six.text_type.__new__(self, value)

View File

@ -131,7 +131,7 @@ def _reverse_ordering(ordering_tuple):
ordering and return a new tuple, eg. `('created', '-uuid')`.
"""
def invert(x):
return x[1:] if (x.startswith('-')) else '-' + x
return x[1:] if x.startswith('-') else '-' + x
return tuple([invert(item) for item in ordering_tuple])

View File

@ -503,7 +503,7 @@ class ManyRelatedField(Field):
return []
relationship = get_attribute(instance, self.source_attrs)
return relationship.all() if (hasattr(relationship, 'all')) else relationship
return relationship.all() if hasattr(relationship, 'all') else relationship
def to_representation(self, iterable):
return [

View File

@ -228,7 +228,7 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
renderer_context = renderer_context or {}
response = renderer_context['response']
response = renderer_context.get('response')
if response and response.exception:
request = renderer_context['request']
@ -540,7 +540,7 @@ class BrowsableAPIRenderer(BaseRenderer):
# If possible, serialize the initial content for the generic form
default_parser = view.parser_classes[0]
renderer_class = getattr(default_parser, 'renderer_class', None)
if (hasattr(view, 'get_serializer') and renderer_class):
if hasattr(view, 'get_serializer') and renderer_class:
# View has a serializer defined and parser class has a
# corresponding renderer that can be used to render the data.
@ -598,7 +598,7 @@ class BrowsableAPIRenderer(BaseRenderer):
paginator = getattr(view, 'paginator', None)
if isinstance(data, list):
pass
elif (paginator is not None and data is not None):
elif paginator is not None and data is not None:
try:
paginator.get_results(data)
except (TypeError, KeyError):
@ -738,7 +738,7 @@ class AdminRenderer(BrowsableAPIRenderer):
ret = template_render(template, context, request=renderer_context['request'])
# Creation and deletion should use redirects in the admin style.
if (response.status_code == status.HTTP_201_CREATED) and ('Location' in response):
if response.status_code == status.HTTP_201_CREATED and 'Location' in response:
response.status_code = status.HTTP_303_SEE_OTHER
response['Location'] = request.build_absolute_uri()
ret = ''
@ -764,7 +764,7 @@ class AdminRenderer(BrowsableAPIRenderer):
)
paginator = getattr(context['view'], 'paginator', None)
if (paginator is not None and data is not None):
if paginator is not None and data is not None:
try:
results = paginator.get_results(data)
except (TypeError, KeyError):

View File

@ -152,7 +152,7 @@ class Request(object):
force_user = getattr(request, '_force_auth_user', None)
force_token = getattr(request, '_force_auth_token', None)
if (force_user is not None or force_token is not None):
if force_user is not None or force_token is not None:
forced_auth = ForcedAuthentication(force_user, force_token)
self.authenticators = (forced_auth,)

View File

@ -1177,7 +1177,7 @@ class ModelSerializer(Serializer):
if postgres_fields and isinstance(model_field, postgres_fields.ArrayField):
# Populate the `child` argument on `ListField` instances generated
# for the PostgrSQL specfic `ArrayField`.
# for the PostgreSQL specific `ArrayField`.
child_model_field = model_field.base_field
child_field_class, child_field_kwargs = self.build_standard_field(
'child', child_model_field

View File

@ -117,7 +117,7 @@ class NamespaceVersioning(BaseVersioning):
def determine_version(self, request, *args, **kwargs):
resolver_match = getattr(request, 'resolver_match', None)
if (resolver_match is None or not resolver_match.namespace):
if resolver_match is None or not resolver_match.namespace:
return self.default_version
# Allow for possibly nested namespaces.

View File

@ -2,6 +2,7 @@ from datetime import date, datetime, timedelta, tzinfo
from decimal import Decimal
from uuid import uuid4
import pytest
from django.test import TestCase
from rest_framework.compat import coreapi
@ -57,7 +58,7 @@ class JSONEncoderTests(TestCase):
current_time = datetime.now().time()
current_time = current_time.replace(tzinfo=UTC())
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
self.encoder.default(current_time)
def test_encode_date(self):
@ -85,8 +86,8 @@ class JSONEncoderTests(TestCase):
"""
Tests encoding a coreapi objects raises proper error
"""
with self.assertRaises(RuntimeError):
with pytest.raises(RuntimeError):
self.encoder.default(coreapi.Document())
with self.assertRaises(RuntimeError):
with pytest.raises(RuntimeError):
self.encoder.default(coreapi.Error())

View File

@ -268,7 +268,7 @@ class TestSimpleMetadataFieldInfo(TestCase):
def test_null_boolean_field_info_type(self):
options = metadata.SimpleMetadata()
field_info = options.get_field_info(serializers.NullBooleanField())
self.assertEqual(field_info['type'], 'boolean')
assert field_info['type'] == 'boolean'
def test_related_field_choices(self):
options = metadata.SimpleMetadata()
@ -277,7 +277,7 @@ class TestSimpleMetadataFieldInfo(TestCase):
field_info = options.get_field_info(
serializers.RelatedField(queryset=BasicModel.objects.all())
)
self.assertNotIn('choices', field_info)
assert 'choices' not in field_info
class TestModelSerializerMetadata(TestCase):

View File

@ -35,7 +35,7 @@ class TestFormParser(TestCase):
stream = StringIO(self.string)
data = parser.parse(stream)
self.assertEqual(Form(data).is_valid(), True)
assert Form(data).is_valid() is True
class TestFileUploadParser(TestCase):
@ -62,7 +62,7 @@ class TestFileUploadParser(TestCase):
self.stream.seek(0)
data_and_files = parser.parse(self.stream, None, self.parser_context)
file_obj = data_and_files.files['file']
self.assertEqual(file_obj._size, 14)
assert file_obj._size == 14
def test_parse_missing_filename(self):
"""
@ -108,22 +108,22 @@ class TestFileUploadParser(TestCase):
def test_get_filename(self):
parser = FileUploadParser()
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'file.txt')
assert filename == 'file.txt'
def test_get_encoded_filename(self):
parser = FileUploadParser()
self.__replace_content_disposition('inline; filename*=utf-8\'\'ÀĥƦ.txt')
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'ÀĥƦ.txt')
assert filename == 'ÀĥƦ.txt'
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, 'ÀĥƦ.txt')
assert filename == 'ÀĥƦ.txt'
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'en-us\'ÀĥƦ.txt')
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'ÀĥƦ.txt')
assert filename == 'ÀĥƦ.txt'
def __replace_content_disposition(self, disposition):
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition

View File

@ -5,9 +5,11 @@ import json
import re
from collections import MutableMapping, OrderedDict
import pytest
from django.conf.urls import include, url
from django.core.cache import cache
from django.db import models
from django.http.request import HttpRequest
from django.test import TestCase, override_settings
from django.utils import six
from django.utils.safestring import SafeText
@ -15,8 +17,10 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import permissions, serializers, status
from rest_framework.renderers import (
BaseRenderer, BrowsableAPIRenderer, HTMLFormRenderer, JSONRenderer
AdminRenderer, BaseRenderer, BrowsableAPIRenderer,
HTMLFormRenderer, JSONRenderer, StaticHTMLRenderer
)
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
@ -269,6 +273,18 @@ def strip_trailing_whitespace(content):
return re.sub(' +\n', '\n', content)
class BaseRendererTests(TestCase):
"""
Tests BaseRenderer
"""
def test_render_raise_error(self):
"""
BaseRenderer.render should raise NotImplementedError
"""
with pytest.raises(NotImplementedError):
BaseRenderer().render('test')
class JSONRendererTests(TestCase):
"""
Tests specific to the JSON Renderer
@ -568,3 +584,67 @@ class TestMultipleChoiceFieldHTMLFormRenderer(TestCase):
result)
self.assertInHTML('<option value="1">Option1</option>', result)
self.assertInHTML('<option value="2">Option2</option>', result)
class StaticHTMLRendererTests(TestCase):
"""
Tests specific for Static HTML Renderer
"""
def setUp(self):
self.renderer = StaticHTMLRenderer()
def test_static_renderer(self):
data = '<html><body>text</body></html>'
result = self.renderer.render(data)
assert result == data
def test_static_renderer_with_exception(self):
context = {
'response': Response(status=500, exception=True),
'request': Request(HttpRequest())
}
result = self.renderer.render({}, renderer_context=context)
assert result == '500 Internal Server Error'
class BrowsableAPIRendererTests(TestCase):
def setUp(self):
self.renderer = BrowsableAPIRenderer()
def test_get_description_returns_empty_string_for_401_and_403_statuses(self):
assert self.renderer.get_description({}, status_code=401) == ''
assert self.renderer.get_description({}, status_code=403) == ''
def test_get_filter_form_returns_none_if_data_is_not_list_instance(self):
class DummyView(object):
get_queryset = None
filter_backends = None
result = self.renderer.get_filter_form(data='not list',
view=DummyView(), request={})
assert result is None
class AdminRendererTests(TestCase):
def setUp(self):
self.renderer = AdminRenderer()
def test_render_when_resource_created(self):
class DummyView(APIView):
renderer_classes = (AdminRenderer, )
request = Request(HttpRequest())
request.build_absolute_uri = lambda: 'http://example.com'
response = Response(status=201, headers={'Location': '/test'})
context = {
'view': DummyView(),
'request': request,
'response': response
}
result = self.renderer.render(data={'test': 'test'},
renderer_context=context)
assert result == ''
assert response.status_code == status.HTTP_303_SEE_OTHER
assert response['Location'] == 'http://example.com'

View File

@ -44,9 +44,9 @@ class BulkCreateSerializerTests(TestCase):
]
serializer = self.BookSerializer(data=data, many=True)
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(serializer.validated_data, data)
self.assertEqual(serializer.errors, [])
assert serializer.is_valid() is True
assert serializer.validated_data == data
assert serializer.errors == []
def test_bulk_create_errors(self):
"""
@ -75,9 +75,9 @@ class BulkCreateSerializerTests(TestCase):
]
serializer = self.BookSerializer(data=data, many=True)
self.assertEqual(serializer.is_valid(), False)
self.assertEqual(serializer.errors, expected_errors)
self.assertEqual(serializer.validated_data, [])
assert serializer.is_valid() is False
assert serializer.errors == expected_errors
assert serializer.validated_data == []
def test_invalid_list_datatype(self):
"""
@ -85,7 +85,7 @@ class BulkCreateSerializerTests(TestCase):
"""
data = ['foo', 'bar', 'baz']
serializer = self.BookSerializer(data=data, many=True)
self.assertEqual(serializer.is_valid(), False)
assert serializer.is_valid() is False
text_type_string = six.text_type.__name__
message = 'Invalid data. Expected a dictionary, but got %s.' % text_type_string
@ -95,7 +95,7 @@ class BulkCreateSerializerTests(TestCase):
{'non_field_errors': [message]}
]
self.assertEqual(serializer.errors, expected_errors)
assert serializer.errors == expected_errors
def test_invalid_single_datatype(self):
"""
@ -103,11 +103,11 @@ class BulkCreateSerializerTests(TestCase):
"""
data = 123
serializer = self.BookSerializer(data=data, many=True)
self.assertEqual(serializer.is_valid(), False)
assert serializer.is_valid() is False
expected_errors = {'non_field_errors': ['Expected a list of items but got type "int".']}
self.assertEqual(serializer.errors, expected_errors)
assert serializer.errors == expected_errors
def test_invalid_single_object(self):
"""
@ -120,8 +120,8 @@ class BulkCreateSerializerTests(TestCase):
'author': 'Tom Wolfe'
}
serializer = self.BookSerializer(data=data, many=True)
self.assertEqual(serializer.is_valid(), False)
assert serializer.is_valid() is False
expected_errors = {'non_field_errors': ['Expected a list of items but got type "dict".']}
self.assertEqual(serializer.errors, expected_errors)
assert serializer.errors == expected_errors

View File

@ -3,15 +3,20 @@ Tests for the throttling implementations in the permissions module.
"""
from __future__ import unicode_literals
import pytest
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest
from django.test import TestCase
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
from rest_framework.test import APIRequestFactory, force_authenticate
from rest_framework.throttling import (
BaseThrottle, ScopedRateThrottle, UserRateThrottle
AnonRateThrottle, BaseThrottle, ScopedRateThrottle, SimpleRateThrottle,
UserRateThrottle
)
from rest_framework.views import APIView
@ -189,6 +194,8 @@ class ScopedRateThrottleTests(TestCase):
"""
def setUp(self):
self.throttle = ScopedRateThrottle()
class XYScopedRateThrottle(ScopedRateThrottle):
TIMER_SECONDS = 0
THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
@ -288,6 +295,18 @@ class ScopedRateThrottleTests(TestCase):
response = self.unscoped_view(request)
assert response.status_code == 200
def test_get_cache_key_returns_correct_key_if_user_is_authenticated(self):
class DummyView(object):
throttle_scope = 'user'
request = Request(HttpRequest())
user = User.objects.create(username='test')
force_authenticate(request, user)
request.user = user
self.throttle.allow_request(request, DummyView())
cache_key = self.throttle.get_cache_key(request, view=DummyView())
assert cache_key == 'throttle_user_%s' % user.pk
class XffTestingBase(TestCase):
def setUp(self):
@ -354,3 +373,79 @@ class XffUniqueMachinesTest(XffTestingBase):
self.view(self.request)
self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 7.7.7.7, 2.2.2.2'
assert self.view(self.request).status_code == 200
class BaseThrottleTests(TestCase):
def test_allow_request_raises_not_implemented_error(self):
with pytest.raises(NotImplementedError):
BaseThrottle().allow_request(request={}, view={})
class SimpleRateThrottleTests(TestCase):
def setUp(self):
SimpleRateThrottle.scope = 'anon'
def test_get_rate_raises_error_if_scope_is_missing(self):
throttle = SimpleRateThrottle()
with pytest.raises(ImproperlyConfigured):
throttle.scope = None
throttle.get_rate()
def test_throttle_raises_error_if_rate_is_missing(self):
SimpleRateThrottle.scope = 'invalid scope'
with pytest.raises(ImproperlyConfigured):
SimpleRateThrottle()
def test_parse_rate_returns_tuple_with_none_if_rate_not_provided(self):
rate = SimpleRateThrottle().parse_rate(None)
assert rate == (None, None)
def test_allow_request_returns_true_if_rate_is_none(self):
assert SimpleRateThrottle().allow_request(request={}, view={}) is True
def test_get_cache_key_raises_not_implemented_error(self):
with pytest.raises(NotImplementedError):
SimpleRateThrottle().get_cache_key({}, {})
def test_allow_request_returns_true_if_key_is_none(self):
throttle = SimpleRateThrottle()
throttle.rate = 'some rate'
throttle.get_cache_key = lambda *args: None
assert throttle.allow_request(request={}, view={}) is True
def test_wait_returns_correct_waiting_time_without_history(self):
throttle = SimpleRateThrottle()
throttle.num_requests = 1
throttle.duration = 60
throttle.history = []
waiting_time = throttle.wait()
assert isinstance(waiting_time, float)
assert waiting_time == 30.0
def test_wait_returns_none_if_there_are_no_available_requests(self):
throttle = SimpleRateThrottle()
throttle.num_requests = 1
throttle.duration = 60
throttle.now = throttle.timer()
throttle.history = [throttle.timer() for _ in range(3)]
assert throttle.wait() is None
class AnonRateThrottleTests(TestCase):
def setUp(self):
self.throttle = AnonRateThrottle()
def test_authenticated_user_not_affected(self):
request = Request(HttpRequest())
user = User.objects.create(username='test')
force_authenticate(request, user)
request.user = user
assert self.throttle.get_cache_key(request, view={}) is None
def test_get_cache_key_returns_correct_value(self):
request = Request(HttpRequest())
cache_key = self.throttle.get_cache_key(request, view={})
assert cache_key == 'throttle_anon_None'

View File

@ -7,7 +7,7 @@ envlist =
{py27,py33,py34,py35}-django18,
{py27,py34,py35}-django19,
{py27,py34,py35}-django110,
{py27,py34,py35}-django{master}
{py27,py34,py35,py36}-django{master}
[testenv]
commands = ./runtests.py --fast {posargs} --coverage -rw
@ -22,6 +22,7 @@ deps =
-rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt
basepython =
py36: python3.6
py35: python3.5
py34: python3.4
py33: python3.3