Skip to content

Commit

Permalink
Fixed #32316 -- Deferred accessing __file__.
Browse files Browse the repository at this point in the history
Deferred accessing the module-global variable __file__ because the
Python import API does not guarantee it always exists—in particular, it
does not exist in certain "frozen" environments. The following changes
advanced this goal.

Thanks to Carlton Gibson, Tom Forbes, Mariusz Felisiak, and Shreyas
Ravi for review and feedback.
  • Loading branch information
wkschwartz authored and felixxm committed Apr 1, 2021
1 parent cfe47b7 commit 9ee693b
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 11 deletions.
9 changes: 7 additions & 2 deletions django/contrib/auth/password_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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
Expand Down Expand Up @@ -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}
Expand Down
4 changes: 1 addition & 3 deletions django/forms/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
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():
Expand All @@ -33,7 +31,7 @@ def get_template(self, template_name):
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': {},
})
Expand Down
4 changes: 4 additions & 0 deletions django/utils/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
18 changes: 13 additions & 5 deletions django/views/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@
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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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({
Expand All @@ -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(),
Expand Down
22 changes: 21 additions & 1 deletion tests/version/tests.py
Original file line number Diff line number Diff line change
@@ -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'),
Expand Down

0 comments on commit 9ee693b

Please sign in to comment.