diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..98ce26a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python +python: + - "2.7" +install: + - pip install -r requirements-test.txt + - pip install pytest-cov + - pip install coveralls +script: + py.test --cov-report= --cov=rest_framework_ccbv tests/ +after_success: + coveralls diff --git a/README.md b/README.md index e8542de..957649d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Classy Django REST Framework. +# Classy Django REST Framework. [![Build Status](https://travis-ci.org/vintasoftware/cdrf.co.svg?branch=develop)](https://travis-ci.org/vintasoftware/cdrf.co) [![Coverage Status](https://coveralls.io/repos/github/vintasoftware/cdrf.co/badge.svg?branch=develop)](https://coveralls.io/github/vintasoftware/cdrf.co?branch=develop) ## What is this? diff --git a/build.ini b/build.ini index 443864e..abeffb6 100644 --- a/build.ini +++ b/build.ini @@ -1,6 +1,6 @@ [tox] skipsdist=True -envlist = drf{21,22,23,24,30,31,32,33},drfbuild{21,22,23,24,30,31,32,33} +envlist = drf{21,22,23,24,30,31,32,33,34},drfbuild{21,22,23,24,30,31,32,33,34} [testenv] deps= @@ -29,10 +29,15 @@ deps32 = deps33 = djangorestframework>=3.3,<3.4 +deps34 = + djangorestframework>=3.4,<3.5 + setenv = PYTHONPATH = {toxinidir} +# INDEX GENERATION + [index] commands = fab index_generator_for_version @@ -97,6 +102,15 @@ deps = commands = {[index]commands} +[testenv:drf34] +deps = + {[testenv]deps} + {[testenv]deps34} +commands = + {[index]commands} + + +# SITE GENERATION [testenv:drfbuild21] deps = @@ -161,3 +175,11 @@ envdir = {toxworkdir}/drf33 commands = {[build]commands} + +[testenv:drfbuild34] +deps = + {[testenv:drf34]deps} +envdir = + {toxworkdir}/drf34 +commands = + {[build]commands} diff --git a/requirements-test.txt b/requirements-test.txt index ffdefdb..d52b662 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,4 @@ -r requirements-tox.txt -djangorestframework==3.1 +djangorestframework==3.3 +mock==2.0.0 pytest==2.6.4 diff --git a/requirements-tox.txt b/requirements-tox.txt index 7b33ee4..82aafbd 100644 --- a/requirements-tox.txt +++ b/requirements-tox.txt @@ -1,5 +1,6 @@ -Django==1.7.5 +Django==1.8.13 Fabric==1.10.1 Jinja2==2.7.3 Pygments==2.0.2 python-decouple==2.3 +pycrypto==2.6.1 diff --git a/requirements.txt b/requirements.txt index ce78bb2..85c6df5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Fabric==1.10.1 python-decouple==2.3 +pycrypto==2.6.1 tox==1.9.0 diff --git a/rest_framework_ccbv/config.py b/rest_framework_ccbv/config.py index 15f0250..eeeebaf 100644 --- a/rest_framework_ccbv/config.py +++ b/rest_framework_ccbv/config.py @@ -10,6 +10,7 @@ '3.1', '3.2', '3.3', + '3.4', ] diff --git a/rest_framework_ccbv/custom_formatter.py b/rest_framework_ccbv/custom_formatter.py new file mode 100644 index 0000000..73d6903 --- /dev/null +++ b/rest_framework_ccbv/custom_formatter.py @@ -0,0 +1,87 @@ +import inspect +from collections import deque + +from pygments.formatters import HtmlFormatter +from pygments.formatters.html import _escape_html_table +from pygments.token import Token + + +class CodeHtmlFormatter(HtmlFormatter): + def __init__(self, instance_class, *args, **kwargs): + self.instance_class = instance_class + super(CodeHtmlFormatter, self).__init__(*args, **kwargs) + + def _format_lines(self, tokensource): + """ + Just format the tokens, without any wrapping tags. + Yield individual lines. + + most of this code is extracted from HtmlFormatter. Sadly there was no easy + way to reuse with the changes we needed without copy pasting. + """ + nocls = self.noclasses + lsep = self.lineseparator + # for lookup only + getcls = self.ttype2class.get + c2s = self.class2style + escape_table = _escape_html_table + lookback = deque() + instance_class = self.instance_class + + lspan = '' + line = '' + for ttype, value in tokensource: + lookback.append((ttype, value)) + if nocls: + cclass = getcls(ttype) + while cclass is None: + ttype = ttype.parent + cclass = getcls(ttype) + cspan = cclass and '' % c2s[cclass][0] or '' + else: + cls = self._get_css_class(ttype) + cspan = cls and '' % cls or '' + + parts = value.translate(escape_table).split('\n') + + # check if we are dealing with a method + if ttype in Token.Name and lookback[-2][1] == '.' and lookback[-3][1] == 'self': + try: + is_method = inspect.ismethod(getattr(instance_class, value)) + except AttributeError: + # This means it's an attribute that is not in the instance_class + pass + else: + if is_method: + parts[0] = "%s" % \ + (value, parts[0]) + parts[-1] = parts[-1] + "" + + # for all but the last line + for part in parts[:-1]: + if line: + if lspan != cspan: + line += (lspan and '') + cspan + part + \ + (cspan and '') + lsep + else: # both are the same + line += part + (lspan and '') + lsep + yield 1, line + line = '' + elif part: + yield 1, cspan + part + (cspan and '') + lsep + else: + yield 1, lsep + # for the last line + if line and parts[-1]: + if lspan != cspan: + line += (lspan and '') + cspan + parts[-1] + lspan = cspan + else: + line += parts[-1] + elif parts[-1]: + line = cspan + parts[-1] + lspan = cspan + # else we neither have to open a new span nor set lspan + + if line: + yield 1, line + (lspan and '') + lsep diff --git a/rest_framework_ccbv/django_config.py b/rest_framework_ccbv/django_config.py index 5db3a75..740cd64 100644 --- a/rest_framework_ccbv/django_config.py +++ b/rest_framework_ccbv/django_config.py @@ -43,8 +43,5 @@ def configure(): ), ) - try: - import django - django.setup() - except AttributeError: - pass + import django + django.setup() diff --git a/rest_framework_ccbv/inspector.py b/rest_framework_ccbv/inspector.py index 61ab050..eade290 100644 --- a/rest_framework_ccbv/inspector.py +++ b/rest_framework_ccbv/inspector.py @@ -12,9 +12,10 @@ from rest_framework.compat import View from rest_framework import serializers from rest_framework.serializers import BaseSerializer -from pygments import highlight +from pygments import highlight, lex from pygments.lexers import PythonLexer -from pygments.formatters import HtmlFormatter +from pygments.token import Token +from custom_formatter import CodeHtmlFormatter def add_to_klasses_if_its_restframework(klasses, klass): @@ -48,10 +49,11 @@ def get_klasses(): class Attribute(object): - def __init__(self, name, value, classobject): + def __init__(self, name, value, classobject, instance_class): self.name = name self.value = value self.classobject = classobject + self.instance_class = instance_class self.dirty = False def __eq__(self, obj): @@ -84,7 +86,7 @@ def params_string(self): def code(self): code = inspect.getsource(self.value) - return highlight(code, PythonLexer(), HtmlFormatter()) + return highlight(code, PythonLexer(), CodeHtmlFormatter(self.instance_class)) class Attributes(collections.MutableSequence): @@ -154,7 +156,8 @@ def get_attributes(self): if (not attr_str.startswith('__') and not isinstance(attr, types.MethodType)): attrs.append(Attribute(name=attr_str, value=attr, - classobject=klass)) + classobject=klass, + instance_class=self.get_klass())) return attrs def get_methods(self): @@ -167,7 +170,8 @@ def get_methods(self): isinstance(attr, types.MethodType)): attrs.append(Method(name=attr_str, value=attr, - classobject=klass)) + classobject=klass, + instance_class=self.get_klass())) return attrs def get_direct_ancestors(self): @@ -183,3 +187,36 @@ def get_available_versions(self): for version in klass_versions if self.module_name in klass_versions[version] and self.klass_name in klass_versions[version][self.module_name]] + + def get_unavailable_methods(self): + def next_token(tokensource, lookahead, is_looking_ahead): + for ttype, value in tokensource: + while lookahead and not is_looking_ahead: + yield lookahead.popleft() + yield ttype, value + + def lookahed_token_from_iter(lookahead, next_token_iter): + lookahead_token = next(next_token_iter) + lookahead.append(lookahead_token) + return lookahead_token + + not_implemented_methods = [] + for method in self.get_methods(): + lookahead = collections.deque() + lookback = collections.deque() + is_looking_ahead = False + tokensource = lex(inspect.getsource(method.value), PythonLexer()) + next_token_iter = next_token(tokensource, lookahead, is_looking_ahead) + for ttype, value in next_token_iter: + lookback.append((ttype, value)) + if ttype in Token.Name and lookback[-2][1] == '.' and lookback[-3][1] == 'self': + if not hasattr(self.get_klass(), value): + is_looking_ahead = True + try: + _, la_value = lookahed_token_from_iter(lookahead, next_token_iter) + if la_value == '(': + not_implemented_methods.append(value) + except StopIteration: + pass + is_looking_ahead = False + return set(not_implemented_methods) diff --git a/rest_framework_ccbv/renderers.py b/rest_framework_ccbv/renderers.py index 3ebf13c..ccd1365 100644 --- a/rest_framework_ccbv/renderers.py +++ b/rest_framework_ccbv/renderers.py @@ -57,6 +57,7 @@ def get_context(self): context['children'] = self.inspector.get_children() context['this_module'] = context['this_klass'].__module__ + context['unavailable_methods'] = self.inspector.get_unavailable_methods() return context diff --git a/static/style.css b/static/style.css index 4ecc187..a50f066 100644 --- a/static/style.css +++ b/static/style.css @@ -166,3 +166,7 @@ footer p { width: 200px; } } +.highlight a { + text-decoration: none; + border-bottom: solid 1px rgba(0, 114, 255, 0.21); +} diff --git a/templates/detail_view.html b/templates/detail_view.html index c85a4b1..e8c68e9 100644 --- a/templates/detail_view.html +++ b/templates/detail_view.html @@ -22,16 +22,16 @@ function goToAnchor() { var anchor = window.location.hash.replace("#", ""); $(".collapse").collapse('hide'); - $("#" + anchor).collapse('show'); - $('#' + anchor + ' .collapse').collapse('show'); - setTimeout(function(){ - if ($('#' + anchor)) { - console.log("scrolling"); + if (anchor) { + $("#" + anchor).collapse('show'); + $('#' + anchor + ' .collapse').collapse('show'); + setTimeout(function(){ + console.log($('#' + anchor).attr('class')); $('html, body').animate({ scrollTop: $('#' + anchor).offset().top - 70 }, 0); - } - }, 700); + }, 500); + } } $(document).ready(function() { @@ -93,8 +93,6 @@

