Restructure project to add test support
* separated drf_swagger and testproj modules, moved both out of project root * added testing support via pytest and tox * enabled Travis CI * integrated coverage & Coveralls
This commit is contained in:
1 parent
207973ae5a
commit
2b0d80dc0f
44 files changed
+314
-14
No files matched your search
+17
@@ -0,0 +1,17 @@
|
||||
language: python
|
||||
python:
|
||||
- '3.5'
|
||||
- '3.6'
|
||||
- '3.7'
|
||||
env:
|
||||
- TOX_ENV=py35
|
||||
- TOX_ENV=py36
|
||||
- TOX_ENV=py37
|
||||
install:
|
||||
- pip install requirements_dev.txt
|
||||
before_script:
|
||||
- coverage erase
|
||||
script:
|
||||
- tox -e $TOX_ENV
|
||||
after_success:
|
||||
- coveralls
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
include README.md
|
||||
include LICENSE
|
||||
recursive-include drf_swagger/static *
|
||||
recursive-include drf_swagger/templates *
|
||||
include requirements*
|
||||
recursive-include src/drf_swagger/static *
|
||||
recursive-include src/drf_swagger/templates *
|
||||
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
DJANGO_SETTINGS_MODULE = testproj.settings
|
||||
python_paths = testproj
|
||||
@@ -1,2 +1,4 @@
|
||||
pygments>=2.2.0
|
||||
django-cors-headers>=2.1.0
|
||||
# Packages required for development and CI
|
||||
tox>=2.9.1
|
||||
tox-battery>=0.5
|
||||
python-coveralls>=2.9.1
|
||||
@@ -0,0 +1,6 @@
|
||||
# Packages required for running the tests
|
||||
pygments>=2.2.0
|
||||
django-cors-headers>=2.1.0
|
||||
pytest-django>=3.1.2
|
||||
pytest-pythonpath>=0.7.1
|
||||
pytest-cov>=2.5.1
|
||||
@@ -1,2 +1,3 @@
|
||||
# Packages required for the validation feature
|
||||
flex>=6.11.1
|
||||
swagger-spec-validator>=2.1.0
|
||||
@@ -12,16 +12,19 @@ requirements = read_req('requirements.txt')
|
||||
requirements_validation = read_req('requirements_validation.txt')
|
||||
requirements_dev = read_req('requirements_dev.txt')
|
||||
requirements_test = read_req('requirements_test.txt')
|
||||
0
|
||||
|
||||
setup(
|
||||
name='drf-swagger',
|
||||
version='1.0.0rc1',
|
||||
packages=find_packages(include=['drf_swagger']),
|
||||
packages=find_packages('src', include=['drf_swagger']),
|
||||
package_dir={'': 'src'},
|
||||
include_package_data=True,
|
||||
install_requires=requirements,
|
||||
tests_require=requirements_test,
|
||||
extras_require={
|
||||
'validation': requirements_validation
|
||||
'validation': requirements_validation,
|
||||
'test': requirements_test,
|
||||
'dev': requirements_dev,
|
||||
},
|
||||
license='BSD License',
|
||||
description='Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code.',
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,9 +1,9 @@
|
||||
from rest_framework.schemas import SchemaGenerator
|
||||
from rest_framework.schemas import SchemaGenerator as _SchemaGenerator
|
||||
|
||||
from . import openapi
|
||||
|
||||
|
||||
class OpenAPISchemaGenerator(SchemaGenerator):
|
||||
class OpenAPISchemaGenerator(_SchemaGenerator):
|
||||
def __init__(self, info, version, url=None, patterns=None, urlconf=None):
|
||||
super(OpenAPISchemaGenerator, self).__init__(info.title, url, info.description, patterns, urlconf)
|
||||
self.info = info
|
||||
File renamed without changes.
@@ -102,9 +102,9 @@ class Swagger(coreapi.Document):
|
||||
:param string version: API version string
|
||||
:return: an openapi.Swagger
|
||||
"""
|
||||
if document.title != info.title:
|
||||
if document.title and document.title != info.title:
|
||||
warnings.warn("document title is overriden by Swagger Info")
|
||||
if document.description != info.description:
|
||||
if document.description and document.description != info.description:
|
||||
warnings.warn("document description is overriden by Swagger Info")
|
||||
return Swagger(
|
||||
info=info,
|
||||
@@ -3,7 +3,7 @@ from rest_framework.renderers import BaseRenderer
|
||||
from rest_framework.utils import json
|
||||
|
||||
from .app_settings import swagger_settings, redoc_settings
|
||||
from .codec import OpenAPICodecJson, VALIDATORS, OpenAPICodecYaml
|
||||
from .codecs import OpenAPICodecJson, VALIDATORS, OpenAPICodecYaml
|
||||
|
||||
|
||||
class _SpecRenderer(BaseRenderer):
|
||||
Vendored
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -15,11 +15,15 @@ class ExampleProjectsSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class SnippetSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField(read_only=True)
|
||||
"""SnippetSerializer classdoc
|
||||
|
||||
create: docstring for create from serializer classdoc
|
||||
"""
|
||||
id = serializers.IntegerField(read_only=True, help_text="id help text")
|
||||
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
|
||||
code = serializers.CharField(style={'base_template': 'textarea.html'})
|
||||
linenos = serializers.BooleanField(required=False)
|
||||
language = LanguageSerializer()
|
||||
language = LanguageSerializer(help_text="Sample help text for language")
|
||||
style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')
|
||||
lines = serializers.ListField(child=serializers.IntegerField(), allow_empty=True, allow_null=True, required=False)
|
||||
example_projects = serializers.ListSerializer(child=ExampleProjectsSerializer())
|
||||
|
||||
@@ -4,10 +4,32 @@ from snippets.serializers import SnippetSerializer
|
||||
|
||||
|
||||
class SnippetList(generics.ListCreateAPIView):
|
||||
"""SnippetList classdoc"""
|
||||
queryset = Snippet.objects.all()
|
||||
serializer_class = SnippetSerializer
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""post method docstring"""
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
||||
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
SnippetDetail classdoc
|
||||
|
||||
put:
|
||||
put class docstring
|
||||
|
||||
patch:
|
||||
patch class docstring
|
||||
"""
|
||||
queryset = Snippet.objects.all()
|
||||
serializer_class = SnippetSerializer
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
"""patch method docstring"""
|
||||
return super().patch(request, *args, **kwargs)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
"""delete method docstring"""
|
||||
return super().patch(request, *args, **kwargs)
|
||||
@@ -0,0 +1,32 @@
|
||||
import os
|
||||
|
||||
class PytestTestRunner(object):
|
||||
"""Runs pytest to discover and run tests."""
|
||||
|
||||
def __init__(self, verbosity=1, failfast=False, keepdb=False, **kwargs):
|
||||
self.verbosity = verbosity
|
||||
self.failfast = failfast
|
||||
self.keepdb = keepdb
|
||||
|
||||
def run_tests(self, test_labels):
|
||||
"""Run pytest and return the exitcode.
|
||||
|
||||
It translates some of Django's test command option to pytest's.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
argv = []
|
||||
if self.verbosity == 0:
|
||||
argv.append('--quiet')
|
||||
if self.verbosity == 2:
|
||||
argv.append('--verbose')
|
||||
if self.verbosity == 3:
|
||||
argv.append('-vv')
|
||||
if self.failfast:
|
||||
argv.append('--exitfirst')
|
||||
if self.keepdb:
|
||||
argv.append('--reuse-db')
|
||||
|
||||
argv.extend(test_labels)
|
||||
os.chdir('..')
|
||||
return pytest.main(argv)
|
||||
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
|
||||
import sys
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@@ -115,3 +117,5 @@ USE_TZ = True
|
||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
TEST_RUNNER = 'testproj.runner.PytestTestRunner'
|
||||
@@ -0,0 +1,91 @@
|
||||
import json
|
||||
|
||||
from django.test import TestCase
|
||||
from ruamel import yaml
|
||||
|
||||
from drf_swagger import openapi, codecs
|
||||
from drf_swagger.generators import OpenAPISchemaGenerator
|
||||
|
||||
|
||||
class SchemaGeneratorTest(TestCase):
|
||||
def setUp(self):
|
||||
self.generator = OpenAPISchemaGenerator(
|
||||
info=openapi.Info("Test generator", "v1"),
|
||||
version="v2",
|
||||
)
|
||||
self.codec_json = codecs.OpenAPICodecJson(['flex', 'ssv'])
|
||||
self.codec_yaml = codecs.OpenAPICodecYaml(['ssv', 'flex'])
|
||||
|
||||
def _validate_schema(self, swagger):
|
||||
from flex.core import parse as validate_flex
|
||||
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
|
||||
|
||||
validate_flex(swagger)
|
||||
validate_ssv(swagger)
|
||||
|
||||
def test_schema_generates_without_errors(self):
|
||||
self.generator.get_schema(None, True)
|
||||
|
||||
def test_schema_is_valid(self):
|
||||
swagger = self.generator.get_schema(None, True)
|
||||
self.codec_yaml.encode(swagger)
|
||||
|
||||
def test_invalid_schema_fails(self):
|
||||
bad_generator = OpenAPISchemaGenerator(
|
||||
info=openapi.Info(
|
||||
"Test generator", "v1",
|
||||
contact=openapi.Contact(name=69, email=[])
|
||||
),
|
||||
version="v2",
|
||||
)
|
||||
|
||||
swagger = bad_generator.get_schema(None, True)
|
||||
with self.assertRaises(codecs.SwaggerValidationError):
|
||||
self.codec_json.encode(swagger)
|
||||
|
||||
def test_json_codec_roundtrip(self):
|
||||
swagger = self.generator.get_schema(None, True)
|
||||
json_bytes = self.codec_json.encode(swagger)
|
||||
self._validate_schema(json.loads(json_bytes.decode('utf-8')))
|
||||
|
||||
def test_yaml_codec_roundtrip(self):
|
||||
swagger = self.generator.get_schema(None, True)
|
||||
json_bytes = self.codec_yaml.encode(swagger)
|
||||
self._validate_schema(yaml.safe_load(json_bytes.decode('utf-8')))
|
||||
|
||||
|
||||
class SchemaTest(TestCase):
|
||||
def setUp(self):
|
||||
self.generator = OpenAPISchemaGenerator(
|
||||
info=openapi.Info("Test generator", "v1"),
|
||||
version="v2",
|
||||
)
|
||||
self.codec_json = codecs.OpenAPICodecJson(['flex', 'ssv'])
|
||||
self.codec_yaml = codecs.OpenAPICodecYaml(['ssv', 'flex'])
|
||||
|
||||
self.swagger = self.generator.get_schema(None, True)
|
||||
json_bytes = self.codec_yaml.encode(self.swagger)
|
||||
self.swagger_dict = yaml.safe_load(json_bytes.decode('utf-8'))
|
||||
|
||||
def test_paths_not_empty(self):
|
||||
self.assertTrue(bool(self.swagger_dict['paths']))
|
||||
|
||||
def test_appropriate_status_codes(self):
|
||||
snippets_list = self.swagger_dict['paths']['/snippets/']
|
||||
self.assertTrue('200' in snippets_list['get']['responses'])
|
||||
self.assertTrue('201' in snippets_list['post']['responses'])
|
||||
snippets_detail = self.swagger_dict['paths']['/snippets/{id}/']
|
||||
self.assertTrue('200' in snippets_detail['get']['responses'])
|
||||
self.assertTrue('200' in snippets_detail['put']['responses'])
|
||||
self.assertTrue('200' in snippets_detail['patch']['responses'])
|
||||
self.assertTrue('204' in snippets_detail['delete']['responses'])
|
||||
|
||||
def test_operation_docstrings(self):
|
||||
snippets_list = self.swagger_dict['paths']['/snippets/']
|
||||
self.assertEqual(snippets_list['get']['description'], "SnippetList classdoc")
|
||||
self.assertEqual(snippets_list['post']['description'], "post method docstring")
|
||||
snippets_detail = self.swagger_dict['paths']['/snippets/{id}/']
|
||||
self.assertEqual(snippets_detail['get']['description'], "SnippetDetail classdoc")
|
||||
self.assertEqual(snippets_detail['put']['description'], "put class docstring")
|
||||
self.assertEqual(snippets_detail['patch']['description'], "patch method docstring")
|
||||
self.assertEqual(snippets_detail['delete']['description'], "delete method docstring")
|
||||
@@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
from ruamel import yaml
|
||||
|
||||
from drf_swagger import openapi, codecs
|
||||
from drf_swagger.generators import OpenAPISchemaGenerator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def generator():
|
||||
return OpenAPISchemaGenerator(
|
||||
info=openapi.Info("Test generator", "v1"),
|
||||
version="v2",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def codec_json():
|
||||
return codecs.OpenAPICodecJson(['flex', 'ssv'])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def codec_yaml():
|
||||
return codecs.OpenAPICodecYaml(['ssv', 'flex'])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def swagger_dict():
|
||||
swagger = generator().get_schema(None, True)
|
||||
json_bytes = codec_yaml().encode(swagger)
|
||||
return yaml.safe_load(json_bytes.decode('utf-8'))
|
||||
@@ -0,0 +1,51 @@
|
||||
import json
|
||||
|
||||
from ruamel import yaml
|
||||
|
||||
from drf_swagger import openapi, codecs
|
||||
from drf_swagger.generators import OpenAPISchemaGenerator
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def validate_schema(swagger):
|
||||
from flex.core import parse as validate_flex
|
||||
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
|
||||
|
||||
validate_flex(swagger)
|
||||
validate_ssv(swagger)
|
||||
|
||||
|
||||
def test_schema_generates_without_errors(generator):
|
||||
generator.get_schema(None, True)
|
||||
|
||||
|
||||
def test_schema_is_valid(generator, codec_yaml):
|
||||
swagger = generator.get_schema(None, True)
|
||||
codec_yaml.encode(swagger)
|
||||
|
||||
|
||||
def test_invalid_schema_fails(codec_json):
|
||||
bad_generator = OpenAPISchemaGenerator(
|
||||
info=openapi.Info(
|
||||
"Test generator", "v1",
|
||||
contact=openapi.Contact(name=69, email=[])
|
||||
),
|
||||
version="v2",
|
||||
)
|
||||
|
||||
swagger = bad_generator.get_schema(None, True)
|
||||
with pytest.raises(codecs.SwaggerValidationError):
|
||||
codec_json.encode(swagger)
|
||||
|
||||
|
||||
def test_json_codec_roundtrip(codec_json, generator):
|
||||
swagger = generator.get_schema(None, True)
|
||||
json_bytes = codec_json.encode(swagger)
|
||||
validate_schema(json.loads(json_bytes.decode('utf-8')))
|
||||
|
||||
|
||||
def test_yaml_codec_roundtrip(codec_yaml, generator):
|
||||
swagger = generator.get_schema(None, True)
|
||||
json_bytes = codec_yaml.encode(swagger)
|
||||
validate_schema(yaml.safe_load(json_bytes.decode('utf-8')))
|
||||
@@ -0,0 +1,24 @@
|
||||
def test_paths_not_empty(swagger_dict):
|
||||
assert bool(swagger_dict['paths'])
|
||||
|
||||
|
||||
def test_appropriate_status_codes(swagger_dict):
|
||||
snippets_list = swagger_dict['paths']['/snippets/']
|
||||
assert '200' in snippets_list['get']['responses']
|
||||
assert '201' in snippets_list['post']['responses']
|
||||
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
|
||||
assert '200' in snippets_detail['get']['responses']
|
||||
assert '200' in snippets_detail['put']['responses']
|
||||
assert '200' in snippets_detail['patch']['responses']
|
||||
assert '204' in snippets_detail['delete']['responses']
|
||||
|
||||
|
||||
def test_operation_docstrings(swagger_dict):
|
||||
snippets_list = swagger_dict['paths']['/snippets/']
|
||||
assert snippets_list['get']['description'] == "SnippetList classdoc"
|
||||
assert snippets_list['post']['description'] == "post method docstring"
|
||||
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
|
||||
assert snippets_detail['get']['description'] == "SnippetDetail classdoc"
|
||||
assert snippets_detail['put']['description'] == "put class docstring"
|
||||
assert snippets_detail['patch']['description'] == "patch method docstring"
|
||||
assert snippets_detail['delete']['description'] == "delete method docstring"
|
||||
Reference in new issue
Block a user