From 77aa74cb70dd85497dbade6bc0f394aa41e88c94 Mon Sep 17 00:00:00 2001
From: Jon Dufresne <jon.dufresne@gmail.com>
Date: Thu, 7 Nov 2019 01:26:22 -0800
Subject: [PATCH] Refs #29983 -- Added support for using pathlib.Path in all
 settings.

---
 .../management/commands/findstatic.py          |  2 +-
 django/db/backends/sqlite3/base.py             |  4 +++-
 django/db/backends/sqlite3/creation.py         |  5 ++++-
 django/forms/renderers.py                      |  2 +-
 django/template/utils.py                       |  2 +-
 docs/releases/3.1.txt                          |  8 +++++++-
 tests/backends/sqlite/tests.py                 | 18 +++++++++++++++++-
 tests/i18n/test_compilation.py                 |  9 +++++++++
 tests/i18n/test_extraction.py                  |  9 ++++++++-
 tests/model_fields/test_filefield.py           |  9 +++++++++
 tests/sessions_tests/tests.py                  | 12 +++++++++++-
 tests/staticfiles_tests/cases.py               |  5 ++++-
 .../project/pathlib/pathlib.txt                |  1 +
 tests/staticfiles_tests/settings.py            |  2 ++
 tests/staticfiles_tests/test_management.py     | 11 +++++++++++
 tests/template_backends/test_django.py         | 12 ++++++++++++
 tests/template_backends/test_jinja2.py         | 11 +++++++++++
 .../test_localeregexdescriptor.py              |  6 ++++++
 18 files changed, 118 insertions(+), 10 deletions(-)
 create mode 100644 tests/staticfiles_tests/project/pathlib/pathlib.txt

diff --git a/django/contrib/staticfiles/management/commands/findstatic.py b/django/contrib/staticfiles/management/commands/findstatic.py
index cd58015788..fe3b53cbd8 100644
--- a/django/contrib/staticfiles/management/commands/findstatic.py
+++ b/django/contrib/staticfiles/management/commands/findstatic.py
@@ -21,7 +21,7 @@ class Command(LabelCommand):
         if verbosity >= 2:
             searched_locations = (
                 "\nLooking in the following locations:\n  %s" %
-                "\n  ".join(finders.searched_locations)
+                "\n  ".join([str(loc) for loc in finders.searched_locations])
             )
         else:
             searched_locations = ''
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 45a22f5a36..51a15ee10a 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -174,7 +174,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
                 "settings.DATABASES is improperly configured. "
                 "Please supply the NAME value.")
         kwargs = {
-            'database': settings_dict['NAME'],
+            # TODO: Remove str() when dropping support for PY36.
+            # https://bugs.python.org/issue33496
+            'database': str(settings_dict['NAME']),
             'detect_types': Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES,
             **settings_dict['OPTIONS'],
         }
diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py
index 3fcf668ced..d97052f52d 100644
--- a/django/db/backends/sqlite3/creation.py
+++ b/django/db/backends/sqlite3/creation.py
@@ -1,6 +1,7 @@
 import os
 import shutil
 import sys
+from pathlib import Path
 
 from django.db.backends.base.creation import BaseDatabaseCreation
 
@@ -9,7 +10,9 @@ class DatabaseCreation(BaseDatabaseCreation):
 
     @staticmethod
     def is_in_memory_db(database_name):
-        return database_name == ':memory:' or 'mode=memory' in database_name
+        return not isinstance(database_name, Path) and (
+            database_name == ':memory:' or 'mode=memory' in database_name
+        )
 
     def _get_test_db_name(self):
         test_database_name = self.connection.settings_dict['TEST']['NAME'] or ':memory:'
