Repository: pytest-dev/pytest-django (python)

Issue:
Import pathlib from pytest
As discussed on #636 when all supported versions of pytest offer an import of `pathlib`, use that rather than direct dependency on `pathlib2`


Discussion:

Patch:
diff --git a/docs/conf.py b/docs/conf.py
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 import os
 import sys
 import datetime
diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py
--- a/pytest_django/asserts.py
+++ b/pytest_django/asserts.py
@@ -23,10 +23,10 @@ def assertion_func(*args, **kwargs):
 __all__ = []
 assertions_names = set()
 assertions_names.update(
-    set(attr for attr in vars(TestCase) if attr.startswith('assert')),
-    set(attr for attr in vars(SimpleTestCase) if attr.startswith('assert')),
-    set(attr for attr in vars(LiveServerTestCase) if attr.startswith('assert')),
-    set(attr for attr in vars(TransactionTestCase) if attr.startswith('assert')),
+    {attr for attr in vars(TestCase) if attr.startswith('assert')},
+    {attr for attr in vars(SimpleTestCase) if attr.startswith('assert')},
+    {attr for attr in vars(LiveServerTestCase) if attr.startswith('assert')},
+    {attr for attr in vars(TransactionTestCase) if attr.startswith('assert')},
 )
 
 for assert_func in assertions_names:
diff --git a/pytest_django/compat.py b/pytest_django/compat.py
deleted file mode 100644
--- a/pytest_django/compat.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# This file cannot be imported from until Django sets up
-try:
-    # Django 1.11+
-    from django.test.utils import setup_databases, teardown_databases  # noqa: F401, F811
-except ImportError:
-    # In Django prior to 1.11, teardown_databases is only available as a method on DiscoverRunner
-    from django.test.runner import setup_databases, DiscoverRunner  # noqa: F401, F811
-
-    def teardown_databases(db_cfg, verbosity):
-        DiscoverRunner(verbosity=verbosity, interactive=False).teardown_databases(
-            db_cfg
-        )
diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py
--- a/pytest_django/fixtures.py
+++ b/pytest_django/fixtures.py
@@ -1,9 +1,7 @@
 """All pytest-django fixtures"""
 
-from __future__ import with_statement
 
 import os
-import warnings
 from contextlib import contextmanager
 from functools import partial
 
@@ -91,7 +89,7 @@ def django_db_setup(
     django_db_modify_db_settings,
 ):
     """Top level fixture to ensure test databases are available"""
-    from .compat import setup_databases, teardown_databases
+    from django.test.utils import setup_databases, teardown_databases
 
     setup_databases_args = {}
 
@@ -164,7 +162,7 @@ def _disable_native_migrations():
     class MigrateSilentCommand(migrate.Command):
         def handle(self, *args, **kwargs):
             kwargs["verbosity"] = 0
-            return super(MigrateSilentCommand, self).handle(*args, **kwargs)
+            return super().handle(*args, **kwargs)
 
     migrate.Command = MigrateSilentCommand
 
@@ -320,7 +318,7 @@ def rf():
     return RequestFactory()
 
 
-class SettingsWrapper(object):
+class SettingsWrapper:
     _to_restore = []
 
     def __delattr__(self, attr):
@@ -370,8 +368,8 @@ def live_server(request):
     The address the server is started from is taken from the
     --liveserver command line option or if this is not provided from
     the DJANGO_LIVE_TEST_SERVER_ADDRESS environment variable.  If
-    neither is provided ``localhost:8081,8100-8200`` is used.  See the
-    Django documentation for its full syntax.
+    neither is provided ``localhost`` is used.  See the Django
+    documentation for its full syntax.
 
     NOTE: If the live server needs database access to handle a request
           your test will have to request database access.  Furthermore
@@ -385,27 +383,9 @@ def live_server(request):
     """
     skip_if_no_django()
 
-    import django
-
     addr = request.config.getvalue("liveserver") or os.getenv(
         "DJANGO_LIVE_TEST_SERVER_ADDRESS"
-    )
-
-    if addr and ":" in addr:
-        if django.VERSION >= (1, 11):
-            ports = addr.split(":")[1]
-            if "-" in ports or "," in ports:
-                warnings.warn(
-                    "Specifying multiple live server ports is not supported "
-                    "in Django 1.11. This will be an error in a future "
-                    "pytest-django release."
-                )
-
-    if not addr:
-        if django.VERSION < (1, 11):
-            addr = "localhost:8081,8100-8200"
-        else:
-            addr = "localhost"
+    ) or "localhost"
 
     server = live_server_helper.LiveServer(addr)
     request.addfinalizer(server.stop)
@@ -458,14 +438,14 @@ def _assert_num_queries(config, num, exact=True, connection=None, info=None):
                 num,
                 "" if exact else "or less ",
                 "but {} done".format(
-                    num_performed == 1 and "1 was" or "%d were" % (num_performed,)
+                    num_performed == 1 and "1 was" or "{} were".format(num_performed)
                 ),
             )
             if info:
                 msg += "\n{}".format(info)
             if verbose:
                 sqls = (q["sql"] for q in context.captured_queries)
-                msg += "\n\nQueries:\n========\n\n%s" % "\n\n".join(sqls)
+                msg += "\n\nQueries:\n========\n\n" + "\n\n".join(sqls)
             else:
                 msg += " (add -v option to show queries)"
             pytest.fail(msg)
diff --git a/pytest_django/live_server_helper.py b/pytest_django/live_server_helper.py
--- a/pytest_django/live_server_helper.py
+++ b/pytest_django/live_server_helper.py
@@ -1,8 +1,4 @@
-import six
-
-
-@six.python_2_unicode_compatible
-class LiveServer(object):
+class LiveServer:
     """The liveserver fixture
 
     This is the object that the ``live_server`` fixture returns.