Descendants

{% if loop.last %}{% endif %} {% endfor %} - -
{% for attribute in attributes %} {% if loop.first %} @@ -130,6 +128,17 @@

Attributes

{% endif %} {% endfor %}
+
+ {% for method in unavailable_methods %} + {% if loop.first %} +
+

Methods used but not implemented in this class

+
{% endif %} + {% endfor %} +
{% for method in methods %} {% if loop.first %} @@ -180,6 +189,6 @@

{{ child.classobject.__name__ }}

{% set previous_name=method.name %} {% endfor %}
- + {% endblock %} diff --git a/tests/test_formatter.py b/tests/test_formatter.py new file mode 100644 index 0000000..c565d0d --- /dev/null +++ b/tests/test_formatter.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +""" + Most of the tests here are taken from Pygments since we had + to replace the _format_lines method. Credits go to the authors. +""" + +from __future__ import print_function + +import io +import os +import re +import unittest +import tempfile +import inspect +from os.path import join, dirname, isfile + +from pygments.util import StringIO +from pygments.lexers import PythonLexer +from pygments.formatters import NullFormatter +from pygments.formatters.html import escape_html + +from rest_framework_ccbv.custom_formatter import CodeHtmlFormatter + + +class CodeHtmlFormatterTest(unittest.TestCase): + def test_correct_output(self): + hfmt = CodeHtmlFormatter(instance_class=type, nowrap=True) + houtfile = StringIO() + hfmt.format(tokensource, houtfile) + + nfmt = NullFormatter() + noutfile = StringIO() + nfmt.format(tokensource, noutfile) + + stripped_html = re.sub('<.*?>', '', houtfile.getvalue()) + escaped_text = escape_html(noutfile.getvalue()) + self.assertEqual(stripped_html, escaped_text) + + def test_all_options(self): + for optdict in [dict(nowrap=True), + dict(linenos=True), + dict(linenos=True, full=True), + dict(linenos=True, full=True, noclasses=True)]: + + outfile = StringIO() + fmt = CodeHtmlFormatter(instance_class=type, **optdict) + fmt.format(tokensource, outfile) + + def test_linenos(self): + optdict = dict(linenos=True) + outfile = StringIO() + fmt = CodeHtmlFormatter(instance_class=type, **optdict) + fmt.format(tokensource, outfile) + html = outfile.getvalue() + self.assertTrue(re.search("
\s+1\s+2\s+3", html))
+
+    def test_linenos_with_startnum(self):
+        optdict = dict(linenos=True, linenostart=5)
+        outfile = StringIO()
+        fmt = CodeHtmlFormatter(instance_class=type, **optdict)
+        fmt.format(tokensource, outfile)
+        html = outfile.getvalue()
+        self.assertTrue(re.search("
\s+5\s+6\s+7", html))
+
+    def test_lineanchors(self):
+        optdict = dict(lineanchors="foo")
+        outfile = StringIO()
+        fmt = CodeHtmlFormatter(instance_class=type, **optdict)
+        fmt.format(tokensource, outfile)
+        html = outfile.getvalue()
+        self.assertTrue(re.search("
", html))
+
+    def test_lineanchors_with_startnum(self):
+        optdict = dict(lineanchors="foo", linenostart=5)
+        outfile = StringIO()
+        fmt = CodeHtmlFormatter(instance_class=type, **optdict)
+        fmt.format(tokensource, outfile)
+        html = outfile.getvalue()
+        self.assertTrue(re.search("
", html))
+
+    def test_get_style_defs(self):
+        fmt = CodeHtmlFormatter(instance_class=type)
+        sd = fmt.get_style_defs()
+        self.assertTrue(sd.startswith('.'))
+
+        fmt = CodeHtmlFormatter(instance_class=type, cssclass='foo')
+        sd = fmt.get_style_defs()
+        self.assertTrue(sd.startswith('.foo'))
+        sd = fmt.get_style_defs('.bar')
+        self.assertTrue(sd.startswith('.bar'))
+        sd = fmt.get_style_defs(['.bar', '.baz'])
+        fl = sd.splitlines()[0]
+        self.assertTrue('.bar' in fl and '.baz' in fl)
+
+    def test_unicode_options(self):
+        fmt = CodeHtmlFormatter(title=u'Föö',
+                            instance_class=type,
+                            cssclass=u'bär',
+                            cssstyles=u'div:before { content: \'bäz\' }',
+                            encoding='utf-8')
+        handle, pathname = tempfile.mkstemp('.html')
+        tfile = os.fdopen(handle, 'w+b')
+        fmt.format(tokensource, tfile)
+        tfile.close()
+
+    def noop(self):
+        pass
+
+    def test_incode_links(self):
+        # reference another method
+        self.noop()
+        this_token_source = list(PythonLexer().get_tokens(
+            inspect.getsource(CodeHtmlFormatterTest.test_incode_links)
+        ))
+        hfmt = CodeHtmlFormatter(instance_class=self.__class__, nowrap=True)
+        houtfile = StringIO()
+        hfmt.format(this_token_source, houtfile)
+        assert 'noop' in houtfile.getvalue()
+
+
+tokensource = list(PythonLexer().get_tokens(
+    inspect.getsource(CodeHtmlFormatterTest.test_correct_output)))
diff --git a/tests/test_inspector.py b/tests/test_inspector.py
index 115b1c2..d46397b 100644
--- a/tests/test_inspector.py
+++ b/tests/test_inspector.py
@@ -26,7 +26,8 @@ def test_ancestor(self):
     def test_attributes(self):
         self.assertIn(Attribute(name='serializer_class',
                                 value=None,
-                                classobject=None),
+                                classobject=None,
+                                instance_class=self.inspector.get_klass()),
                       self.inspector.get_attributes())
         for attr in self.inspector.get_attributes():
             self.assertFalse(attr.name.startswith('_'))
@@ -52,6 +53,15 @@ def test_direct_acenstors(self):
                               ['CreateModelMixin',
                                'GenericAPIView'])
 
+    def test_unavailable_methods(self):
+        self.klass = 'ListModelMixin'
+        self.module = 'rest_framework.mixins'
+        self.inspector = Inspector(self.klass, self.module)
+        self.assertItemsEqual(self.inspector.get_unavailable_methods(),
+                              ['filter_queryset', 'get_queryset',
+                               'paginate_queryset', 'get_serializer',
+                               'get_paginated_response'])
+
 
 class TestMethod(unittest.TestCase):
     def setUp(self):
@@ -82,15 +92,15 @@ def method7(self, a, b=3, *args):
 
             def method8(self, a=2, b=3, **kwargs):
                 pass
-        self.method = Method('method', A.method, A)
-        self.method1 = Method('method1', A.method1, A)
-        self.method2 = Method('method2', A.method2, A)
-        self.method3 = Method('method3', A.method3, A)
-        self.method4 = Method('method4', A.method4, A)
-        self.method5 = Method('method5', A.method5, A)
-        self.method6 = Method('method6', A.method6, A)
-        self.method7 = Method('method7', A.method7, A)
-        self.method8 = Method('method8', A.method8, A)
+        self.method = Method('method', A.method, A, A)
+        self.method1 = Method('method1', A.method1, A, A)
+        self.method2 = Method('method2', A.method2, A, A)
+        self.method3 = Method('method3', A.method3, A, A)
+        self.method4 = Method('method4', A.method4, A, A)
+        self.method5 = Method('method5', A.method5, A, A)
+        self.method6 = Method('method6', A.method6, A, A)
+        self.method7 = Method('method7', A.method7, A, A)
+        self.method8 = Method('method8', A.method8, A, A)
 
     def test_method(self):
         self.assertEqual(self.method.params_string(), 'self, *args, **kwargs')
diff --git a/tests/test_jinja_utils.py b/tests/test_jinja_utils.py
new file mode 100644
index 0000000..5b67fe8
--- /dev/null
+++ b/tests/test_jinja_utils.py
@@ -0,0 +1,57 @@
+import unittest
+import inspect
+
+from django.views.generic import DetailView
+from rest_framework.generics import ListAPIView
+
+from rest_framework_ccbv.jinja_utils import templateEnv
+from rest_framework_ccbv.config import EXACT_VERSION
+
+
+class TestJinjaUtils(unittest.TestCase):
+
+    def get_context_function(self, name):
+        return templateEnv.globals[name]
+
+    def test_get_klass_url_with_django_view(self):
+        get_klass_url = self.get_context_function('get_klass_url')
+        assert get_klass_url(
+            {}, DetailView
+        ) == 'http://ccbv.co.uk/DetailView'
+
+    def test_get_klass_url_with_drf_view(self):
+        get_klass_url = self.get_context_function('get_klass_url')
+        assert get_klass_url(
+            {}, ListAPIView, 0.1
+        ) == '/0.1/rest_framework.generics/ListAPIView.html'
+
+    def test_get_version_url_without_klass(self):
+        get_version_url = self.get_context_function('get_version_url')
+        assert get_version_url({}, 0.1) == '/0.1/index.html'
+
+    def test_get_version_url_with_klass(self):
+        get_version_url = self.get_context_function('get_version_url')
+        assert get_version_url(
+            {'this_klass': ListAPIView}, 0.1
+        ) == '/0.1/rest_framework.generics/ListAPIView.html'
+
+    def test_get_klass_docs(self):
+        get_klass_docs = self.get_context_function('get_klass_docs')
+        assert get_klass_docs({}, ListAPIView.__doc__.strip())
+
+    def test_get_doc_link(self):
+        get_doc_link = self.get_context_function('get_doc_link')
+        assert get_doc_link(
+            {}, ListAPIView
+        ) == 'http://www.django-rest-framework.org/api-guide/generic-views#listapiview'
+
+    def test_get_src_link(self):
+        get_src_link = self.get_context_function('get_src_link')
+        lineno = str(inspect.getsourcelines(ListAPIView)[-1])
+        assert get_src_link(
+            {},  ListAPIView
+        ) == (
+            'https://github.com/tomchristie/django-rest-framework/blob/' +
+            EXACT_VERSION +
+            '/rest_framework/generics.py#L' + lineno
+        )
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
new file mode 100644
index 0000000..a573451
--- /dev/null
+++ b/tests/test_renderers.py
@@ -0,0 +1,99 @@
+import unittest
+
+from mock import mock_open, patch
+from rest_framework.generics import ListAPIView
+
+from rest_framework_ccbv.renderers import (
+    BasePageRenderer, IndexPageRenderer, LandPageRenderer, ErrorPageRenderer,
+    SitemapRenderer, DetailPageRenderer,
+)
+from rest_framework_ccbv.config import VERSION
+from rest_framework_ccbv.inspector import Attributes
+
+KLASS_FILE_CONTENT = (
+'{"2.2": {"rest_framework.generics": ["RetrieveDestroyAPIView", "ListAPIView"]},'
+'"%s": {"rest_framework.generics": ["RetrieveDestroyAPIView", "ListAPIView"]}}' % VERSION
+)
+
+
+class TestBasePageRenderer(unittest.TestCase):
+    def setUp(self):
+        self.renderer = BasePageRenderer([ListAPIView])
+        self.renderer.template_name = 'base.html'
+
+    @patch('rest_framework_ccbv.renderers.BasePageRenderer.get_context', return_value={'foo': 'bar'})
+    @patch('rest_framework_ccbv.renderers.templateEnv.get_template')
+    @patch('rest_framework_ccbv.renderers.open', new_callable=mock_open)
+    def test_render(self, mock_open, get_template_mock, get_context_mock):
+        self.renderer.render('foo')
+        mock_open.assert_called_once_with('foo', 'w')
+        handle = mock_open()
+        handle.write.assert_called_once()
+        get_template_mock.assert_called_with('base.html')
+        get_template_mock.return_value.render.assert_called_with({'foo': 'bar'})
+
+    @patch('rest_framework_ccbv.renderers.templateEnv.get_template')
+    @patch('rest_framework_ccbv.renderers.open', mock_open())
+    def test_context(self, get_template_mock):
+        self.renderer.render('foo')
+        context = get_template_mock.return_value.render.call_args_list[0][0][0]
+        assert context['version_prefix'] == 'Django REST Framework'
+        assert context['version']
+        assert context['versions']
+        assert context['other_versions']
+        assert context['klasses'] == [ListAPIView]
+
+
+class TestStaticPagesRenderered(unittest.TestCase):
+    def setUp(self):
+        self.rendererIndex = IndexPageRenderer([ListAPIView])
+        self.rendererLandPage = LandPageRenderer([ListAPIView])
+        self.rendererErrorPage = ErrorPageRenderer([ListAPIView])
+
+    @patch('rest_framework_ccbv.renderers.templateEnv.get_template')
+    @patch('rest_framework_ccbv.renderers.open', mock_open())
+    def test_template_name(self, get_template_mock):
+        self.rendererIndex.render('foo')
+        get_template_mock.assert_called_with('index.html')
+        self.rendererLandPage.render('foo')
+        get_template_mock.assert_called_with('home.html')
+        self.rendererErrorPage.render('foo')
+        get_template_mock.assert_called_with('error.html')
+
+
+class TestSitemapRenderer(unittest.TestCase):
+    def setUp(self):
+        self.renderer = SitemapRenderer([ListAPIView])
+
+    @patch('rest_framework_ccbv.renderers.templateEnv.get_template')
+    @patch('rest_framework_ccbv.renderers.open', mock_open(read_data='{}'))
+    def test_context(self, get_template_mock):
+        self.renderer.render('foo')
+        context = get_template_mock.return_value.render.call_args_list[0][0][0]
+        assert context['latest_version']
+        assert context['base_url']
+        assert context['klasses'] == {}
+
+
+class TestDetailPageRenderer(unittest.TestCase):
+    # @patch('rest_framework_ccbv.renderers.open', mock_open(read_data='{}'))
+    def setUp(self):
+        self.renderer = DetailPageRenderer(
+            [ListAPIView], ListAPIView.__name__, ListAPIView.__module__)
+
+    @patch('rest_framework_ccbv.renderers.templateEnv.get_template')
+    @patch('rest_framework_ccbv.renderers.open', mock_open(read_data=KLASS_FILE_CONTENT))
+    @patch('rest_framework_ccbv.inspector.open', mock_open(read_data=KLASS_FILE_CONTENT))
+    def test_context(self, get_template_mock):
+        self.renderer.render('foo')
+        context = get_template_mock.return_value.render.call_args_list[0][0][0]
+        assert context['other_versions'] == ['2.2']
+        assert context['name'] == ListAPIView.__name__
+        assert isinstance(context['ancestors'], (list, tuple))
+        assert isinstance(context['direct_ancestors'], (list, tuple))
+        assert isinstance(context['attributes'], Attributes)
+        assert isinstance(context['methods'], Attributes)
+        assert context['this_klass'] == ListAPIView
+        assert isinstance(context['children'], list)
+        assert context['this_module'] == ListAPIView.__module__
+        assert isinstance(context['unavailable_methods'], set)