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