diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt
index 2b7a18a13..739555667 100644
--- a/requirements/requirements-optionals.txt
+++ b/requirements/requirements-optionals.txt
@@ -1,6 +1,7 @@
# Optional packages which may be used with REST framework.
psycopg2-binary>=2.8.5, <2.9
-markdown==3.1.1
+markdown==3.3;python_version>="3.6"
+markdown==3.2.2;python_version=="3.5"
pygments==2.4.2
django-guardian==2.2.0
django-filter>=2.2.0, <2.3
diff --git a/tests/test_description.py b/tests/test_description.py
index ae00fe4a9..9e7e4dc32 100644
--- a/tests/test_description.py
+++ b/tests/test_description.py
@@ -1,192 +1,180 @@
-from django.test import TestCase
-
-from rest_framework.compat import apply_markdown
-from rest_framework.utils.formatting import dedent
-from rest_framework.views import APIView
-
-# We check that docstrings get nicely un-indented.
-DESCRIPTION = """an example docstring
-====================
-
-* list
-* list
-
-another header
---------------
-
- code block
-
-indented
-
-# hash style header #
-
-``` json
-[{
- "alpha": 1,
- "beta: "this is a string"
-}]
-```"""
-
-
-# If markdown is installed we also test it's working
-# (and that our wrapped forces '=' to h2 and '-' to h3)
-MARKED_DOWN_HILITE = """
-
[{
"alpha": 1,
\
- "beta: "this\
- is a \
-string"
}]
-
-
"""
-
-MARKED_DOWN_NOT_HILITE = """
-json
-[{
- "alpha": 1,
- "beta: "this is a string"
-}]
"""
-
-# We support markdown < 2.1 and markdown >= 2.1
-MARKED_DOWN_lt_21 = """an example docstring
-
-another header
-code block
-
-indented
-%s"""
-
-MARKED_DOWN_gte_21 = """an example docstring
-
-
-code block
-
-indented
-%s"""
-
-
-class TestViewNamesAndDescriptions(TestCase):
- def test_view_name_uses_class_name(self):
- """
- Ensure view names are based on the class name.
- """
- class MockView(APIView):
- pass
- assert MockView().get_view_name() == 'Mock'
-
- def test_view_name_uses_name_attribute(self):
- class MockView(APIView):
- name = 'Foo'
- assert MockView().get_view_name() == 'Foo'
-
- def test_view_name_uses_suffix_attribute(self):
- class MockView(APIView):
- suffix = 'List'
- assert MockView().get_view_name() == 'Mock List'
-
- def test_view_name_preferences_name_over_suffix(self):
- class MockView(APIView):
- name = 'Foo'
- suffix = 'List'
- assert MockView().get_view_name() == 'Foo'
-
- def test_view_description_uses_docstring(self):
- """Ensure view descriptions are based on the docstring."""
- class MockView(APIView):
- """an example docstring
- ====================
-
- * list
- * list
-
- another header
- --------------
-
- code block
-
- indented
-
- # hash style header #
-
- ``` json
- [{
- "alpha": 1,
- "beta: "this is a string"
- }]
- ```"""
-
- assert MockView().get_view_description() == DESCRIPTION
-
- def test_view_description_uses_description_attribute(self):
- class MockView(APIView):
- description = 'Foo'
- assert MockView().get_view_description() == 'Foo'
-
- def test_view_description_allows_empty_description(self):
- class MockView(APIView):
- """Description."""
- description = ''
- assert MockView().get_view_description() == ''
-
- def test_view_description_can_be_empty(self):
- """
- Ensure that if a view has no docstring,
- then it's description is the empty string.
- """
- class MockView(APIView):
- pass
- assert MockView().get_view_description() == ''
-
- def test_view_description_can_be_promise(self):
- """
- Ensure a view may have a docstring that is actually a lazily evaluated
- class that can be converted to a string.
-
- See: https://github.com/encode/django-rest-framework/issues/1708
- """
- # use a mock object instead of gettext_lazy to ensure that we can't end
- # up with a test case string in our l10n catalog
-
- class MockLazyStr:
- def __init__(self, string):
- self.s = string
-
- def __str__(self):
- return self.s
-
- class MockView(APIView):
- __doc__ = MockLazyStr("a gettext string")
-
- assert MockView().get_view_description() == 'a gettext string'
-
- def test_markdown(self):
- """
- Ensure markdown to HTML works as expected.
- """
- if apply_markdown:
- md_applied = apply_markdown(DESCRIPTION)
- gte_21_match = (
- md_applied == (
- MARKED_DOWN_gte_21 % MARKED_DOWN_HILITE) or
- md_applied == (
- MARKED_DOWN_gte_21 % MARKED_DOWN_NOT_HILITE))
- lt_21_match = (
- md_applied == (
- MARKED_DOWN_lt_21 % MARKED_DOWN_HILITE) or
- md_applied == (
- MARKED_DOWN_lt_21 % MARKED_DOWN_NOT_HILITE))
- assert gte_21_match or lt_21_match
-
-
-def test_dedent_tabs():
- result = 'first string\n\nsecond string'
- assert dedent(" first string\n\n second string") == result
- assert dedent("first string\n\n second string") == result
- assert dedent("\tfirst string\n\n\tsecond string") == result
- assert dedent("first string\n\n\tsecond string") == result
+import sys
+
+import pytest
+from django.test import TestCase
+
+from rest_framework.compat import apply_markdown
+from rest_framework.utils.formatting import dedent
+from rest_framework.views import APIView
+
+# We check that docstrings get nicely un-indented.
+DESCRIPTION = """an example docstring
+====================
+
+* list
+* list
+
+another header
+--------------
+
+ code block
+
+indented
+
+# hash style header #
+
+``` json
+[{
+ "alpha": 1,
+ "beta: "this is a string"
+}]
+```"""
+
+
+# If markdown is installed we also test it's working
+# (and that our wrapped forces '=' to h2 and '-' to h3)
+MARKDOWN_BASE = """an example docstring
+
+
+code block
+
+indented
+%s"""
+
+MARKDOWN_gte_33 = """
+[{
\
+ "alpha":\
+ 1,
\
+ "beta: "this\
+ is a \
+string"
}]\
+
+
"""
+
+MARKDOWN_lt_33 = """
+[{
\
+ "alpha":\
+ 1,
\
+ "beta: "this\
+ is a\
+ string"
}]\
+
+
+
"""
+
+
+class TestViewNamesAndDescriptions(TestCase):
+ def test_view_name_uses_class_name(self):
+ """
+ Ensure view names are based on the class name.
+ """
+ class MockView(APIView):
+ pass
+ assert MockView().get_view_name() == 'Mock'
+
+ def test_view_name_uses_name_attribute(self):
+ class MockView(APIView):
+ name = 'Foo'
+ assert MockView().get_view_name() == 'Foo'
+
+ def test_view_name_uses_suffix_attribute(self):
+ class MockView(APIView):
+ suffix = 'List'
+ assert MockView().get_view_name() == 'Mock List'
+
+ def test_view_name_preferences_name_over_suffix(self):
+ class MockView(APIView):
+ name = 'Foo'
+ suffix = 'List'
+ assert MockView().get_view_name() == 'Foo'
+
+ def test_view_description_uses_docstring(self):
+ """Ensure view descriptions are based on the docstring."""
+ class MockView(APIView):
+ """an example docstring
+ ====================
+
+ * list
+ * list
+
+ another header
+ --------------
+
+ code block
+
+ indented
+
+ # hash style header #
+
+ ``` json
+ [{
+ "alpha": 1,
+ "beta: "this is a string"
+ }]
+ ```"""
+
+ assert MockView().get_view_description() == DESCRIPTION
+
+ def test_view_description_uses_description_attribute(self):
+ class MockView(APIView):
+ description = 'Foo'
+ assert MockView().get_view_description() == 'Foo'
+
+ def test_view_description_allows_empty_description(self):
+ class MockView(APIView):
+ """Description."""
+ description = ''
+ assert MockView().get_view_description() == ''
+
+ def test_view_description_can_be_empty(self):
+ """
+ Ensure that if a view has no docstring,
+ then it's description is the empty string.
+ """
+ class MockView(APIView):
+ pass
+ assert MockView().get_view_description() == ''
+
+ def test_view_description_can_be_promise(self):
+ """
+ Ensure a view may have a docstring that is actually a lazily evaluated
+ class that can be converted to a string.
+
+ See: https://github.com/encode/django-rest-framework/issues/1708
+ """
+ # use a mock object instead of gettext_lazy to ensure that we can't end
+ # up with a test case string in our l10n catalog
+
+ class MockLazyStr:
+ def __init__(self, string):
+ self.s = string
+
+ def __str__(self):
+ return self.s
+
+ class MockView(APIView):
+ __doc__ = MockLazyStr("a gettext string")
+
+ assert MockView().get_view_description() == 'a gettext string'
+
+ @pytest.mark.skipif(not apply_markdown, reason="Markdown is not installed")
+ def test_markdown(self):
+ """
+ Ensure markdown to HTML works as expected.
+ """
+ # Markdown 3.3 is only supported on Python 3.6 and higher
+ if sys.version_info >= (3, 6):
+ assert apply_markdown(DESCRIPTION) == MARKDOWN_BASE % MARKDOWN_gte_33
+ else:
+ assert apply_markdown(DESCRIPTION) == MARKDOWN_BASE % MARKDOWN_lt_33
+
+
+def test_dedent_tabs():
+ result = 'first string\n\nsecond string'
+ assert dedent(" first string\n\n second string") == result
+ assert dedent("first string\n\n second string") == result
+ assert dedent("\tfirst string\n\n\tsecond string") == result
+ assert dedent("first string\n\n\tsecond string") == result