@@ -10,7 +6,6 @@ class LiveServer(object):
     """
 
     def __init__(self, addr):
-        import django
         from django.db import connections
         from django.test.testcases import LiveServerThread
         from django.test.utils import modify_settings
@@ -39,17 +34,13 @@ def __init__(self, addr):
 
             liveserver_kwargs["static_handler"] = _StaticFilesHandler
 
-        if django.VERSION < (1, 11):
-            host, possible_ports = parse_addr(addr)
-            self.thread = LiveServerThread(host, possible_ports, **liveserver_kwargs)
+        try:
+            host, port = addr.split(":")
+        except ValueError:
+            host = addr
         else:
-            try:
-                host, port = addr.split(":")
-            except ValueError:
-                host = addr
-            else:
-                liveserver_kwargs["port"] = int(port)
-            self.thread = LiveServerThread(host, **liveserver_kwargs)
+            liveserver_kwargs["port"] = int(port)
+        self.thread = LiveServerThread(host, **liveserver_kwargs)
 
         self._live_server_modified_settings = modify_settings(
             ALLOWED_HOSTS={"append": host}
@@ -69,41 +60,13 @@ def stop(self):
 
     @property
     def url(self):
-        return "http://%s:%s" % (self.thread.host, self.thread.port)
+        return "http://{}:{}".format(self.thread.host, self.thread.port)
 
     def __str__(self):
         return self.url
 
     def __add__(self, other):
-        return "%s%s" % (self, other)
+        return "{}{}".format(self, other)
 
     def __repr__(self):
         return "<LiveServer listening at %s>" % self.url
-
-
-def parse_addr(specified_address):
-    """Parse the --liveserver argument into a host/IP address and port range"""
-    # This code is based on
-    # django.test.testcases.LiveServerTestCase.setUpClass
-
-    # The specified ports may be of the form '8000-8010,8080,9200-9300'
-    # i.e. a comma-separated list of ports or ranges of ports, so we break
-    # it down into a detailed list of all possible ports.
-    possible_ports = []
-    try:
-        host, port_ranges = specified_address.split(":")
-        for port_range in port_ranges.split(","):
-            # A port range can be of either form: '8000' or '8000-8010'.
-            extremes = list(map(int, port_range.split("-")))
-            assert len(extremes) in (1, 2)
-            if len(extremes) == 1:
-                # Port range of the form '8000'
-                possible_ports.append(extremes[0])
-            else:
-                # Port range of the form '8000-8010'
-                for port in range(extremes[0], extremes[1] + 1):
-                    possible_ports.append(port)
-    except Exception:
-        raise Exception('Invalid address ("%s") for live server.' % specified_address)
-
-    return host, possible_ports
diff --git a/pytest_django/migrations.py b/pytest_django/migrations.py
--- a/pytest_django/migrations.py
+++ b/pytest_django/migrations.py
@@ -2,7 +2,7 @@
 from pytest_django.lazy_django import get_django_version
 
 
-class DisableMigrations(object):
+class DisableMigrations:
     def __init__(self):
         self._django_version = get_django_version()
 
@@ -10,7 +10,4 @@ def __contains__(self, item):
         return True
 
     def __getitem__(self, item):
-        if self._django_version >= (1, 9):
-            return None
-        else:
-            return "notmigrations"
+        return None
diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py
--- a/pytest_django/plugin.py
+++ b/pytest_django/plugin.py
@@ -8,6 +8,7 @@
 import inspect
 from functools import reduce
 import os
+import pathlib
 import sys
 import types
 
@@ -39,22 +40,11 @@
 
 from .lazy_django import django_settings_is_configured, skip_if_no_django
 
-try:
-    import pathlib
-except ImportError:
-    import pathlib2 as pathlib
-
 
 SETTINGS_MODULE_ENV = "DJANGO_SETTINGS_MODULE"
 CONFIGURATION_ENV = "DJANGO_CONFIGURATION"
 INVALID_TEMPLATE_VARS_ENV = "FAIL_INVALID_TEMPLATE_VARS"
 
-PY2 = sys.version_info[0] == 2
-
-# pytest 4.2 handles unittest setup/teardown itself via wrapping fixtures.
-_pytest_version_info = tuple(int(x) for x in pytest.__version__.split(".", 2)[:2])
-_handle_unittest_methods = _pytest_version_info < (4, 2)
-
 _report_header = []
 
 
@@ -303,11 +293,11 @@ def _get_option_with_source(option, envname):
     dc, dc_source = _get_option_with_source(options.dc, CONFIGURATION_ENV)
 
     if ds:
-        _report_header.append("settings: %s (from %s)" % (ds, ds_source))
+        _report_header.append("settings: {} (from {})".format(ds, ds_source))
         os.environ[SETTINGS_MODULE_ENV] = ds
 
         if dc:
-            _report_header.append("configuration: %s (from %s)" % (dc, dc_source))
+            _report_header.append("configuration: {} (from {})".format(dc, dc_source))
             os.environ[CONFIGURATION_ENV] = dc
 
             # Install the django-configurations importer
@@ -330,7 +320,7 @@ def pytest_report_header():
         return ["django: " + ", ".join(_report_header)]
 
 
-@pytest.mark.trylast
+@pytest.hookimpl(trylast=True)
 def pytest_configure():
     # Allow Django settings to be configured in a user pytest_configure call,
     # but make sure we call django.setup()
@@ -354,13 +344,7 @@ def _classmethod_is_defined_at_leaf(cls, method_name):
     try:
         f = method.__func__
     except AttributeError:
-        pytest.fail("%s.%s should be a classmethod" % (cls, method_name))
-    if PY2 and not (
-        inspect.ismethod(method)
-        and inspect.isclass(method.__self__)
-        and issubclass(cls, method.__self__)
-    ):
-        pytest.fail("%s.%s should be a classmethod" % (cls, method_name))
+        pytest.fail("{}.{} should be a classmethod".format(cls, method_name))
     return f is not super_method.__func__
 
 
@@ -409,12 +393,6 @@ def _restore_class_methods(cls):
         cls.tearDownClass = tearDownClass
 
 
-def pytest_runtest_setup(item):
-    if _handle_unittest_methods:
-        if django_settings_is_configured() and is_django_unittest(item):
-            _disable_class_methods(item.cls)
-
-
 @pytest.hookimpl(tryfirst=True)
 def pytest_collection_modifyitems(items):
     # If Django is not configured we don't need to bother
@@ -523,33 +501,21 @@ def _django_setup_unittest(request, django_db_blocker):
     # Fix/patch pytest.
     # Before pytest 5.4: https://github.com/pytest-dev/pytest/issues/5991
     # After pytest 5.4: https://github.com/pytest-dev/pytest-django/issues/824
-    from _pytest.monkeypatch import MonkeyPatch
+    from _pytest.unittest import TestCaseFunction
+    original_runtest = TestCaseFunction.runtest
 
     def non_debugging_runtest(self):
         self._testcase(result=self)
 
-    mp_debug = MonkeyPatch()
-    mp_debug.setattr("_pytest.unittest.TestCaseFunction.runtest", non_debugging_runtest)
-
-    request.getfixturevalue("django_db_setup")
-
-    cls = request.node.cls
-
-    with django_db_blocker.unblock():
-        if _handle_unittest_methods:
-            _restore_class_methods(cls)
-            cls.setUpClass()
-            _disable_class_methods(cls)
+    try:
+        TestCaseFunction.runtest = non_debugging_runtest
 
-            yield
+        request.getfixturevalue("django_db_setup")
 
-            _restore_class_methods(cls)
-            cls.tearDownClass()
-        else:
+        with django_db_blocker.unblock():
             yield
-
-    if mp_debug:
-        mp_debug.undo()
+    finally:
+        TestCaseFunction.runtest = original_runtest
 
 
 @pytest.fixture(scope="function", autouse=True)
@@ -591,12 +557,7 @@ def _django_set_urlconf(request):
     if marker:
         skip_if_no_django()
         import django.conf
-
-        try:
-            from django.urls import clear_url_caches, set_urlconf
-        except ImportError:
-            # Removed in Django 2.0
-            from django.core.urlresolvers import clear_url_caches, set_urlconf
+        from django.urls import clear_url_caches, set_urlconf
 
         urls = validate_urls(marker)
         original_urlconf = django.conf.settings.ROOT_URLCONF
@@ -629,7 +590,7 @@ def _fail_for_invalid_template_variable():
     ``pytest.mark.ignore_template_errors``
     """
 
