测试gitnore

This commit is contained in:
ladeng07
2022-05-06 15:45:57 +08:00
parent 12f390949b
commit 51552904f9
2347 changed files with 120102 additions and 53549 deletions
@@ -5,9 +5,9 @@ from django.utils.translation import gettext_lazy as _
class StaticFilesConfig(AppConfig):
name = "django.contrib.staticfiles"
name = 'django.contrib.staticfiles'
verbose_name = _("Static Files")
ignore_patterns = ["CVS", ".*", "*~"]
ignore_patterns = ['CVS', '.*', '*~']
def ready(self):
checks.register(check_finders, checks.Tags.staticfiles)
@@ -4,9 +4,11 @@ import os
from django.apps import apps
from django.conf import settings
from django.contrib.staticfiles import utils
from django.core.checks import Error, Warning
from django.core.checks import Error
from django.core.exceptions import ImproperlyConfigured
from django.core.files.storage import FileSystemStorage, Storage, default_storage
from django.core.files.storage import (
FileSystemStorage, Storage, default_storage,
)
from django.utils._os import safe_join
from django.utils.functional import LazyObject, empty
from django.utils.module_loading import import_string
@@ -19,11 +21,10 @@ class BaseFinder:
"""
A base file finder to be used for custom staticfiles finder classes.
"""
def check(self, **kwargs):
raise NotImplementedError(
"subclasses may provide a check() method to verify the finder is "
"configured correctly."
'subclasses may provide a check() method to verify the finder is '
'configured correctly.'
)
def find(self, path, all=False):
@@ -33,18 +34,14 @@ class BaseFinder:
If the ``all`` parameter is False (default) return only the first found
file path; if True, return a list of all found files paths.
"""
raise NotImplementedError(
"subclasses of BaseFinder must provide a find() method"
)
raise NotImplementedError('subclasses of BaseFinder must provide a find() method')
def list(self, ignore_patterns):
"""
Given an optional list of paths to ignore, return a two item iterable
consisting of the relative path and storage instance.
"""
raise NotImplementedError(
"subclasses of BaseFinder must provide a list() method"
)
raise NotImplementedError('subclasses of BaseFinder must provide a list() method')
class FileSystemFinder(BaseFinder):
@@ -52,7 +49,6 @@ class FileSystemFinder(BaseFinder):
A static files finder that uses the ``STATICFILES_DIRS`` setting
to locate files.
"""
def __init__(self, app_names=None, *args, **kwargs):
# List of locations with static files
self.locations = []
@@ -62,7 +58,7 @@ class FileSystemFinder(BaseFinder):
if isinstance(root, (list, tuple)):
prefix, root = root
else:
prefix = ""
prefix = ''
if (prefix, root) not in self.locations:
self.locations.append((prefix, root))
for prefix, root in self.locations:
@@ -74,43 +70,26 @@ class FileSystemFinder(BaseFinder):
def check(self, **kwargs):
errors = []
if not isinstance(settings.STATICFILES_DIRS, (list, tuple)):
errors.append(
Error(
"The STATICFILES_DIRS setting is not a tuple or list.",
hint="Perhaps you forgot a trailing comma?",
id="staticfiles.E001",
)
)
return errors
errors.append(Error(
'The STATICFILES_DIRS setting is not a tuple or list.',
hint='Perhaps you forgot a trailing comma?',
id='staticfiles.E001',
))
for root in settings.STATICFILES_DIRS:
if isinstance(root, (list, tuple)):
prefix, root = root
if prefix.endswith("/"):
errors.append(
Error(
"The prefix %r in the STATICFILES_DIRS setting must "
"not end with a slash." % prefix,
id="staticfiles.E003",
)
)
if settings.STATIC_ROOT and os.path.abspath(
settings.STATIC_ROOT
) == os.path.abspath(root):
errors.append(
Error(
"The STATICFILES_DIRS setting should not contain the "
"STATIC_ROOT setting.",
id="staticfiles.E002",
)
)
if not os.path.isdir(root):
errors.append(
Warning(
f"The directory '{root}' in the STATICFILES_DIRS setting "
f"does not exist.",
id="staticfiles.W004",
)
)
if prefix.endswith('/'):
errors.append(Error(
'The prefix %r in the STATICFILES_DIRS setting must '
'not end with a slash.' % prefix,
id='staticfiles.E003',
))
if settings.STATIC_ROOT and os.path.abspath(settings.STATIC_ROOT) == os.path.abspath(root):
errors.append(Error(
'The STATICFILES_DIRS setting should not contain the '
'STATIC_ROOT setting.',
id='staticfiles.E002',
))
return errors
def find(self, path, all=False):
@@ -134,10 +113,10 @@ class FileSystemFinder(BaseFinder):
absolute path (or ``None`` if no match).
"""
if prefix:
prefix = "%s%s" % (prefix, os.sep)
prefix = '%s%s' % (prefix, os.sep)
if not path.startswith(prefix):
return None
path = path[len(prefix) :]
path = path[len(prefix):]
path = safe_join(root, path)
if os.path.exists(path):
return path
@@ -147,11 +126,9 @@ class FileSystemFinder(BaseFinder):
List all files in all locations.
"""
for prefix, root in self.locations:
# Skip nonexistent directories.
if os.path.isdir(root):
storage = self.storages[root]
for path in utils.get_files(storage, ignore_patterns):
yield path, storage
storage = self.storages[root]
for path in utils.get_files(storage, ignore_patterns):
yield path, storage
class AppDirectoriesFinder(BaseFinder):
@@ -159,9 +136,8 @@ class AppDirectoriesFinder(BaseFinder):
A static files finder that looks in the directory of each app as
specified in the source_dir attribute.
"""
storage_class = FileSystemStorage
source_dir = "static"
source_dir = 'static'
def __init__(self, app_names=None, *args, **kwargs):
# The list of apps that are handled
@@ -174,8 +150,7 @@ class AppDirectoriesFinder(BaseFinder):
app_configs = [ac for ac in app_configs if ac.name in app_names]
for app_config in app_configs:
app_storage = self.storage_class(
os.path.join(app_config.path, self.source_dir)
)
os.path.join(app_config.path, self.source_dir))
if os.path.isdir(app_storage.location):
self.storages[app_config.name] = app_storage
if app_config.name not in self.apps:
@@ -187,7 +162,7 @@ class AppDirectoriesFinder(BaseFinder):
List all files in all app storages.
"""
for storage in self.storages.values():
if storage.exists(""): # check if storage location exists
if storage.exists(''): # check if storage location exists
for path in utils.get_files(storage, ignore_patterns):
yield path, storage
@@ -224,18 +199,15 @@ class BaseStorageFinder(BaseFinder):
A base static files finder to be used to extended
with an own storage class.
"""
storage = None
def __init__(self, storage=None, *args, **kwargs):
if storage is not None:
self.storage = storage
if self.storage is None:
raise ImproperlyConfigured(
"The staticfiles storage finder %r "
"doesn't have a storage class "
"assigned." % self.__class__
)
raise ImproperlyConfigured("The staticfiles storage finder %r "
"doesn't have a storage class "
"assigned." % self.__class__)
# Make sure we have a storage instance here.
if not isinstance(self.storage, (Storage, LazyObject)):
self.storage = self.storage()
@@ -246,7 +218,7 @@ class BaseStorageFinder(BaseFinder):
Look for files in the default file storage, if it's local.
"""
try:
self.storage.path("")
self.storage.path('')
except NotImplementedError:
pass
else:
@@ -271,18 +243,15 @@ class DefaultStorageFinder(BaseStorageFinder):
"""
A static files finder that uses the default storage backend.
"""
storage = default_storage
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
base_location = getattr(self.storage, "base_location", empty)
base_location = getattr(self.storage, 'base_location', empty)
if not base_location:
raise ImproperlyConfigured(
"The storage backend of the "
"staticfiles finder %r doesn't have "
"a valid location." % self.__class__
)
raise ImproperlyConfigured("The storage backend of the "
"staticfiles finder %r doesn't have "
"a valid location." % self.__class__)
def find(path, all=False):
@@ -320,7 +289,6 @@ def get_finder(import_path):
"""
Finder = import_string(import_path)
if not issubclass(Finder, BaseFinder):
raise ImproperlyConfigured(
'Finder "%s" is not a subclass of "%s"' % (Finder, BaseFinder)
)
raise ImproperlyConfigured('Finder "%s" is not a subclass of "%s"' %
(Finder, BaseFinder))
return Finder()
@@ -16,7 +16,6 @@ class StaticFilesHandlerMixin:
"""
Common methods used by WSGI and ASGI handlers.
"""
# May be used to differentiate between handler types (e.g. in a
# request_finished signal)
handles_files = True
@@ -42,7 +41,7 @@ class StaticFilesHandlerMixin:
"""
Return the relative path to the media file on disk for the given URL.
"""
relative_url = url[len(self.base_url[2]) :]
relative_url = url[len(self.base_url[2]):]
return url2pathname(relative_url)
def serve(self, request):
@@ -59,9 +58,7 @@ class StaticFilesHandlerMixin:
try:
return await sync_to_async(self.serve, thread_sensitive=False)(request)
except Http404 as e:
return await sync_to_async(response_for_exception, thread_sensitive=False)(
request, e
)
return await sync_to_async(response_for_exception, thread_sensitive=False)(request, e)
class StaticFilesHandler(StaticFilesHandlerMixin, WSGIHandler):
@@ -69,7 +66,6 @@ class StaticFilesHandler(StaticFilesHandlerMixin, WSGIHandler):
WSGI middleware that intercepts calls to the static files directory, as
defined by the STATIC_URL setting, and serves those files.
"""
def __init__(self, application):
self.application = application
self.base_url = urlparse(self.get_base_url())
@@ -86,14 +82,13 @@ class ASGIStaticFilesHandler(StaticFilesHandlerMixin, ASGIHandler):
ASGI application which wraps another and intercepts requests for static
files, passing them off to Django's static file serving.
"""
def __init__(self, application):
self.application = application
self.base_url = urlparse(self.get_base_url())
async def __call__(self, scope, receive, send):
# Only even look at HTTP requests
if scope["type"] == "http" and self._should_handle(scope["path"]):
if scope['type'] == 'http' and self._should_handle(scope['path']):
# Serve static content
# (the one thing super() doesn't do is __call__, apparently)
return await super().__call__(scope, receive, send)
@@ -15,7 +15,6 @@ class Command(BaseCommand):
Copies or symlinks static files from different locations to the
settings.STATIC_ROOT.
"""
help = "Collect static files in a single location."
requires_system_checks = [Tags.staticfiles]
@@ -31,78 +30,58 @@ class Command(BaseCommand):
@cached_property
def local(self):
try:
self.storage.path("")
self.storage.path('')
except NotImplementedError:
return False
return True
def add_arguments(self, parser):
parser.add_argument(
"--noinput",
"--no-input",
action="store_false",
dest="interactive",
'--noinput', '--no-input', action='store_false', dest='interactive',
help="Do NOT prompt the user for input of any kind.",
)
parser.add_argument(
"--no-post-process",
action="store_false",
dest="post_process",
'--no-post-process', action='store_false', dest='post_process',
help="Do NOT post process collected files.",
)
parser.add_argument(
"-i",
"--ignore",
action="append",
default=[],
dest="ignore_patterns",
metavar="PATTERN",
'-i', '--ignore', action='append', default=[],
dest='ignore_patterns', metavar='PATTERN',
help="Ignore files or directories matching this glob-style "
"pattern. Use multiple times to ignore more.",
"pattern. Use multiple times to ignore more.",
)
parser.add_argument(
"-n",
"--dry-run",
action="store_true",
'-n', '--dry-run', action='store_true',
help="Do everything except modify the filesystem.",
)
parser.add_argument(
"-c",
"--clear",
action="store_true",
'-c', '--clear', action='store_true',
help="Clear the existing files using the storage "
"before trying to copy or link the original file.",
"before trying to copy or link the original file.",
)
parser.add_argument(
"-l",
"--link",
action="store_true",
'-l', '--link', action='store_true',
help="Create a symbolic link to each file instead of copying.",
)
parser.add_argument(
"--no-default-ignore",
action="store_false",
dest="use_default_ignore_patterns",
help=(
"Don't ignore the common private glob-style patterns (defaults to "
"'CVS', '.*' and '*~')."
),
'--no-default-ignore', action='store_false', dest='use_default_ignore_patterns',
help="Don't ignore the common private glob-style patterns (defaults to 'CVS', '.*' and '*~').",
)
def set_options(self, **options):
"""
Set instance variables based on an options dict
"""
self.interactive = options["interactive"]
self.verbosity = options["verbosity"]
self.symlink = options["link"]
self.clear = options["clear"]
self.dry_run = options["dry_run"]
ignore_patterns = options["ignore_patterns"]
if options["use_default_ignore_patterns"]:
ignore_patterns += apps.get_app_config("staticfiles").ignore_patterns
self.interactive = options['interactive']
self.verbosity = options['verbosity']
self.symlink = options['link']
self.clear = options['clear']
self.dry_run = options['dry_run']
ignore_patterns = options['ignore_patterns']
if options['use_default_ignore_patterns']:
ignore_patterns += apps.get_app_config('staticfiles').ignore_patterns
self.ignore_patterns = list({os.path.normpath(p) for p in ignore_patterns})
self.post_process = options["post_process"]
self.post_process = options['post_process']
def collect(self):
"""
@@ -114,7 +93,7 @@ class Command(BaseCommand):
raise CommandError("Can't symlink to a remote destination.")
if self.clear:
self.clear_dir("")
self.clear_dir('')
if self.symlink:
handler = self.link_file
@@ -125,7 +104,7 @@ class Command(BaseCommand):
for finder in get_finders():
for path, storage in finder.list(self.ignore_patterns):
# Prefix the relative path if the source storage contains it
if getattr(storage, "prefix", None):
if getattr(storage, 'prefix', None):
prefixed_path = os.path.join(storage.prefix, path)
else:
prefixed_path = path
@@ -143,8 +122,9 @@ class Command(BaseCommand):
)
# Storage backends may define a post_process() method.
if self.post_process and hasattr(self.storage, "post_process"):
processor = self.storage.post_process(found_files, dry_run=self.dry_run)
if self.post_process and hasattr(self.storage, 'post_process'):
processor = self.storage.post_process(found_files,
dry_run=self.dry_run)
for original_path, processed_path, processed in processor:
if isinstance(processed, Exception):
self.stderr.write("Post-processing '%s' failed!" % original_path)
@@ -153,85 +133,75 @@ class Command(BaseCommand):
self.stderr.write()
raise processed
if processed:
self.log(
"Post-processed '%s' as '%s'" % (original_path, processed_path),
level=2,
)
self.log("Post-processed '%s' as '%s'" %
(original_path, processed_path), level=2)
self.post_processed_files.append(original_path)
else:
self.log("Skipped post-processing '%s'" % original_path)
return {
"modified": self.copied_files + self.symlinked_files,
"unmodified": self.unmodified_files,
"post_processed": self.post_processed_files,
'modified': self.copied_files + self.symlinked_files,
'unmodified': self.unmodified_files,
'post_processed': self.post_processed_files,
}
def handle(self, **options):
self.set_options(**options)
message = ["\n"]
message = ['\n']
if self.dry_run:
message.append(
"You have activated the --dry-run option so no files will be "
"modified.\n\n"
'You have activated the --dry-run option so no files will be modified.\n\n'
)
message.append(
"You have requested to collect static files at the destination\n"
"location as specified in your settings"
'You have requested to collect static files at the destination\n'
'location as specified in your settings'
)
if self.is_local_storage() and self.storage.location:
destination_path = self.storage.location
message.append(":\n\n %s\n\n" % destination_path)
should_warn_user = self.storage.exists(destination_path) and any(
self.storage.listdir(destination_path)
message.append(':\n\n %s\n\n' % destination_path)
should_warn_user = (
self.storage.exists(destination_path) and
any(self.storage.listdir(destination_path))
)
else:
destination_path = None
message.append(".\n\n")
message.append('.\n\n')
# Destination files existence not checked; play it safe and warn.
should_warn_user = True
if self.interactive and should_warn_user:
if self.clear:
message.append("This will DELETE ALL FILES in this location!\n")
message.append('This will DELETE ALL FILES in this location!\n')
else:
message.append("This will overwrite existing files!\n")
message.append('This will overwrite existing files!\n')
message.append(
"Are you sure you want to do this?\n\n"
'Are you sure you want to do this?\n\n'
"Type 'yes' to continue, or 'no' to cancel: "
)
if input("".join(message)) != "yes":
if input(''.join(message)) != 'yes':
raise CommandError("Collecting static files cancelled.")
collected = self.collect()
if self.verbosity >= 1:
modified_count = len(collected["modified"])
unmodified_count = len(collected["unmodified"])
post_processed_count = len(collected["post_processed"])
modified_count = len(collected['modified'])
unmodified_count = len(collected['unmodified'])
post_processed_count = len(collected['post_processed'])
return (
"\n%(modified_count)s %(identifier)s %(action)s"
"%(destination)s%(unmodified)s%(post_processed)s."
) % {
"modified_count": modified_count,
"identifier": "static file" + ("" if modified_count == 1 else "s"),
"action": "symlinked" if self.symlink else "copied",
"destination": (
" to '%s'" % destination_path if destination_path else ""
),
"unmodified": (
", %s unmodified" % unmodified_count
if collected["unmodified"]
else ""
),
"post_processed": (
collected["post_processed"]
and ", %s post-processed" % post_processed_count
or ""
),
'modified_count': modified_count,
'identifier': 'static file' + ('' if modified_count == 1 else 's'),
'action': 'symlinked' if self.symlink else 'copied',
'destination': (" to '%s'" % destination_path if destination_path else ''),
'unmodified': (', %s unmodified' % unmodified_count if collected['unmodified'] else ''),
'post_processed': (collected['post_processed'] and
', %s post-processed'
% post_processed_count or ''),
}
def log(self, msg, level=2):
@@ -298,17 +268,16 @@ class Command(BaseCommand):
# previous collectstatic was with --link), the old
# links/files must be deleted so it's not safe to skip
# unmodified files.
can_skip_unmodified_files = not (
self.symlink ^ os.path.islink(full_path)
)
can_skip_unmodified_files = not (self.symlink ^ os.path.islink(full_path))
else:
# In remote storages, skipping is only based on the
# modified times since symlinks aren't relevant.
can_skip_unmodified_files = True
# Avoid sub-second precision (see #14665, #19540)
file_is_unmodified = target_last_modified.replace(
microsecond=0
) >= source_last_modified.replace(microsecond=0)
file_is_unmodified = (
target_last_modified.replace(microsecond=0) >=
source_last_modified.replace(microsecond=0)
)
if file_is_unmodified and can_skip_unmodified_files:
if prefixed_path not in self.unmodified_files:
self.unmodified_files.append(prefixed_path)
@@ -345,13 +314,14 @@ class Command(BaseCommand):
if os.path.lexists(full_path):
os.unlink(full_path)
os.symlink(source_path, full_path)
except AttributeError:
import platform
raise CommandError("Symlinking is not supported by Python %s." %
platform.python_version())
except NotImplementedError:
import platform
raise CommandError(
"Symlinking is not supported in this "
"platform (%s)." % platform.platform()
)
raise CommandError("Symlinking is not supported in this "
"platform (%s)." % platform.platform())
except OSError as e:
raise CommandError(e)
if prefixed_path not in self.symlinked_files:
@@ -6,43 +6,38 @@ from django.core.management.base import LabelCommand
class Command(LabelCommand):
help = "Finds the absolute paths for the given static file(s)."
label = "staticfile"
label = 'staticfile'
def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument(
"--first",
action="store_false",
dest="all",
'--first', action='store_false', dest='all',
help="Only return the first match for each static file.",
)
def handle_label(self, path, **options):
verbosity = options["verbosity"]
result = finders.find(path, all=options["all"])
verbosity = options['verbosity']
result = finders.find(path, all=options['all'])
if verbosity >= 2:
searched_locations = (
"\nLooking in the following locations:\n %s"
% "\n ".join([str(loc) for loc in finders.searched_locations])
"\nLooking in the following locations:\n %s" %
"\n ".join([str(loc) for loc in finders.searched_locations])
)
else:
searched_locations = ""
searched_locations = ''
if result:
if not isinstance(result, (list, tuple)):
result = [result]
result = (os.path.realpath(path) for path in result)
if verbosity >= 1:
file_list = "\n ".join(result)
return "Found '%s' here:\n %s%s" % (
path,
file_list,
searched_locations,
)
file_list = '\n '.join(result)
return ("Found '%s' here:\n %s%s" %
(path, file_list, searched_locations))
else:
return "\n".join(result)
return '\n'.join(result)
else:
message = ["No matching file found for '%s'." % path]
if verbosity >= 2:
message.append(searched_locations)
if verbosity >= 1:
self.stderr.write("\n".join(message))
self.stderr.write('\n'.join(message))
@@ -1,26 +1,22 @@
from django.conf import settings
from django.contrib.staticfiles.handlers import StaticFilesHandler
from django.core.management.commands.runserver import Command as RunserverCommand
from django.core.management.commands.runserver import (
Command as RunserverCommand,
)
class Command(RunserverCommand):
help = (
"Starts a lightweight web server for development and also serves static files."
)
help = "Starts a lightweight Web server for development and also serves static files."
def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument(
"--nostatic",
action="store_false",
dest="use_static_handler",
help="Tells Django to NOT automatically serve static files at STATIC_URL.",
'--nostatic', action="store_false", dest='use_static_handler',
help='Tells Django to NOT automatically serve static files at STATIC_URL.',
)
parser.add_argument(
"--insecure",
action="store_true",
dest="insecure_serving",
help="Allows serving static files even if DEBUG is False.",
'--insecure', action="store_true", dest='insecure_serving',
help='Allows serving static files even if DEBUG is False.',
)
def get_handler(self, *args, **options):
@@ -29,8 +25,8 @@ class Command(RunserverCommand):
if static files should be served. Otherwise return the default handler.
"""
handler = super().get_handler(*args, **options)
use_static_handler = options["use_static_handler"]
insecure_serving = options["insecure_serving"]
use_static_handler = options['use_static_handler']
insecure_serving = options['insecure_serving']
if use_static_handler and (settings.DEBUG or insecure_serving):
return StaticFilesHandler(handler)
return handler
@@ -20,7 +20,6 @@ class StaticFilesStorage(FileSystemStorage):
The defaults for ``location`` and ``base_url`` are
``STATIC_ROOT`` and ``STATIC_URL``.
"""
def __init__(self, location=None, base_url=None, *args, **kwargs):
if location is None:
location = settings.STATIC_ROOT
@@ -36,37 +35,20 @@ class StaticFilesStorage(FileSystemStorage):
def path(self, name):
if not self.location:
raise ImproperlyConfigured(
"You're using the staticfiles app "
"without having set the STATIC_ROOT "
"setting to a filesystem path."
)
raise ImproperlyConfigured("You're using the staticfiles app "
"without having set the STATIC_ROOT "
"setting to a filesystem path.")
return super().path(name)
class HashedFilesMixin:
default_template = """url("%(url)s")"""
default_template = """url("%s")"""
max_post_process_passes = 5
patterns = (
(
"*.css",
(
r"""(?P<matched>url\(['"]{0,1}\s*(?P<url>.*?)["']{0,1}\))""",
(
r"""(?P<matched>@import\s*["']\s*(?P<url>.*?)["'])""",
"""@import url("%(url)s")""",
),
),
),
(
"*.js",
(
(
r"(?m)(?P<matched>)^(//# (?-i:sourceMappingURL)=(?P<url>.*))$",
"//# sourceMappingURL=%(url)s",
),
),
),
("*.css", (
r"""(url\(['"]{0,1}\s*(.*?)["']{0,1}\))""",
(r"""(@import\s*["']\s*(.*?)["'])""", """@import url("%s")"""),
)),
)
keep_intermediate_files = True
@@ -103,9 +85,7 @@ class HashedFilesMixin:
opened = content is None
if opened:
if not self.exists(filename):
raise ValueError(
"The file '%s' could not be found with %r." % (filename, self)
)
raise ValueError("The file '%s' could not be found with %r." % (filename, self))
try:
content = self.open(filename)
except OSError:
@@ -118,14 +98,15 @@ class HashedFilesMixin:
content.close()
path, filename = os.path.split(clean_name)
root, ext = os.path.splitext(filename)
file_hash = (".%s" % file_hash) if file_hash else ""
hashed_name = os.path.join(path, "%s%s%s" % (root, file_hash, ext))
file_hash = ('.%s' % file_hash) if file_hash else ''
hashed_name = os.path.join(path, "%s%s%s" %
(root, file_hash, ext))
unparsed_name = list(parsed_name)
unparsed_name[2] = hashed_name
# Special casing for a @font-face hack, like url(myfont.eot?#iefix")
# http://www.fontspring.com/blog/the-new-bulletproof-font-face-syntax
if "?#" in name and not unparsed_name[3]:
unparsed_name[2] += "?"
if '?#' in name and not unparsed_name[3]:
unparsed_name[2] += '?'
return urlunsplit(unparsed_name)
def _url(self, hashed_name_func, name, force=False, hashed_files=None):
@@ -133,10 +114,10 @@ class HashedFilesMixin:
Return the non-hashed URL in DEBUG mode.
"""
if settings.DEBUG and not force:
hashed_name, fragment = name, ""
hashed_name, fragment = name, ''
else:
clean_name, fragment = urldefrag(name)
if urlsplit(clean_name).path.endswith("/"): # don't hash paths
if urlsplit(clean_name).path.endswith('/'): # don't hash paths
hashed_name = name
else:
args = (clean_name,)
@@ -148,13 +129,13 @@ class HashedFilesMixin:
# Special casing for a @font-face hack, like url(myfont.eot?#iefix")
# http://www.fontspring.com/blog/the-new-bulletproof-font-face-syntax
query_fragment = "?#" in name # [sic!]
query_fragment = '?#' in name # [sic!]
if fragment or query_fragment:
urlparts = list(urlsplit(final_url))
if fragment and not urlparts[4]:
urlparts[4] = fragment
if query_fragment and not urlparts[3]:
urlparts[2] += "?"
urlparts[2] += '?'
final_url = urlunsplit(urlparts)
return unquote(final_url)
@@ -179,50 +160,43 @@ class HashedFilesMixin:
This requires figuring out which files the matched URL resolves
to and calling the url() method of the storage.
"""
matches = matchobj.groupdict()
matched = matches["matched"]
url = matches["url"]
matched, url = matchobj.groups()
# Ignore absolute/protocol-relative and data-uri URLs.
if re.match(r"^[a-z]+:", url):
if re.match(r'^[a-z]+:', url):
return matched
# Ignore absolute URLs that don't point to a static file (dynamic
# CSS / JS?). Note that STATIC_URL cannot be empty.
if url.startswith("/") and not url.startswith(settings.STATIC_URL):
if url.startswith('/') and not url.startswith(settings.STATIC_URL):
return matched
# Strip off the fragment so a path-like fragment won't interfere.
url_path, fragment = urldefrag(url)
if url_path.startswith("/"):
if url_path.startswith('/'):
# Otherwise the condition above would have returned prematurely.
assert url_path.startswith(settings.STATIC_URL)
target_name = url_path[len(settings.STATIC_URL) :]
target_name = url_path[len(settings.STATIC_URL):]
else:
# We're using the posixpath module to mix paths and URLs conveniently.
source_name = name if os.sep == "/" else name.replace(os.sep, "/")
source_name = name if os.sep == '/' else name.replace(os.sep, '/')
target_name = posixpath.join(posixpath.dirname(source_name), url_path)
# Determine the hashed name of the target file with the storage backend.
hashed_url = self._url(
self._stored_name,
unquote(target_name),
force=True,
hashed_files=hashed_files,
self._stored_name, unquote(target_name),
force=True, hashed_files=hashed_files,
)
transformed_url = "/".join(
url_path.split("/")[:-1] + hashed_url.split("/")[-1:]
)
transformed_url = '/'.join(url_path.split('/')[:-1] + hashed_url.split('/')[-1:])
# Restore the fragment that was stripped off earlier.
if fragment:
transformed_url += ("?#" if "?#" in url else "#") + fragment
transformed_url += ('?#' if '?#' in url else '#') + fragment
# Return the hashed version to the file
matches["url"] = unquote(transformed_url)
return template % matches
return template % unquote(transformed_url)
return converter
@@ -249,46 +223,31 @@ class HashedFilesMixin:
# build a list of adjustable files
adjustable_paths = [
path for path in paths if matches_patterns(path, self._patterns)
path for path in paths
if matches_patterns(path, self._patterns)
]
# Adjustable files to yield at end, keyed by the original path.
processed_adjustable_paths = {}
# Do a single pass first. Post-process all files once, yielding not
# adjustable files and exceptions, and collecting adjustable files.
for name, hashed_name, processed, _ in self._post_process(
paths, adjustable_paths, hashed_files
):
if name not in adjustable_paths or isinstance(processed, Exception):
yield name, hashed_name, processed
else:
processed_adjustable_paths[name] = (name, hashed_name, processed)
# Do a single pass first. Post-process all files once, then repeat for
# adjustable files.
for name, hashed_name, processed, _ in self._post_process(paths, adjustable_paths, hashed_files):
yield name, hashed_name, processed
paths = {path: paths[path] for path in adjustable_paths}
substitutions = False
for i in range(self.max_post_process_passes):
substitutions = False
for name, hashed_name, processed, subst in self._post_process(
paths, adjustable_paths, hashed_files
):
# Overwrite since hashed_name may be newer.
processed_adjustable_paths[name] = (name, hashed_name, processed)
for name, hashed_name, processed, subst in self._post_process(paths, adjustable_paths, hashed_files):
yield name, hashed_name, processed
substitutions = substitutions or subst
if not substitutions:
break
if substitutions:
yield "All", None, RuntimeError("Max post-process passes exceeded.")
yield 'All', None, RuntimeError('Max post-process passes exceeded.')
# Store the processed paths
self.hashed_files.update(hashed_files)
# Yield adjustable files with final, hashed name.
yield from processed_adjustable_paths.values()
def _post_process(self, paths, adjustable_paths, hashed_files):
# Sort the files by directory level
def path_level(name):
@@ -311,7 +270,7 @@ class HashedFilesMixin:
hashed_name = hashed_files[hash_key]
# then get the original's file content..
if hasattr(original_file, "seek"):
if hasattr(original_file, 'seek'):
original_file.seek(0)
hashed_file_exists = self.exists(hashed_name)
@@ -320,13 +279,11 @@ class HashedFilesMixin:
# ..to apply each replacement pattern to the content
if name in adjustable_paths:
old_hashed_name = hashed_name
content = original_file.read().decode("utf-8")
content = original_file.read().decode('utf-8')
for extension, patterns in self._patterns.items():
if matches_patterns(path, (extension,)):
for pattern, template in patterns:
converter = self.url_converter(
name, hashed_files, template
)
converter = self.url_converter(name, hashed_files, template)
try:
content = pattern.sub(converter, content)
except ValueError as exc:
@@ -364,7 +321,7 @@ class HashedFilesMixin:
yield name, hashed_name, processed, substitutions
def clean_name(self, name):
return name.replace("\\", "/")
return name.replace('\\', '/')
def hash_key(self, name):
return name
@@ -406,21 +363,18 @@ class HashedFilesMixin:
class ManifestFilesMixin(HashedFilesMixin):
manifest_version = "1.0" # the manifest format standard
manifest_name = "staticfiles.json"
manifest_version = '1.0' # the manifest format standard
manifest_name = 'staticfiles.json'
manifest_strict = True
keep_intermediate_files = False
def __init__(self, *args, manifest_storage=None, **kwargs):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if manifest_storage is None:
manifest_storage = self
self.manifest_storage = manifest_storage
self.hashed_files = self.load_manifest()
def read_manifest(self):
try:
with self.manifest_storage.open(self.manifest_name) as manifest:
with self.open(self.manifest_name) as manifest:
return manifest.read().decode()
except FileNotFoundError:
return None
@@ -434,26 +388,24 @@ class ManifestFilesMixin(HashedFilesMixin):
except json.JSONDecodeError:
pass
else:
version = stored.get("version")
if version == "1.0":
return stored.get("paths", {})
raise ValueError(
"Couldn't load manifest '%s' (version %s)"
% (self.manifest_name, self.manifest_version)
)
version = stored.get('version')
if version == '1.0':
return stored.get('paths', {})
raise ValueError("Couldn't load manifest '%s' (version %s)" %
(self.manifest_name, self.manifest_version))
def post_process(self, *args, **kwargs):
self.hashed_files = {}
yield from super().post_process(*args, **kwargs)
if not kwargs.get("dry_run"):
if not kwargs.get('dry_run'):
self.save_manifest()
def save_manifest(self):
payload = {"paths": self.hashed_files, "version": self.manifest_version}
if self.manifest_storage.exists(self.manifest_name):
self.manifest_storage.delete(self.manifest_name)
payload = {'paths': self.hashed_files, 'version': self.manifest_version}
if self.exists(self.manifest_name):
self.delete(self.manifest_name)
contents = json.dumps(payload).encode()
self.manifest_storage._save(self.manifest_name, ContentFile(contents))
self._save(self.manifest_name, ContentFile(contents))
def stored_name(self, name):
parsed_name = urlsplit(unquote(name))
@@ -462,16 +414,14 @@ class ManifestFilesMixin(HashedFilesMixin):
cache_name = self.hashed_files.get(hash_key)
if cache_name is None:
if self.manifest_strict:
raise ValueError(
"Missing staticfiles manifest entry for '%s'" % clean_name
)
raise ValueError("Missing staticfiles manifest entry for '%s'" % clean_name)
cache_name = self.clean_name(self.hashed_name(name))
unparsed_name = list(parsed_name)
unparsed_name[2] = cache_name
# Special casing for a @font-face hack, like url(myfont.eot?#iefix")
# http://www.fontspring.com/blog/the-new-bulletproof-font-face-syntax
if "?#" in name and not unparsed_name[3]:
unparsed_name[2] += "?"
if '?#' in name and not unparsed_name[3]:
unparsed_name[2] += '?'
return urlunsplit(unparsed_name)
@@ -480,7 +430,6 @@ class ManifestStaticFilesStorage(ManifestFilesMixin, StaticFilesStorage):
A static file system storage backend which also saves
hashed copies of the files it saves.
"""
pass
@@ -13,7 +13,7 @@ def matches_patterns(path, patterns):
return any(fnmatch.fnmatchcase(path, pattern) for pattern in patterns)
def get_files(storage, ignore_patterns=None, location=""):
def get_files(storage, ignore_patterns=None, location=''):
"""
Recursively walk the storage directories yielding the paths
of all files that should be copied.
@@ -48,24 +48,16 @@ def check_settings(base_url=None):
if not base_url:
raise ImproperlyConfigured(
"You're using the staticfiles app "
"without having set the required STATIC_URL setting."
)
"without having set the required STATIC_URL setting.")
if settings.MEDIA_URL == base_url:
raise ImproperlyConfigured(
"The MEDIA_URL and STATIC_URL settings must have different values"
)
if (
settings.DEBUG
and settings.MEDIA_URL
and settings.STATIC_URL
and settings.MEDIA_URL.startswith(settings.STATIC_URL)
):
raise ImproperlyConfigured("The MEDIA_URL and STATIC_URL "
"settings must have different values")
if (settings.DEBUG and settings.MEDIA_URL and settings.STATIC_URL and
settings.MEDIA_URL.startswith(settings.STATIC_URL)):
raise ImproperlyConfigured(
"runserver can't serve media if MEDIA_URL is within STATIC_URL."
)
if (settings.MEDIA_ROOT and settings.STATIC_ROOT) and (
settings.MEDIA_ROOT == settings.STATIC_ROOT
):
raise ImproperlyConfigured(
"The MEDIA_ROOT and STATIC_ROOT settings must have different values"
)
if ((settings.MEDIA_ROOT and settings.STATIC_ROOT) and
(settings.MEDIA_ROOT == settings.STATIC_ROOT)):
raise ImproperlyConfigured("The MEDIA_ROOT and STATIC_ROOT "
"settings must have different values")
@@ -29,10 +29,10 @@ def serve(request, path, insecure=False, **kwargs):
"""
if not settings.DEBUG and not insecure:
raise Http404
normalized_path = posixpath.normpath(path).lstrip("/")
normalized_path = posixpath.normpath(path).lstrip('/')
absolute_path = finders.find(normalized_path)
if not absolute_path:
if path.endswith("/") or path == "":
if path.endswith('/') or path == '':
raise Http404("Directory indexes are not allowed here.")
raise Http404("'%s' could not be found" % path)
document_root, path = os.path.split(absolute_path)