diff --git a/django/forms/renderers.py b/django/forms/renderers.py
index 9fb695bdb2..19fab493cf 100644
--- a/django/forms/renderers.py
+++ b/django/forms/renderers.py
@@ -39,7 +39,7 @@ class EngineMixin:
     def engine(self):
         return self.backend({
             'APP_DIRS': True,
-            'DIRS': [str(ROOT / self.backend.app_dirname)],
+            'DIRS': [ROOT / self.backend.app_dirname],
             'NAME': 'djangoforms',
             'OPTIONS': {},
         })
diff --git a/django/template/utils.py b/django/template/utils.py
index f4ed2750c2..2d30e1637a 100644
--- a/django/template/utils.py
+++ b/django/template/utils.py
@@ -99,7 +99,7 @@ def get_app_template_dirs(dirname):
     installed applications.
     """
     template_dirs = [
-        str(Path(app_config.path) / dirname)
+        Path(app_config.path) / dirname
         for app_config in apps.get_app_configs()
         if app_config.path and (Path(app_config.path) / dirname).is_dir()
     ]
diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt
index 37a3ea9e85..84a7e33b03 100644
--- a/docs/releases/3.1.txt
+++ b/docs/releases/3.1.txt
@@ -96,7 +96,7 @@ Minor features
 :mod:`django.contrib.staticfiles`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-* ...
+* The :setting:`STATICFILES_DIRS` setting now supports :class:`pathlib.Path`.
 
 :mod:`django.contrib.syndication`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -226,6 +226,12 @@ Validators
 
 * ...
 
+Miscellaneous
+~~~~~~~~~~~~~
+
+* The SQLite backend now supports :class:`pathlib.Path` for the ``NAME``
+  setting.
+
 .. _backwards-incompatible-3.1:
 
 Backwards incompatible changes in 3.1
diff --git a/tests/backends/sqlite/tests.py b/tests/backends/sqlite/tests.py
index 21be45fb11..3447fb6096 100644
--- a/tests/backends/sqlite/tests.py
+++ b/tests/backends/sqlite/tests.py
@@ -1,11 +1,14 @@
+import os
 import re
+import tempfile
 import threading
 import unittest
+from pathlib import Path
 from sqlite3 import dbapi2
 from unittest import mock
 
 from django.core.exceptions import ImproperlyConfigured
-from django.db import connection, transaction
+from django.db import ConnectionHandler, connection, transaction
 from django.db.models import Avg, StdDev, Sum, Variance
 from django.db.models.aggregates import Aggregate
 from django.db.models.fields import CharField
@@ -89,6 +92,19 @@ class Tests(TestCase):
                 value = bool(value) if value in {0, 1} else value
                 self.assertIs(value, expected)
 
+    def test_pathlib_name(self):
+        with tempfile.TemporaryDirectory() as tmp:
+            settings_dict = {
+                'default': {
+                    'ENGINE': 'django.db.backends.sqlite3',
+                    'NAME': Path(tmp) / 'test.db',
+                },
+            }
+            connections = ConnectionHandler(settings_dict)
+            connections['default'].ensure_connection()
+            connections['default'].close()
+            self.assertTrue(os.path.isfile(os.path.join(tmp, 'test.db')))
+
 
 @unittest.skipUnless(connection.vendor == 'sqlite', 'SQLite tests')
 @isolate_apps('backends')
diff --git a/tests/i18n/test_compilation.py b/tests/i18n/test_compilation.py
index 91e0714cec..cda5592155 100644
--- a/tests/i18n/test_compilation.py
+++ b/tests/i18n/test_compilation.py
@@ -227,3 +227,12 @@ class AppCompilationTest(ProjectAndAppTests):
         call_command('compilemessages', locale=[self.LOCALE], stdout=StringIO())
         self.assertTrue(os.path.exists(self.PROJECT_MO_FILE))
         self.assertTrue(os.path.exists(self.APP_MO_FILE))
+
+
+class PathLibLocaleCompilationTests(MessageCompilationTests):
+    work_subdir = 'exclude'
+
+    def test_locale_paths_pathlib(self):
+        with override_settings(LOCALE_PATHS=[Path(self.test_dir) / 'canned_locale']):
+            call_command('compilemessages', locale=['fr'], stdout=StringIO())
+            self.assertTrue(os.path.exists('canned_locale/fr/LC_MESSAGES/django.mo'))
diff --git a/tests/i18n/test_extraction.py b/tests/i18n/test_extraction.py
index 74ccb84d61..e18a335135 100644
--- a/tests/i18n/test_extraction.py
+++ b/tests/i18n/test_extraction.py
@@ -5,6 +5,7 @@ import tempfile
 import time
 import warnings
 from io import StringIO
+from pathlib import Path
 from unittest import mock, skipIf, skipUnless
 
 from admin_scripts.tests import AdminScriptTestCase
@@ -735,11 +736,17 @@ class CustomLayoutExtractionTests(ExtractorTests):
             management.call_command('makemessages', locale=LOCALE, verbosity=0)
 
     def test_project_locale_paths(self):
+        self._test_project_locale_paths(os.path.join(self.test_dir, 'project_locale'))
+
+    def test_project_locale_paths_pathlib(self):
+        self._test_project_locale_paths(Path(self.test_dir) / 'project_locale')
+
+    def _test_project_locale_paths(self, locale_path):
         """
         * translations for an app containing a locale folder are stored in that folder
         * translations outside of that app are in LOCALE_PATHS[0]
         """
-        with override_settings(LOCALE_PATHS=[os.path.join(self.test_dir, 'project_locale')]):
+        with override_settings(LOCALE_PATHS=[locale_path]):
             management.call_command('makemessages', locale=[LOCALE], verbosity=0)
             project_de_locale = os.path.join(
                 self.test_dir, 'project_locale', 'de', 'LC_MESSAGES', 'django.po')
diff --git a/tests/model_fields/test_filefield.py b/tests/model_fields/test_filefield.py
index 9642e7e80b..8150260476 100644
--- a/tests/model_fields/test_filefield.py
+++ b/tests/model_fields/test_filefield.py
@@ -1,6 +1,8 @@
 import os
 import sys
+import tempfile
 import unittest
+from pathlib import Path
 
 from django.core.files import temp
 from django.core.files.base import ContentFile
@@ -94,3 +96,10 @@ class FileFieldTests(TestCase):
         # open() doesn't write to disk.
         d.myfile.file = ContentFile(b'', name='bla')
         self.assertEqual(d.myfile, d.myfile.open())
+
+    def test_media_root_pathlib(self):
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            with override_settings(MEDIA_ROOT=Path(tmp_dir)):
+                with TemporaryUploadedFile('foo.txt', 'text/plain', 1, 'utf-8') as tmp_file:
+                    Document.objects.create(myfile=tmp_file)
+                    self.assertTrue(os.path.exists(os.path.join(tmp_dir, 'unused', 'foo.txt')))
diff --git a/tests/sessions_tests/tests.py b/tests/sessions_tests/tests.py
index 24e4e0c81b..ebdd311816 100644
--- a/tests/sessions_tests/tests.py
+++ b/tests/sessions_tests/tests.py
@@ -6,6 +6,7 @@ import tempfile
 import unittest
 from datetime import timedelta
 from http import cookies
+from pathlib import Path
 
 from django.conf import settings
 from django.contrib.sessions.backends.base import UpdateError
@@ -521,7 +522,7 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase):
     def setUp(self):
         # Do file session tests in an isolated directory, and kill it after we're done.
         self.original_session_file_path = settings.SESSION_FILE_PATH
-        self.temp_session_store = settings.SESSION_FILE_PATH = tempfile.mkdtemp()
+        self.temp_session_store = settings.SESSION_FILE_PATH = self.mkdtemp()
         # Reset the file session backend's internal caches
         if hasattr(self.backend, '_storage_path'):
             del self.backend._storage_path
@@ -532,6 +533,9 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase):
         settings.SESSION_FILE_PATH = self.original_session_file_path
         shutil.rmtree(self.temp_session_store)
 
+    def mkdtemp(self):
+        return tempfile.mkdtemp()
+
     @override_settings(
         SESSION_FILE_PATH='/if/this/directory/exists/you/have/a/weird/computer',
     )
@@ -598,6 +602,12 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase):
         self.assertEqual(1, count_sessions())
 
 
+class FileSessionPathLibTests(FileSessionTests):
+    def mkdtemp(self):
+        tmp_dir = super().mkdtemp()
+        return Path(tmp_dir)
+
+
 class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
 
     backend = CacheSession
diff --git a/tests/staticfiles_tests/cases.py b/tests/staticfiles_tests/cases.py
index 24de4e029e..4e767d9cb0 100644
--- a/tests/staticfiles_tests/cases.py
+++ b/tests/staticfiles_tests/cases.py
@@ -64,7 +64,7 @@ class CollectionTestCase(BaseStaticFilesMixin, SimpleTestCase):
 
     def setUp(self):
         super().setUp()
-        temp_dir = tempfile.mkdtemp()
+        temp_dir = self.mkdtemp()
         # Override the STATIC_ROOT for all tests from setUp to tearDown
         # rather than as a context manager
         self.patched_settings = self.settings(STATIC_ROOT=temp_dir)
@@ -78,6 +78,9 @@ class CollectionTestCase(BaseStaticFilesMixin, SimpleTestCase):
         self.patched_settings.disable()
         super().tearDown()
 
+    def mkdtemp(self):
+        return tempfile.mkdtemp()
+
     def run_collectstatic(self, *, verbosity=0, **kwargs):
         call_command('collectstatic', interactive=False, verbosity=verbosity,
                      ignore_patterns=['*.ignoreme'], **kwargs)
diff --git a/tests/staticfiles_tests/project/pathlib/pathlib.txt b/tests/staticfiles_tests/project/pathlib/pathlib.txt
new file mode 100644
index 0000000000..c7709d3d41
--- /dev/null
+++ b/tests/staticfiles_tests/project/pathlib/pathlib.txt
@@ -0,0 +1 @@
+pathlib
diff --git a/tests/staticfiles_tests/settings.py b/tests/staticfiles_tests/settings.py
index 1320da7a0d..444450358f 100644
--- a/tests/staticfiles_tests/settings.py
+++ b/tests/staticfiles_tests/settings.py
@@ -1,4 +1,5 @@
 import os.path
+from pathlib import Path
 
 TEST_ROOT = os.path.dirname(__file__)
 
@@ -10,6 +11,7 @@ TEST_SETTINGS = {
     'STATICFILES_DIRS': [
         os.path.join(TEST_ROOT, 'project', 'documents'),
         ('prefix', os.path.join(TEST_ROOT, 'project', 'prefixed')),
+        Path(TEST_ROOT) / 'project' / 'pathlib',
     ],
     'STATICFILES_FINDERS': [
         'django.contrib.staticfiles.finders.FileSystemFinder',
diff --git a/tests/staticfiles_tests/test_management.py b/tests/staticfiles_tests/test_management.py
index 7630efbd9b..1236d533d3 100644
--- a/tests/staticfiles_tests/test_management.py
+++ b/tests/staticfiles_tests/test_management.py
@@ -4,6 +4,7 @@ import shutil
 import tempfile
 import unittest
 from io import StringIO
+from pathlib import Path
 from unittest import mock
 
 from admin_scripts.tests import AdminScriptTestCase
@@ -102,6 +103,7 @@ class TestFindStatic(TestDefaults, CollectionTestCase):
         # FileSystemFinder searched locations
         self.assertIn(TEST_SETTINGS['STATICFILES_DIRS'][1][1], searched_locations)
         self.assertIn(TEST_SETTINGS['STATICFILES_DIRS'][0], searched_locations)
+        self.assertIn(str(TEST_SETTINGS['STATICFILES_DIRS'][2]), searched_locations)
         # DefaultStorageFinder searched locations
         self.assertIn(
             os.path.join('staticfiles_tests', 'project', 'site_media', 'media'),
@@ -174,6 +176,15 @@ class TestCollection(TestDefaults, CollectionTestCase):
         self.assertFileNotFound('test/backup~')
         self.assertFileNotFound('test/CVS')
 
+    def test_pathlib(self):
+        self.assertFileContains('pathlib.txt', 'pathlib')
+
+
+class TestCollectionPathLib(TestCollection):
+    def mkdtemp(self):
+        tmp_dir = super().mkdtemp()
+        return Path(tmp_dir)
+
 
 class TestCollectionVerbosity(CollectionTestCase):
     copying_msg = 'Copying '
diff --git a/tests/template_backends/test_django.py b/tests/template_backends/test_django.py
index e7a4a03546..6f5035c741 100644
--- a/tests/template_backends/test_django.py
+++ b/tests/template_backends/test_django.py
@@ -1,3 +1,5 @@
+from pathlib import Path
+
 from template_tests.test_response import test_processor_name
 
 from django.template import Context, EngineHandler, RequestContext
@@ -164,3 +166,13 @@ class DjangoTemplatesTests(TemplateStringsTests):
     def test_debug_default_template_loaders(self):
         engine = DjangoTemplates({'DIRS': [], 'APP_DIRS': True, 'NAME': 'django', 'OPTIONS': {}})
         self.assertEqual(engine.engine.loaders, self.default_loaders)
+
+    def test_dirs_pathlib(self):
+        engine = DjangoTemplates({
+            'DIRS': [Path(__file__).parent / 'templates' / 'template_backends'],
+            'APP_DIRS': False,
+            'NAME': 'django',
+            'OPTIONS': {},
+        })
+        template = engine.get_template('hello.html')
+        self.assertEqual(template.render({'name': 'Joe'}), 'Hello Joe!\n')
diff --git a/tests/template_backends/test_jinja2.py b/tests/template_backends/test_jinja2.py
index 117719fa0d..a454e93a39 100644
--- a/tests/template_backends/test_jinja2.py
+++ b/tests/template_backends/test_jinja2.py
@@ -1,3 +1,4 @@
+from pathlib import Path
 from unittest import skipIf
 
 from django.template import TemplateSyntaxError
@@ -85,3 +86,13 @@ class Jinja2Tests(TemplateStringsTests):
         with self.settings(STATIC_URL='/s/'):
             content = template.render(request=request)
         self.assertEqual(content, 'Static URL: /s/')
+
+    def test_dirs_pathlib(self):
+        engine = Jinja2({
+            'DIRS': [Path(__file__).parent / 'templates' / 'template_backends'],
+            'APP_DIRS': False,
+            'NAME': 'jinja2',
+            'OPTIONS': {},
+        })
+        template = engine.get_template('hello.html')
+        self.assertEqual(template.render({'name': 'Joe'}), 'Hello Joe!')
diff --git a/tests/urlpatterns_reverse/test_localeregexdescriptor.py b/tests/urlpatterns_reverse/test_localeregexdescriptor.py
index 25e6cd962a..32e36569f0 100644
--- a/tests/urlpatterns_reverse/test_localeregexdescriptor.py
+++ b/tests/urlpatterns_reverse/test_localeregexdescriptor.py
@@ -1,4 +1,5 @@
 import os
+from pathlib import Path
 from unittest import mock
 
 from django.core.exceptions import ImproperlyConfigured
@@ -52,3 +53,8 @@ class LocaleRegexDescriptorTests(SimpleTestCase):
 
     def test_access_locale_regex_descriptor(self):
         self.assertIsInstance(RegexPattern.regex, LocaleRegexDescriptor)
+
+
+@override_settings(LOCALE_PATHS=[Path(here) / 'translations' / 'locale'])
+class LocaleRegexDescriptorPathLibTests(LocaleRegexDescriptorTests):
+    pass