-    class InvalidVarException(object):
+    class InvalidVarException:
         """Custom handler for invalid strings in templates."""
 
         def __init__(self):
@@ -677,7 +638,7 @@ def __mod__(self, var):
             """Handle TEMPLATE_STRING_IF_INVALID % var."""
             origin = self._get_origin()
             if origin:
-                msg = "Undefined template variable '%s' in '%s'" % (var, origin)
+                msg = "Undefined template variable '{}' in '{}'".format(var, origin)
             else:
                 msg = "Undefined template variable '%s'" % var
             if self.fail:
@@ -732,7 +693,7 @@ def _django_clear_site_cache():
 # ############### Helper Functions ################
 
 
-class _DatabaseBlockerContextManager(object):
+class _DatabaseBlockerContextManager:
     def __init__(self, db_blocker):
         self._db_blocker = db_blocker
 
@@ -743,7 +704,7 @@ def __exit__(self, exc_type, exc_value, traceback):
         self._db_blocker.restore()
 
 
-class _DatabaseBlocker(object):
+class _DatabaseBlocker:
     """Manager for django.db.backends.base.base.BaseDatabaseWrapper.
 
     This is the object returned by django_db_blocker.
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-# -*- coding: utf-8 -*-
 
 import codecs
 import os
@@ -28,11 +27,10 @@ def read(fname):
     license='BSD-3-Clause',
     packages=['pytest_django'],
     long_description=read('README.rst'),
-    python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
+    python_requires='>=3.5',
     setup_requires=['setuptools_scm>=1.11.1'],
     install_requires=[
-        'pytest>=3.6',
-        'pathlib2;python_version<"3.4"',
+        'pytest>=5.4.0',
     ],
     extras_require={
         'docs': [
@@ -42,17 +40,10 @@ def read(fname):
         'testing': [
             'Django',
             'django-configurations>=2.0',
-            'six',
         ],
     },
     classifiers=['Development Status :: 5 - Production/Stable',
                  'Framework :: Django',
-                 'Framework :: Django :: 1.8',
-                 'Framework :: Django :: 1.9',
-                 'Framework :: Django :: 1.10',
-                 'Framework :: Django :: 1.11',
-                 'Framework :: Django :: 2.0',
-                 'Framework :: Django :: 2.1',
                  'Framework :: Django :: 2.2',
                  'Framework :: Django :: 3.0',
                  'Framework :: Django :: 3.1',
@@ -60,8 +51,6 @@ def read(fname):
                  'License :: OSI Approved :: BSD License',
                  'Operating System :: OS Independent',
                  'Programming Language :: Python',
-                 'Programming Language :: Python :: 2.7',
-                 'Programming Language :: Python :: 3.4',
                  'Programming Language :: Python :: 3.5',
                  'Programming Language :: Python :: 3.6',
                  'Programming Language :: Python :: 3.7',


Test patch:
diff --git a/pytest_django_test/app/migrations/0001_initial.py b/pytest_django_test/app/migrations/0001_initial.py
--- a/pytest_django_test/app/migrations/0001_initial.py
+++ b/pytest_django_test/app/migrations/0001_initial.py
@@ -1,6 +1,4 @@
-# -*- coding: utf-8 -*-
 # Generated by Django 1.9a1 on 2016-06-22 04:33
-from __future__ import unicode_literals
 
 from django.db import migrations, models
 
diff --git a/pytest_django_test/compat.py b/pytest_django_test/compat.py
deleted file mode 100644
--- a/pytest_django_test/compat.py
+++ /dev/null
@@ -1,4 +0,0 @@
-try:
-    from urllib2 import urlopen, HTTPError
-except ImportError:
-    from urllib.request import urlopen, HTTPError  # noqa: F401
diff --git a/pytest_django_test/db_helpers.py b/pytest_django_test/db_helpers.py
--- a/pytest_django_test/db_helpers.py
+++ b/pytest_django_test/db_helpers.py
@@ -31,7 +31,7 @@ def get_db_engine():
     return _settings["ENGINE"].split(".")[-1]
 
 
-class CmdResult(object):
+class CmdResult:
     def __init__(self, status_code, std_out, std_err):
         self.status_code = status_code
         self.std_out = std_out
@@ -64,7 +64,7 @@ def skip_if_sqlite_in_memory():
 def _get_db_name(db_suffix=None):
     name = TEST_DB_NAME
     if db_suffix:
-        name = "%s_%s" % (name, db_suffix)
+        name = "{}_{}".format(name, db_suffix)
     return name
 
 
@@ -72,7 +72,7 @@ def drop_database(db_suffix=None):
     name = _get_db_name(db_suffix)
     db_engine = get_db_engine()
 
-    if db_engine == "postgresql_psycopg2":
+    if db_engine == "postgresql":
         r = run_cmd("psql", "postgres", "-c", "DROP DATABASE %s" % name)
         assert "DROP DATABASE" in force_str(
             r.std_out
@@ -94,7 +94,7 @@ def db_exists(db_suffix=None):
     name = _get_db_name(db_suffix)
     db_engine = get_db_engine()
 
-    if db_engine == "postgresql_psycopg2":
+    if db_engine == "postgresql":
         r = run_cmd("psql", name, "-c", "SELECT 1")
         return r.status_code == 0
 
@@ -111,7 +111,7 @@ def db_exists(db_suffix=None):
 def mark_database():
     db_engine = get_db_engine()
 
-    if db_engine == "postgresql_psycopg2":
+    if db_engine == "postgresql":
         r = run_cmd("psql", TEST_DB_NAME, "-c", "CREATE TABLE mark_table();")
         assert r.status_code == 0
         return
@@ -136,7 +136,7 @@ def mark_database():
 def mark_exists():
     db_engine = get_db_engine()
 
-    if db_engine == "postgresql_psycopg2":
+    if db_engine == "postgresql":
         r = run_cmd("psql", TEST_DB_NAME, "-c", "SELECT 1 FROM mark_table")
 
         # When something pops out on std_out, we are good
diff --git a/pytest_django_test/settings_base.py b/pytest_django_test/settings_base.py
--- a/pytest_django_test/settings_base.py
+++ b/pytest_django_test/settings_base.py
@@ -1,5 +1,3 @@
-import django
-
 ROOT_URLCONF = "pytest_django_test.urls"
 INSTALLED_APPS = [
     "django.contrib.auth",
@@ -20,9 +18,6 @@
     "django.contrib.messages.middleware.MessageMiddleware",
 ]
 
-if django.VERSION < (1, 10):
-    MIDDLEWARE_CLASSES = MIDDLEWARE
-
 
 TEMPLATES = [
     {
diff --git a/pytest_django_test/settings_postgres.py b/pytest_django_test/settings_postgres.py
--- a/pytest_django_test/settings_postgres.py
+++ b/pytest_django_test/settings_postgres.py
@@ -2,7 +2,7 @@
 
 # PyPy compatibility
 try:
-    from psycopg2ct import compat
+    from psycopg2cffi import compat
 
     compat.register()
 except ImportError:
@@ -11,9 +11,7 @@
 
 DATABASES = {
     "default": {
-        "ENGINE": "django.db.backends.postgresql_psycopg2",
+        "ENGINE": "django.db.backends.postgresql",
         "NAME": "pytest_django_should_never_get_accessed",
-        "HOST": "localhost",
-        "USER": "",
     }
 }
diff --git a/pytest_django_test/urls.py b/pytest_django_test/urls.py
--- a/pytest_django_test/urls.py
+++ b/pytest_django_test/urls.py
@@ -1,8 +1,8 @@
-from django.conf.urls import url
+from django.urls import path
 
 from .app import views
 
 urlpatterns = [
-    url(r"^item_count/$", views.item_count),
-    url(r"^admin-required/$", views.admin_required_view),
+    path("item_count/", views.item_count),
+    path("admin-required/", views.admin_required_view),
 ]
diff --git a/pytest_django_test/urls_overridden.py b/pytest_django_test/urls_overridden.py
--- a/pytest_django_test/urls_overridden.py
+++ b/pytest_django_test/urls_overridden.py
@@ -1,6 +1,6 @@
-from django.conf.urls import url
+from django.urls import path
 from django.http import HttpResponse
 
 urlpatterns = [
-    url(r"^overridden_url/$", lambda r: HttpResponse("Overridden urlconf works!"))
+    path("overridden_url/", lambda r: HttpResponse("Overridden urlconf works!"))
 ]
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,7 +3,6 @@
 from textwrap import dedent
 
 import pytest
-import six
 from django.conf import settings
 
 try:
@@ -58,7 +57,7 @@ def django_testdir(request, testdir, monkeypatch):
 
         # Pypy compatibility
         try:
-            from psycopg2ct import compat
+            from psycopg2cffi import compat
         except ImportError:
             pass
         else:
@@ -81,9 +80,6 @@ def django_testdir(request, testdir, monkeypatch):
             'django.contrib.messages.middleware.MessageMiddleware',
         ]
 
-        if django.VERSION < (1, 10):
-            MIDDLEWARE_CLASSES = MIDDLEWARE
-
         TEMPLATES = [
             {
                 'BACKEND': 'django.template.backends.django.DjangoTemplates',
@@ -118,7 +114,7 @@ def django_testdir(request, testdir, monkeypatch):
     test_app_path = tpkg_path.join("app")
 
     # Copy the test app to make it available in the new test run
-    shutil.copytree(six.text_type(app_source), six.text_type(test_app_path))
+    shutil.copytree(str(app_source), str(test_app_path))
     tpkg_path.join("the_settings.py").write(test_settings)
 
     monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.the_settings")
diff --git a/tests/test_database.py b/tests/test_database.py
--- a/tests/test_database.py
+++ b/tests/test_database.py
@@ -1,5 +1,3 @@
-from __future__ import with_statement
-
 import pytest
 from django.db import connection
 from django.test.testcases import connections_support_transactions
diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py
--- a/tests/test_db_setup.py
+++ b/tests/test_db_setup.py
@@ -1,6 +1,5 @@
 import pytest
 
-from pytest_django.lazy_django import get_django_version
 from pytest_django_test.db_helpers import (
     db_exists,
     drop_database,
@@ -453,33 +452,7 @@ def test_a():
         result.stdout.fnmatch_lines(["*PASSED*test_a*"])
 
 
-@pytest.mark.skipif(
-    get_django_version() >= (1, 9),
-    reason=(
-        "Django 1.9 requires migration and has no concept of initial data fixtures"
-    ),
-)
-def test_initial_data(django_testdir_initial):
-    """Test that initial data gets loaded."""
-    django_testdir_initial.create_test_module(
-        """
-        import pytest
-
-        from .app.models import Item
-
-        @pytest.mark.django_db
-        def test_inner():
-            assert [x.name for x in Item.objects.all()] \
-                == ["mark_initial_data"]
-    """
-    )
-
-    result = django_testdir_initial.runpytest_subprocess("--tb=short", "-v")
-    assert result.ret == 0
-    result.stdout.fnmatch_lines(["*test_inner*PASSED*"])
-
-
-class TestNativeMigrations(object):
+class TestNativeMigrations:
     """ Tests for Django Migrations """
 
     def test_no_migrations(self, django_testdir):
diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py
--- a/tests/test_django_settings_module.py
+++ b/tests/test_django_settings_module.py
@@ -3,7 +3,6 @@
 If these tests fail you probably forgot to run "python setup.py develop".
 """
 
