diff --git a/django/contrib/auth/password_validation.py b/django/contrib/auth/password_validation.py index 845f4d86d5..7c9e75e62e 100644 --- a/django/contrib/auth/password_validation.py +++ b/django/contrib/auth/password_validation.py @@ -8,7 +8,7 @@ from django.conf import settings from django.core.exceptions import ( FieldDoesNotExist, ImproperlyConfigured, ValidationError, ) -from django.utils.functional import lazy +from django.utils.functional import cached_property, lazy from django.utils.html import format_html, format_html_join from django.utils.module_loading import import_string from django.utils.translation import gettext as _, ngettext @@ -167,9 +167,14 @@ class CommonPasswordValidator: https://gist.github.com/roycewilliams/281ce539915a947a23db17137d91aeb7 The password list must be lowercased to match the comparison in validate(). """ - DEFAULT_PASSWORD_LIST_PATH = Path(__file__).resolve().parent / 'common-passwords.txt.gz' + + @cached_property + def DEFAULT_PASSWORD_LIST_PATH(self): + return Path(__file__).resolve().parent / 'common-passwords.txt.gz' def __init__(self, password_list_path=DEFAULT_PASSWORD_LIST_PATH): + if password_list_path is CommonPasswordValidator.DEFAULT_PASSWORD_LIST_PATH: + password_list_path = self.DEFAULT_PASSWORD_LIST_PATH try: with gzip.open(password_list_path, 'rt', encoding='utf-8') as f: self.passwords = {x.strip() for x in f} diff --git a/django/forms/renderers.py b/django/forms/renderers.py index dcf3d92302..ce3b7097e6 100644 --- a/django/forms/renderers.py +++ b/django/forms/renderers.py @@ -7,8 +7,6 @@ from django.template.loader import get_template from django.utils.functional import cached_property from django.utils.module_loading import import_string -ROOT = Path(__file__).parent - @functools.lru_cache() def get_default_renderer(): @@ -33,7 +31,7 @@ class EngineMixin: def engine(self): return self.backend({ 'APP_DIRS': True, - 'DIRS': [ROOT / self.backend.app_dirname], + 'DIRS': [Path(__file__).parent / self.backend.app_dirname], 'NAME': 'djangoforms', 'OPTIONS': {}, }) diff --git a/django/utils/version.py b/django/utils/version.py index 50be432942..d8437ad07e 100644 --- a/django/utils/version.py +++ b/django/utils/version.py @@ -77,6 +77,10 @@ def get_git_changeset(): This value isn't guaranteed to be unique, but collisions are very unlikely, so it's sufficient for generating the development version numbers. """ + # Repository may not be found if __file__ is undefined, e.g. in a frozen + # module. + if '__file__' not in globals(): + return None repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) git_log = subprocess.run( 'git log --pretty=format:%ct --quiet -1 HEAD', diff --git a/django/views/debug.py b/django/views/debug.py index 1b8637874a..67bb5de20b 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -26,7 +26,15 @@ DEBUG_ENGINE = Engine( libraries={'i18n': 'django.templatetags.i18n'}, ) -CURRENT_DIR = Path(__file__).parent + +def builtin_template_path(name): + """ + Return a path to a builtin template. + + Avoid calling this function at the module level or in a class-definition + because __file__ may not exist, e.g. in frozen environments. + """ + return Path(__file__).parent / 'templates' / name class ExceptionCycleWarning(UserWarning): @@ -248,11 +256,11 @@ class ExceptionReporter: @property def html_template_path(self): - return CURRENT_DIR / 'templates' / 'technical_500.html' + return builtin_template_path('technical_500.html') @property def text_template_path(self): - return CURRENT_DIR / 'templates' / 'technical_500.txt' + return builtin_template_path('technical_500.txt') def __init__(self, request, exc_type, exc_value, tb, is_email=False): self.request = request @@ -534,7 +542,7 @@ def technical_404_response(request, exception): module = obj.__module__ caller = '%s.%s' % (module, caller) - with Path(CURRENT_DIR, 'templates', 'technical_404.html').open(encoding='utf-8') as fh: + with builtin_template_path('technical_404.html').open(encoding='utf-8') as fh: t = DEBUG_ENGINE.from_string(fh.read()) reporter_filter = get_default_exception_reporter_filter() c = Context({ @@ -553,7 +561,7 @@ def technical_404_response(request, exception): def default_urlconf(request): """Create an empty URLconf 404 error response.""" - with Path(CURRENT_DIR, 'templates', 'default_urlconf.html').open(encoding='utf-8') as fh: + with builtin_template_path('default_urlconf.html').open(encoding='utf-8') as fh: t = DEBUG_ENGINE.from_string(fh.read()) c = Context({ 'version': get_docs_version(), diff --git a/tests/version/tests.py b/tests/version/tests.py index bfa4af0496..7cf4dce0a6 100644 --- a/tests/version/tests.py +++ b/tests/version/tests.py @@ -1,17 +1,37 @@ +from unittest import skipUnless + +import django.utils.version from django import get_version from django.test import SimpleTestCase -from django.utils.version import get_complete_version, get_version_tuple +from django.utils.version import ( + get_complete_version, get_git_changeset, get_version_tuple, +) class VersionTests(SimpleTestCase): def test_development(self): + get_git_changeset.cache_clear() ver_tuple = (1, 4, 0, 'alpha', 0) # This will return a different result when it's run within or outside # of a git clone: 1.4.devYYYYMMDDHHMMSS or 1.4. ver_string = get_version(ver_tuple) self.assertRegex(ver_string, r'1\.4(\.dev[0-9]+)?') + @skipUnless( + hasattr(django.utils.version, '__file__'), + 'test_development() checks the same when __file__ is already missing, ' + 'e.g. in a frozen environments' + ) + def test_development_no_file(self): + get_git_changeset.cache_clear() + version_file = django.utils.version.__file__ + try: + del django.utils.version.__file__ + self.test_development() + finally: + django.utils.version.__file__ = version_file + def test_releases(self): tuples_to_strings = ( ((1, 4, 0, 'alpha', 1), '1.4a1'),