-import django
 import pytest
 
 
@@ -308,10 +307,6 @@ def test_debug_is_false():
     assert r.ret == 0
 
 
-@pytest.mark.skipif(
-    not hasattr(django, "setup"),
-    reason="This Django version does not support app loading",
-)
 @pytest.mark.django_project(
     extra_settings="""
     INSTALLED_APPS = [
@@ -329,10 +324,7 @@ class TestApp(AppConfig):
             name = 'tpkg.app'
 
             def ready(self):
-                try:
-                    populating = apps.loading
-                except AttributeError:  # Django < 2.0
-                    populating = apps._lock.locked()
+                populating = apps.loading
                 print('READY(): populating=%r' % populating)
         """,
         "apps.py",
@@ -342,10 +334,7 @@ def ready(self):
         """
         from django.apps import apps
 
-        try:
-            populating = apps.loading
-        except AttributeError:  # Django < 2.0
-            populating = apps._lock.locked()
+        populating = apps.loading
 
         print('IMPORT: populating=%r,ready=%r' % (populating, apps.ready))
         SOME_THING = 1234
@@ -360,10 +349,7 @@ def ready(self):
         from tpkg.app.models import SOME_THING
 
         def test_anything():
-            try:
-                populating = apps.loading
-            except AttributeError:  # Django < 2.0
-                populating = apps._lock.locked()
+            populating = apps.loading
 
             print('TEST: populating=%r,ready=%r' % (populating, apps.ready))
         """
@@ -372,10 +358,7 @@ def test_anything():
     result = django_testdir.runpytest_subprocess("-s", "--tb=line")
     result.stdout.fnmatch_lines(["*IMPORT: populating=True,ready=False*"])
     result.stdout.fnmatch_lines(["*READY(): populating=True*"])
-    if django.VERSION < (2, 0):
-        result.stdout.fnmatch_lines(["*TEST: populating=False,ready=True*"])
-    else:
-        result.stdout.fnmatch_lines(["*TEST: populating=True,ready=True*"])
+    result.stdout.fnmatch_lines(["*TEST: populating=True,ready=True*"])
     assert result.ret == 0
 
 
diff --git a/tests/test_environment.py b/tests/test_environment.py
--- a/tests/test_environment.py
+++ b/tests/test_environment.py
@@ -1,5 +1,3 @@
-from __future__ import with_statement
-
 import os
 
 import pytest
@@ -8,7 +6,6 @@
 from django.core import mail
 from django.db import connection
 from django.test import TestCase
-from pytest_django.lazy_django import get_django_version
 
 from pytest_django_test.app.models import Item
 
@@ -57,11 +54,11 @@ def test_two(self):
 def test_invalid_template_variable(django_testdir):
     django_testdir.create_app_file(
         """
-        from django.conf.urls import url
+        from django.urls import path
 
         from tpkg.app import views
 
-        urlpatterns = [url(r'invalid_template/', views.invalid_template)]
+        urlpatterns = [path('invalid_template/', views.invalid_template)]
         """,
         "urls.py",
     )
@@ -95,10 +92,7 @@ def test_ignore(client):
     )
     result = django_testdir.runpytest_subprocess("-s", "--fail-on-template-vars")
 
-    if get_django_version() >= (1, 9):
-        origin = "'*/tpkg/app/templates/invalid_template_base.html'"
-    else:
-        origin = "'invalid_template.html'"
+    origin = "'*/tpkg/app/templates/invalid_template_base.html'"
     result.stdout.fnmatch_lines_random(
         [
             "tpkg/test_the_test.py F.*",
@@ -163,11 +157,11 @@ def test_for_invalid_template():
 def test_invalid_template_variable_opt_in(django_testdir):
     django_testdir.create_app_file(
         """
-        from django.conf.urls import url
+        from django.urls import path
 
         from tpkg.app import views
 
-        urlpatterns = [url(r'invalid_template/', views.invalid_template)]
+        urlpatterns = [path('invalid_template', views.invalid_template)]
         """,
         "urls.py",
     )
@@ -255,14 +249,9 @@ def test_verbose_with_v(self, testdir):
         """Verbose output with '-v'."""
         result = testdir.runpytest_subprocess("-s", "-v")
         result.stdout.fnmatch_lines_random(["tpkg/test_the_test.py:*", "*PASSED*"])
-        if get_django_version() >= (2, 2):
-            result.stderr.fnmatch_lines(
-                ["*Destroying test database for alias 'default'*"]
-            )
-        else:
-            result.stdout.fnmatch_lines(
-                ["*Destroying test database for alias 'default'...*"]
-            )
+        result.stderr.fnmatch_lines(
+            ["*Destroying test database for alias 'default'*"]
+        )
 
     def test_more_verbose_with_vv(self, testdir):
         """More verbose output with '-v -v'."""
@@ -275,37 +264,22 @@ def test_more_verbose_with_vv(self, testdir):
                 "*PASSED*",
             ]
         )
-        if get_django_version() >= (2, 2):
-            result.stderr.fnmatch_lines(
-                [
-                    "*Creating test database for alias*",
-                    "*Destroying test database for alias 'default'*",
-                ]
-            )
-        else:
-            result.stdout.fnmatch_lines(
-                [
-                    "*Creating test database for alias*",
-                    "*Destroying test database for alias 'default'*",
-                ]
-            )
+        result.stderr.fnmatch_lines(
+            [
+                "*Creating test database for alias*",
+                "*Destroying test database for alias 'default'*",
+            ]
+        )
 
     def test_more_verbose_with_vv_and_reusedb(self, testdir):
         """More verbose output with '-v -v', and --create-db."""
         result = testdir.runpytest_subprocess("-s", "-v", "-v", "--create-db")
         result.stdout.fnmatch_lines(["tpkg/test_the_test.py:*", "*PASSED*"])
-        if get_django_version() >= (2, 2):
-            result.stderr.fnmatch_lines(["*Creating test database for alias*"])
-            assert (
-                "*Destroying test database for alias 'default' ('*')...*"
-                not in result.stderr.str()
-            )
-        else:
-            result.stdout.fnmatch_lines(["*Creating test database for alias*"])
-            assert (
-                "*Destroying test database for alias 'default' ('*')...*"
-                not in result.stdout.str()
-            )
+        result.stderr.fnmatch_lines(["*Creating test database for alias*"])
+        assert (
+            "*Destroying test database for alias 'default' ('*')...*"
+            not in result.stderr.str()
+        )
 
 
 @pytest.mark.django_db
diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py
--- a/tests/test_fixtures.py
+++ b/tests/test_fixtures.py
@@ -4,10 +4,10 @@
 fixtures are tested in test_database.
 """
 
-from __future__ import with_statement
 
 import socket
 from contextlib import contextmanager
+from urllib.request import urlopen, HTTPError
 
 import pytest
 from django.conf import settings as real_settings
@@ -17,9 +17,7 @@
 from django.test.testcases import connections_support_transactions
 from django.utils.encoding import force_str
 
-from pytest_django.lazy_django import get_django_version
 from pytest_django_test.app.models import Item
-from pytest_django_test.compat import HTTPError, urlopen
 
 
 @contextmanager
@@ -322,7 +320,7 @@ def test_settings_before(self):
         from django.conf import settings
 
         assert (
-            "%s.%s" % (settings.__class__.__module__, settings.__class__.__name__)
+            "{}.{}".format(settings.__class__.__module__, settings.__class__.__name__)
             == "django.conf.Settings"
         )
         TestLiveServer._test_settings_before_run = True
@@ -335,18 +333,14 @@ def test_change_settings(self, live_server, settings):
 
     def test_settings_restored(self):
         """Ensure that settings are restored after test_settings_before."""
-        import django
         from django.conf import settings
 
         assert TestLiveServer._test_settings_before_run is True
         assert (
-            "%s.%s" % (settings.__class__.__module__, settings.__class__.__name__)
+            "{}.{}".format(settings.__class__.__module__, settings.__class__.__name__)
             == "django.conf.Settings"
         )
-        if django.VERSION >= (1, 11):
-            assert settings.ALLOWED_HOSTS == ["testserver"]
-        else:
-            assert settings.ALLOWED_HOSTS == ["*"]
+        assert settings.ALLOWED_HOSTS == ["testserver"]
 
     def test_transactions(self, live_server):
         if not connections_support_transactions():
@@ -417,12 +411,9 @@ def test_serve_static_with_staticfiles_app(self, django_testdir, settings):
         """
         django_testdir.create_test_module(
             """
-            from django.utils.encoding import force_str
+            from urllib.request import urlopen
 
-            try:
-                from urllib2 import urlopen
-            except ImportError:
-                from urllib.request import urlopen
+            from django.utils.encoding import force_str
 
             class TestLiveServer:
                 def test_a(self, live_server, settings):
@@ -445,28 +436,6 @@ def test_serve_static_dj17_without_staticfiles_app(self, live_server, settings):
         with pytest.raises(HTTPError):
             urlopen(live_server + "/static/a_file.txt").read()
 
-    @pytest.mark.skipif(
-        get_django_version() < (1, 11), reason="Django >= 1.11 required"
-    )
-    def test_specified_port_range_error_message_django_111(self, django_testdir):
-        django_testdir.create_test_module(
-            """
-        def test_with_live_server(live_server):
-            pass
-        """
-        )
-
-        result = django_testdir.runpytest_subprocess("--liveserver=localhost:1234-2345")
-        result.stdout.fnmatch_lines(
-            [
-                "*Specifying multiple live server ports is not supported in Django 1.11. This "
-                "will be an error in a future pytest-django release.*"
-            ]
-        )
-
-    @pytest.mark.skipif(
-        get_django_version() < (1, 11, 2), reason="Django >= 1.11.2 required"
-    )
     def test_specified_port_django_111(self, django_testdir):
         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         try:
@@ -516,16 +485,11 @@ class MyCustomUser(AbstractUser):
     )
     django_testdir.create_app_file(
         """
-        from tpkg.app import views
+        from django.urls import path
 
-        try:
-            from django.urls import path
-        except ImportError:
-            from django.conf.urls import url
+        from tpkg.app import views
 
-            urlpatterns = [url(r'admin-required/', views.admin_required_view)]
-        else:
-            urlpatterns = [path('admin-required/', views.admin_required_view)]
+        urlpatterns = [path('admin-required/', views.admin_required_view)]
         """,
         "urls.py",
     )
@@ -556,9 +520,6 @@ def test_custom_user_model(admin_client):
     django_testdir.create_app_file("", "migrations/__init__.py")
     django_testdir.create_app_file(
         """
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
 from django.db import models, migrations
 import django.utils.timezone
 import django.core.validators
diff --git a/tests/test_unittest.py b/tests/test_unittest.py
--- a/tests/test_unittest.py
+++ b/tests/test_unittest.py
@@ -1,7 +1,6 @@
 import pytest
 from django.test import TestCase
 
-from pytest_django.plugin import _pytest_version_info
 from pytest_django_test.app.models import Item
 
 
@@ -161,16 +160,8 @@ def test_pass(self):
         result = django_testdir.runpytest_subprocess("-v", "-s")
         expected_lines = [
             "* ERROR at setup of TestFoo.test_pass *",
+            "E * TypeError: *",
         ]
-        if _pytest_version_info < (4, 2):
-            expected_lines += [
-                "E *Failed: <class 'tpkg.test_the_test.TestFoo'>.setUpClass should be a classmethod",  # noqa:E501
-            ]
-        else:
-            expected_lines += [
-                "E * TypeError: *",
-            ]
-
         result.stdout.fnmatch_lines(expected_lines)
         assert result.ret == 1
 
@@ -217,7 +208,7 @@ def test_setUpClass_mixin(self, django_testdir):
             """
             from django.test import TestCase
 
-            class TheMixin(object):
+            class TheMixin:
                 @classmethod
                 def setUpClass(cls):
                     super(TheMixin, cls).setUpClass()
@@ -289,7 +280,7 @@ def test_multi_inheritance_setUpClass(self, django_testdir):
             # Using a mixin is a regression test, see #280 for more details:
             # https://github.com/pytest-dev/pytest-django/issues/280
 
-            class SomeMixin(object):
+            class SomeMixin:
                 pass
 
             class TestA(SomeMixin, TestCase):
diff --git a/tests/test_urls.py b/tests/test_urls.py
--- a/tests/test_urls.py
+++ b/tests/test_urls.py
@@ -1,14 +1,11 @@
 import pytest
 from django.conf import settings
+from django.urls import is_valid_path
 from django.utils.encoding import force_str
 
 
 @pytest.mark.urls("pytest_django_test.urls_overridden")
 def test_urls():
-    try:
-        from django.urls import is_valid_path
-    except ImportError:
-        from django.core.urlresolvers import is_valid_path
     assert settings.ROOT_URLCONF == "pytest_django_test.urls_overridden"
     assert is_valid_path("/overridden_url/")
 
@@ -22,21 +19,18 @@ def test_urls_client(client):
 def test_urls_cache_is_cleared(testdir):
     testdir.makepyfile(
         myurls="""
-        from django.conf.urls import url
+        from django.urls import path
 
         def fake_view(request):
             pass
 
-        urlpatterns = [url(r'first/$', fake_view, name='first')]
+        urlpatterns = [path('first', fake_view, name='first')]
     """
     )
 
     testdir.makepyfile(
         """
-        try:
-            from django.urls import reverse, NoReverseMatch
-        except ImportError:  # Django < 2.0
-            from django.core.urlresolvers import reverse, NoReverseMatch
+        from django.urls import reverse, NoReverseMatch
         import pytest
 
         @pytest.mark.urls('myurls')
@@ -58,32 +52,29 @@ def test_something_else():
 def test_urls_cache_is_cleared_and_new_urls_can_be_assigned(testdir):
     testdir.makepyfile(
         myurls="""
-        from django.conf.urls import url
+        from django.urls import path
 
         def fake_view(request):
             pass
 
-        urlpatterns = [url(r'first/$', fake_view, name='first')]
+        urlpatterns = [path('first', fake_view, name='first')]
     """
     )
 
     testdir.makepyfile(
         myurls2="""
-        from django.conf.urls import url
+        from django.urls import path
 
         def fake_view(request):
             pass
 
-        urlpatterns = [url(r'second/$', fake_view, name='second')]
+        urlpatterns = [path('second', fake_view, name='second')]
     """
     )
 
     testdir.makepyfile(
         """
-        try:
-            from django.urls import reverse, NoReverseMatch
-        except ImportError:  # Django < 2.0
-            from django.core.urlresolvers import reverse, NoReverseMatch
+        from django.urls import reverse, NoReverseMatch
         import pytest
 
         @pytest.mark.urls('myurls')